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

时间:2009-06-07 20:56:09

标签: javascript random sorting shuffle

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

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

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

然后我做了一些网络搜索,几乎在顶部找到了article,这段代码最简单地被复制。看起来像一个非常受人尊敬的网站和作者...

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

但你怎么看?

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

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

12 个答案:

答案 0 :(得分:114)

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


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

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

正如乔恩所指出的那样,单凭这一点并不是因为随机数生成器也会将有限数量的伪随机值映射到sort()排列,因此没有理由更喜欢Fisher-Yates使用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^c至少可能n!sort()具有相同的幅度,从而导致以不均匀的分布,即使排序算法在哪里均匀地映射到permutaions。如果这有任何实际影响超出我的范围。

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


底线:只要2^c ≤ n!使用Mergesort,你应该在角落情况下是合理安全的(至少我希望{{1}}是一个角落(如果没有),所有投注均已结束。

答案 1 :(得分:108)

它从来都不是我最喜欢的改组方式,部分原因是因为 特定于实现,正如你所说的那样。特别是,我似乎记得从Java或.NET(不确定哪个)排序的标准库经常可以检测到您是否最终在某些元素之间进行了不一致的比较(例如,您首先声明A < B和{{ 1}},然后是B < C)。

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

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

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

此方法称为Fisher-Yates shuffle

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

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

答案 2 :(得分:16)

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

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

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

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

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

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

// 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));

答案 3 :(得分:11)

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

他们使用了稍微不同的比较函数:

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"));

答案 4 :(得分:9)

我已将a simple test page放在我的网站上,显示当前浏览器与使用不同方法进行随机播放的其他流行浏览器的偏见。它显示了使用Math.random()-0.5的可怕偏见,另一个没有偏差的'随机'洗牌,以及上面提到的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;
}

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

答案 5 :(得分:5)

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

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

答案 6 :(得分: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)

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

答案 7 :(得分: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. 因此,硬币翻转排序永远不会导致洗牌的公平分配。
  6. 唯一可能正确分布的尺寸是n = 0,1,2。


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


    证明中存在一个缺口:如果排序算法取决于比较器的一致性,并且具有不一致的运行时与不一致的比较器,它可以具有无限的概率总和,允许加起来为1 / 6即使总和中的每个分母都是2的幂。试着找一个。

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

答案 8 :(得分:1)

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

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

Mike正在详细介绍它:

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

答案 9 :(得分: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

答案 10 :(得分:0)

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

基本逻辑是:

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

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

    答案 11 :(得分:-4)

    它没有任何问题。

    传递给.sort()的函数通常类似于

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

    您在sortingFunc中的工作是返回:

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

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

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

    与MySQL一样:

    SELECT * from table ORDER BY rand()