使用JavaScript Array.sort()方法进行改组是否正确?


126

我正在用他的JavaScript代码帮助某人,而我的眼睛被一段看起来像这样的部分所吸引:

function randOrd(){
  return (Math.round(Math.random())-0.5);
}
coords.sort(randOrd);
alert(coords);

我的第一个想法是:嘿,这可能行不通!但是后来我做了一些实验,发现它确实至少提供了很好的随机结果。

然后,我进行了一些网络搜索,几乎在顶部找到了一篇文章,该文章是最不经意地被复制的。看起来像一个非常受人尊敬的网站和作者。

但是我的直觉告诉我,这一定是错误的。特别是由于ECMA标准未指定排序算法。我认为不同的排序算法将导致不同的不均匀混洗。一些排序算法甚至可能无限循环...

但是你觉得呢?

还有另一个问题……我现在将如何去衡量这种混洗技术的结果有多随机?

更新:我做了一些测量,并将结果发布在下面作为答案之一。


只是要注意,仅对符号计数进行四舍五入是没有用的
bormat

2
我发现它似乎提供了很好的随机结果。 ”- 真的吗?
Bergi

Answers:


109

这从来不是我最喜欢的改组方式,部分原因您所说的特定于实现的。尤其是,我似乎还记得,从Java或.NET(不确定哪个)进行标准库排序通常可以检测到是否最终在某些元素之间进行了不一致的比较(例如,首先声明A < BB < C,然后声明C < A)。

最后,它会以比您真正需要的更复杂的方式(就执行时间而言)结束。

我更喜欢shuffle算法,该算法可以有效地将集合划分为“ shuffled”(在集合的开头,最初是空的)和“ unshuffled”(集合的其余部分)。在算法的每个步骤中,选择一个随机的未改组元素(可以是第一个元素),然后将其与第一个未改组的元素交换-然后将其视为已改组(即,在精神上移动分区以将其包括在内)。

这是O(n),只需要对随机数生成器进行n-1次调用,这很好。它还会产生真正的随机播放-任何元素,无论其原始位置如何(假设RNG合理),都有1 / n的机会出现在每个空间中。排序后的版本近似于均匀分布(假设随机数生成器没有两次选择相同的值,如果返回随机双精度数则不太可能),但我发现更容易推断出随机版本:)

这种方法称为Fisher-Yates随机播放

我认为最好的做法是对这种改编进行一次编码,然后在需要改编项目的任何地方重用它。然后,您不必担心可靠性或复杂性方面的排序实现。只有几行代码(我不会在JavaScript中尝试!)

在洗牌维基百科的文章值得一读的一般洗牌的差实现的部分,所以你知道,以避免什么- (尤其是洗牌的算法部分)有关排序随机投影会谈。


5
雷蒙德·陈(Raymond Chen)深入探讨了排序比较功能遵循规则的重要性:blogs.msdn.com/oldnewthing/archive/2009/05/08/9595334.aspx
Jason Kresowaty,2009年

1
如果我的推论是正确的,则排序后的版本不会产生“真正的”随机播放!
Christoph

@Christoph:考虑到这一点,即使保证rand(x)恰好在其范围内,即使Fisher-Yates也只能给出“完美”的分布。考虑到RNG通常在某些x上有2 ^ x个可能的状态,我认为对于rand(3)来说它甚至不完全是。
乔恩·斯基特

@Jon:但是Fisher-Yates将为2^x每个数组索引创建状态,即总共将有2 ^(xn)个状态,该状态应该比2 ^ c大很多-有关详细信息,请参阅我的编辑答案
Christoph

@Christoph:我可能没有正确解释自己。假设您只有3个元素。您从第3个元素中随机选择第一个元素。要获得完全均匀的分布,必须能够完全均匀地选择[0,3)范围内的随机数-如果PRNG具有2 ^ n可能的状态,你不能做到这一点-一个或两个的可能性将有小幅发生的概率较高。
乔恩·斯基特

118

在Jon讲完理论之后,这里是一个实现:

function shuffle(array) {
    var tmp, current, top = array.length;

    if(top) while(--top) {
        current = Math.floor(Math.random() * (top + 1));
        tmp = array[current];
        array[current] = array[top];
        array[top] = tmp;
    }

    return array;
}

该算法为O(n),而排序应为O(n log n)。根据与本地sort()函数相比执行JS代码的开销,这可能会导致性能上的显着差异,差异应随数组大小而增加。


在对bobobobo答案的评论中,我说过所讨论的算法可能不会产生均匀分布的概率(取决于的实现sort())。

我的论点是这样的:排序算法需要一定数量c的比较,例如c = n(n-1)/2针对Bubblesort。我们的随机比较功能使每个比较的结果均具有相同的可能性,即,结果均2^c 具有相同的可能性。现在,每个结果必须对应于n!数组条目的排列之一,这使得在一般情况下不可能进行均匀分布。(这是一种简化,因为需要进行的实际比较次数取决于输入数组,但是断言仍应保持。)

正如乔恩(Jon)所指出的那样,仅此一项就没有理由使Fisher-Yates胜过使用sort(),因为随机数生成器还将有限数量的伪随机值映射到n!排列。但是Fisher-Yates的结果仍然应该更好:

Math.random()产生范围内的伪随机数[0;1[。由于JS使用双精度浮点值,因此它对应于2^x其中的可能值52 ≤ x ≤ 63(我太懒了,无法找到实际的数字)。Math.random()如果原子事件的数量处于相同数量级,则使用生成的概率分布将无法正常运行。

使用Fisher-Yates时,相关参数是数组的大小,2^52由于实际限制,该大小永远都不能接近。

当使用随机比较函数进行排序时,该函数基本上只关心返回值是正数还是负数,因此这永远不会成为问题。但是有一个类似的结果:由于比较函数的行为良好,因此如上所述,2^c可能的结果是同等可能的。如果是,c ~ n log n2^c ~ n^(a·n)在哪里a = const,即使排序算法在哪里均匀地映射到排列上,也至少有可能2^c与(甚至小于)幅度相等n!,从而导致分布不均。如果有任何实际影响超出我的范围。

真正的问题是排序算法不能保证均匀地映射到排列上。可以很容易地看出Mergesort是对称的,但是对诸如Bubblesort或更重要的是Quicksort或Heapsort之类的推理却并非如此。


底线:只要sort()使用Mergesort,除了在极端情况下(至少我希望是在极端情况下),您应该都应该相当安全2^c ≤ n!,如果没有,所有选择都将关闭。


感谢您的实施。太快了!特别是与我本人自己写的那首慢话相比。
Rene Saarsoo 09年

1
如果您使用的是underscore.js库,则可以使用上述Fisher-Yates随机播放方法扩展它:github.com/ryantenney/underscore/commit/…–
Steve

非常感谢您,您和Johns的回答共同帮助我解决了一个问题,我和一位同事共花费了将近4个小时!我们最初有一种与OP相似的方法,但是发现随机性非常不稳定,因此我们采用了您的方法,并对其进行了少许更改,以使用一些jquery来混淆图像列表(用于滑块)以获取一些图像。很棒的随机化。
Hello World

16

我对这种随机排序的结果的随机性做了一些测量。

我的技术是取一个小的数组[1,2,3,4]并创建它的所有(4!= 24)排列。然后,我将改组函数大量应用于数组,并计算每次排列生成多少次。好的混洗算法会在所有排列中平均分配结果,而不好的算法不会产生相同的结果。

使用以下代码,我在Firefox,Opera,Chrome,IE6 / 7/8中进行了测试。

令我惊讶的是,随机排序和真正的混洗都创建了同样均匀的分布。因此,正如许多人所建议的那样,主要浏览器似乎正在使用合并排序。当然,这并不意味着没有浏览器,它的功能有所不同,但是我要说的是,这种随机排序方法足够可靠,可以在实践中使用。

编辑:该测试并未真正正确地测量其随机性或缺乏性。查看我发布的其他答案。

但是在性能方面,克里斯托弗(Cristoph)提供的混洗功能无疑是赢家。即使对于小的四元素数组,真正的随机播放速度也大约是随机排序的两倍!

// Cristoph发布的随机播放功能。
var shuffle = function(array){
    var tmp,current,top = array.length;

    if(top)while(-top){
        当前= Math.floor(Math.random()*(top + 1));
        tmp = array [当前];
        array [current] = array [top];
        array [top] = tmp;
    }

    返回数组;
};

//随机排序函数
var rnd = function(){
  返回Math.round(Math.random())-0.5;
};
var randSort = function(A){
  返回A.sort(rnd);
};

var排列= function(A){
  如果(A.length == 1){
    返回[A];
  }
  其他{
    var perms = [];
    对于(var i = 0; i <A.length; i ++){
      var x = A.slice(i,i + 1);
      var xs = A.slice(0,i).concat(A.slice(i + 1));
      var subperms =排列(xs);
      for(var j = 0; j <subperms.length; j ++){
        perms.push(x.concat(subperms [j]));
      }
    }
    返回烫发
  }
};

var test = function(A,迭代,函数){
  //初始化排列
  var stats = {};
  var perms = permutations(A);
  为(var i in perms){
    stats [“” + perms [i]] = 0;
  }

  //多次洗牌并收集统计数据
  var start = new Date();
  for(var i = 0; i <iterations; i ++){
    var shuffled = func(A);
    stats [“” + shuffled] ++;
  }
  var end = new Date();

  //格式化结果
  var arr = [];
  对于(统计信息中的var i){
    arr.push(i +“” + stats [i]);
  }
  返回arr.join(“ \ n”)+“ \ n \ n花费的时间:” +((结束-开始)/ 1000)+“秒。
};

alert(“ random sort:” + test([1,2,3,4],100000,randSort));
alert(“ shuffle:” + test([1,2,3,4],100000,shuffle));

11

有趣的是,Microsoft在其挑选随机浏览器页面中使用了相同的技术

他们使用了稍微不同的比较功能:

function RandomSort(a,b) {
    return (0.5 - Math.random());
}

在我看来几乎一样,但事实并非如此随机...

因此,我再次使用链接文章中使用的相同方法进行了一些测试,结果确实是-随机排序方法产生了错误的结果。新的测试代码在这里:

function shuffle(arr) {
  arr.sort(function(a,b) {
    return (0.5 - Math.random());
  });
}

function shuffle2(arr) {
  arr.sort(function(a,b) {
    return (Math.round(Math.random())-0.5);
  });
}

function shuffle3(array) {
  var tmp, current, top = array.length;

  if(top) while(--top) {
    current = Math.floor(Math.random() * (top + 1));
    tmp = array[current];
    array[current] = array[top];
    array[top] = tmp;
  }

  return array;
}

var counts = [
  [0,0,0,0,0],
  [0,0,0,0,0],
  [0,0,0,0,0],
  [0,0,0,0,0],
  [0,0,0,0,0]
];

var arr;
for (var i=0; i<100000; i++) {
  arr = [0,1,2,3,4];
  shuffle3(arr);
  arr.forEach(function(x, i){ counts[x][i]++;});
}

alert(counts.map(function(a){return a.join(", ");}).join("\n"));

我不明白为什么它必须是0.5-Math.random(),为什么不只是Math.random()?
亚历山大·米尔斯

1
@AlexanderMills:比较器功能传递给sort()应该返回一个大于,小于,或等于零取决于所述比较ab。(developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/...
LarsH

@LarsH是的,这很有道理
Alexander Mills

9

我已经在我的网站上放置了一个简单的测试页,显示了您当前的浏览器与其他使用不同方法进行改组的流行浏览器之间的偏差。它显示了仅使用Math.random()-0.5,没有受到偏见的另一个“随机”混洗以及上面提到的Fisher-Yates方法的严重偏见。

您会发现,在某些浏览器中,某些元素在“随机播放”期间根本不会改变的可能性高达50%!

注意:通过将代码更改为以下内容,您可以使@Christoph的Fisher-Yates改编对于Safari的执行速度稍快一些:

function shuffle(array) {
  for (var tmp, cur, top=array.length; top--;){
    cur = (Math.random() * (top + 1)) << 0;
    tmp = array[cur]; array[cur] = array[top]; array[top] = tmp;
  }
  return array;
}

测试结果:http : //jsperf.com/optimized-fisher-yates


5

我认为这对于您不挑剔分发并且希望源代码较小的情况很好。

在JavaScript(源不断传输)中,较小的带宽会有所不同。


2
事实是,您几乎总是对分配比自己想像的要挑剔,对于“小代码”,总会有arr = arr.map(function(n){return [Math.random(),n]}).sort().map(function(n){return n[1]});,其优点是不必花太多时间才能真正分配出去。还有非常压缩的Knuth / FY随机播放变体。
Daniel Martin

@DanielMartin单线应该是一个答案。另外,为避免解析错误,需要添加两个分号,使其看起来像这样:arr = arr.map(function(n){return [Math.random(),n];}).sort().map(function(n){return n[1];});
Giacomo1968

2

当然,这是一个hack。实际上,无限循环算法是不可能的。如果要对对象进行排序,则可以遍历coords数组并执行类似的操作:

for (var i = 0; i < coords.length; i++)
    coords[i].sortValue = Math.random();

coords.sort(useSortValue)

function useSortValue(a, b)
{
  return a.sortValue - b.sortValue;
}

(然后再次遍历它们以删除sortValue)

仍然是一个hack。如果您想做得很好,就必须用困难的方法来做:)


2

已经四年了,但是我想指出的是,无论使用哪种排序算法,随机比较器方法都不会正确分布。

证明:

  1. 对于n元素数组,存在确切的n!排列(即可能的混洗)。
  2. 随机播放期间的每个比较都是在两组排列之间进行选择。对于随机比较器,选择每个集合的机会为1/2。
  3. 因此,对于每个置换p,以置换p结尾的机会是分母为2 ^ k(对于某些k)的分数,因为它是此类分数的总和(例如1/8 + 1/16 = 3/16 )。
  4. 对于n = 3,存在六个同样可能的排列。那么,每个排列的机会是1/6。1/6不能表示为以2的幂为分母的分数。
  5. 因此,硬币翻转排序将永远不会导致随机分配的公平分布。

唯一可能正确分配的大小为n = 0,1,2。


作为练习,请尝试为n = 3绘制不同排序算法的决策树。


证明中有一个空白:如果排序算法取决于比较器的一致性,并且在比较器不一致的情况下运行时间不受限制,则它可以具有无限的概率之和,即使相加达到1/6,总和中的每个分母都是2的幂。尝试找到一个。

同样,如果比较器有固定的机会给出任一答案(例如(Math.random() < P)*2 - 1,对于constant P),则上述证明成立。如果比较器根据先前的答案更改赔率,则有可能产生公平的结果。为给定的排序算法找到这种比较器可能是一篇研究论文。


1

如果您使用的是D3,则有一个内置的随机播放功能(使用Fisher-Yates):

var days = ['Lundi','Mardi','Mercredi','Jeudi','Vendredi','Samedi','Dimanche'];
d3.shuffle(days);

这是Mike讨论的细节:

http://bost.ocks.org/mike/shuffle/


0

这是一种使用单个数组的方法:

基本逻辑是:

  • 从n个元素的数组开始
  • 从数组中删除随机元素,然后将其推入数组
  • 从数组的前n-1个元素中删除一个随机元素,并将其推入数组
  • 从数组的前n-2个元素中删除一个随机元素,并将其推入数组
  • ...
  • 删除数组的第一个元素并将其推入数组
  • 码:

    for(i=a.length;i--;) a.push(a.splice(Math.floor(Math.random() * (i + 1)),1)[0]);

    您的实现存在很高的风险,要让大量元素保持不变。它们只是在整个数组中被劣势元素顶上的数量所移动。在改组中绘制了一个模式,使其变得不可靠。
    Kir Kanos 2014年

    @KirKanos,我不确定我是否理解您的评论。我提出的解决方案是O(n)。肯定会“触摸”每个元素。这是一个小提琴来演示。
    ic3b3rg 2014年

    0

    您可以使用该Array.sort()函数来随机排列数组吗?是的。

    结果足够随机吗?

    考虑以下代码片段:

    var array = ["a", "b", "c", "d", "e"];
    var stats = {};
    array.forEach(function(v) {
      stats[v] = Array(array.length).fill(0);
    });
    //stats = {
    //    a: [0, 0, 0, ...]
    //    b: [0, 0, 0, ...]
    //    c: [0, 0, 0, ...]
    //    ...
    //    ...
    //}
    var i, clone;
    for (i = 0; i < 100; i++) {
      clone = array.slice(0);
      clone.sort(function() {
        return Math.random() - 0.5;
      });
      clone.forEach(function(v, i) {
        stats[v][i]++;
      });
    }
    
    Object.keys(stats).forEach(function(v, i) {
      console.log(v + ": [" + stats[v].join(", ") + "]");
    })

    样本输出:

    a [29, 38, 20,  6,  7]
    b [29, 33, 22, 11,  5]
    c [17, 14, 32, 17, 20]
    d [16,  9, 17, 35, 23]
    e [ 9,  6,  9, 31, 45]

    理想情况下,计数应均匀分布(对于上面的示例,所有计数应在20左右)。但事实并非如此。显然,分布取决于浏览器实现哪种排序算法以及如何迭代数组项进行排序。

    本文提供了更多的见解:
    不应使用Array.sort()来随机播放数组


    -3

    没有什么问题。

    传递给.sort()的函数通常看起来像

    函数sortingFunc(first,second)
    {
      //示例:
      返回第一-第二;
    }
    

    您在sortingFunc中的工作是返回:

    • 如果负数先于负数,则为负数
    • 如果第一个应该在第二个之后
    • 如果它们完全相等,则返回0

    上面的排序功能将事物排序。

    如果您按原样随机返回-和+,则会得到随机排序。

    就像在MySQL中一样:

    从表ORDER BY rand()中选择*
    

    5
    还有就是什么不对这种方法:根据由JS实现中使用排序算法上,概率不会平均分配!
    Christoph

    这是我们实际上担心的事情吗?
    bobobobo

    4
    @bobobobo:是的,取决于应用程序,是的,有时我们会这样做;同样,正确工作的代码shuffle()只需写入一次,因此这并不是真正的问题:只需将代码段放入代码库中,并在需要时进行挖掘
    Christoph
    By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
    Licensed under cc by-sa 3.0 with attribution required.