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

问题描述 投票:123回答:12

我用他的JavaScript代码帮助了一个人,我的眼睛被一个看起来像这样的部分抓住了:

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

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

然后我做了一些网络搜索,几乎在顶部发现了一个article,从这个代码最简单的复制。看起来像一个相当可敬的网站和作者......

但我的直觉告诉我,这一定是错的。特别是因为ECMA标准没有规定排序算法。我认为不同的排序算法会导致不同的非均匀混洗。一些排序算法甚至可能无限循环...

但你怎么看?

而另一个问题是......现在我将如何衡量这种改组技术的结果是多么随机?

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

javascript random sorting shuffle
12个回答
108
投票

它从来都不是我最喜欢的洗牌方式,部分原因是因为它是特定于实现的。特别是,我似乎记得从Java或.NET(不确定哪个)排序的标准库经常可以检测到你是否最终在一些元素之间进行了不一致的比较(例如,你首先声称A < BB < C,但是然后C < A) 。

它最终会比你真正需要的更复杂(在执行时间方面)洗牌。

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

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

这种方法称为Fisher-Yates shuffle

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

Wikipedia article on shuffling(特别是shuffle算法部分)讨论了对随机投影进行排序的问题 - 一般来说,值得阅读关于糟糕的混乱实现的部分,所以你知道要避免什么。


0
投票

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

基本逻辑是:

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

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

    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() should not be used to shuffle an array


    -3
    投票

    没有什么问题。

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

    function sortingFunc( first, second )
    {
      // example:
      return first - second ;
    }
    

    您在sortingFunc中的工作是返回:

    • 如果先在第二个之前进行,则为负数
    • 一个正数,如果第一个应该在第二个之后
    • 如果它们完全相等则为0

    上面的排序功能使事情井然有序。

    如果你随机返回-s和+,你得到一个随机排序。

    像MySQL一样:

    SELECT * from table ORDER BY rand()
    

    116
    投票

    在Jon已经covered the theory之后,这是一个实现:

    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)。根据执行JS代码与本机sort()函数相比的开销,这可能会导致noticable difference in performance随着数组大小的增加而增加。


    在对bobobobo's answer的评论中,我说有问题的算法可能不会产生均匀分布的概率(取决于sort()的实现)。

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

    正如乔恩指出的那样,仅凭这一点就没有理由更喜欢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 n然后2^c ~ n^(a·n)在哪里a = const,这使得它至少有可能2^cn!具有相同的量级(或甚至更小)并因此导致不均匀分布,即使排序算法在哪里均匀地映射到permutaions。如果这有任何实际影响超出我。

    真正的问题是不能保证排序算法均匀地映射到排列上。很容易看出Mergesort的确是对称的,但是像Bubblesort或更重要的是Quicksort或Heapsort这样的推理并不是。


    底线:只要sort()使用Mergesort,你应该合理安全,除了在角落的情况下(至少我希望2^c ≤ n!是一个角落的情况),如果没有,所有的赌注都关闭。


    16
    投票

    我做了一些关于随机排序结果随机性的测量结果......

    我的技术是采用一个小数组[1,2,3,4]并创建它的所有(4!= 24)排列。然后,我会将洗牌函数多次应用于数组,并计算每个排列生成的次数。一个好的改组算法会在所有排列上非常均匀地分配结果,而一个糟糕的算法不会产生统一的结果。

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

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

    编辑:这个测试没有真正正确地测量随机性或缺乏。看到我发布的其他答案。

    但在表演方面,Cristoph给出的随机播放功能是一个明显的赢家。即使对于小型四元素阵列,真正的shuffle执行速度也是随机排序的两倍!

    // The shuffle function posted by Cristoph.
    var shuffle = function(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;
    };
    
    // the random sort function
    var rnd = function() {
      return Math.round(Math.random())-0.5;
    };
    var randSort = function(A) {
      return A.sort(rnd);
    };
    
    var permutations = function(A) {
      if (A.length == 1) {
        return [A];
      }
      else {
        var perms = [];
        for (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 = permutations(xs);
          for (var j=0; j<subperms.length; j++) {
            perms.push(x.concat(subperms[j]));
          }
        }
        return perms;
      }
    };
    
    var test = function(A, iterations, func) {
      // init permutations
      var stats = {};
      var perms = permutations(A);
      for (var i in perms){
        stats[""+perms[i]] = 0;
      }
    
      // shuffle many times and gather stats
      var start=new Date();
      for (var i=0; i<iterations; i++) {
        var shuffled = func(A);
        stats[""+shuffled]++;
      }
      var end=new Date();
    
      // format result
      var arr=[];
      for (var i in stats) {
        arr.push(i+" "+stats[i]);
      }
      return arr.join("\n")+"\n\nTime taken: " + ((end - start)/1000) + " seconds.";
    };
    
    alert("random sort: " + test([1,2,3,4], 100000, randSort));
    alert("shuffle: " + test([1,2,3,4], 100000, shuffle));
    

    11
    投票

    有趣的是,微软在他们的随机浏览器页面中使用了相同的技术。

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

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

    看起来和我几乎一样,但是it turned out to be not so 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"));
    

    9
    投票

    我已经在我的网站上放置了a simple test page,显示当前浏览器与其他流行浏览器的偏见,使用不同的方法进行随机播放。它显示了使用Math.random()-0.5的可怕偏见,另一个没有偏差的“随机”shuffle,以及上面提到的Fisher-Yates方法。

    你可以看到,在某些浏览器中,在'shuffle'期间,某些元素根本不会改变位置的可能性高达50%!

    注意:通过将代码更改为以下内容,您可以通过@Christoph实现Fisher-Yates shuffle稍微快一点的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;
    }
    

    Test results: http://jsperf.com/optimized-fisher-yates


    5
    投票

    我认为这对你不喜欢发行并且希望源代码很小的情况很好。

    在JavaScript(源不断传输)中,small会对带宽成本产生影响。


    2
    投票

    当然,这是一个黑客。实际上,不可能有无限循环算法。如果你正在对对象进行排序,你可以遍历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)

    但仍然是一个黑客。如果你想做得很好,你必须这么做:)


    2
    投票

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

    证明:

    1. 对于n元素的数组,确切地存在n!排列(即可能的shuffle)。
    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绘制不同排序算法的决策树。


    证明中存在一个缺口:如果排序算法取决于比较器的一致性,并且具有不一致的运算符和不一致的比较器,它可以具有无限的概率总和,即使总和中的每个分母都是2的幂。试着找到一个。

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


    1
    投票

    如果你使用D3,则有一个内置的shuffle功能(使用Fisher-Yates):

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

    以下是Mike详细介绍它:

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

    © www.soinside.com 2019 - 2024. All rights reserved.