如何随机化(洗牌)一个 JavaScript 数组?

IT技术 javascript arrays random shuffle
2020-12-20 16:07:24

我有一个这样的数组:

var arr1 = ["a", "b", "c", "d"];

我怎样才能随机化/洗牌呢?

6个回答

事实上的无偏洗牌算法是 Fisher-Yates(又名 Knuth)洗牌。

https://github.com/coolaj86/knuth-shuffle

你可以在这里看到一个很棒的可视化(以及链接到这个的原始帖子

function shuffle(array) {
  let currentIndex = array.length,  randomIndex;

  // While there remain elements to shuffle...
  while (currentIndex != 0) {

    // Pick a remaining element...
    randomIndex = Math.floor(Math.random() * currentIndex);
    currentIndex--;

    // And swap it with the current element.
    [array[currentIndex], array[randomIndex]] = [
      array[randomIndex], array[currentIndex]];
  }

  return array;
}

// Used like so
var arr = [2, 11, 37, 42];
shuffle(arr);
console.log(arr);

关于所用算法的更多信息

@ggorlen 在这种情况下转译是什么意思?你能给我们一个例子或进一步的解释吗?
2021-02-07 16:07:24
@ggorlen 谢谢!我现在明白了 - 我很困惑,因为我通常使用 Node,所以没有立即想到浏览器。谢谢澄清!
2021-02-09 16:07:24
上面的答案跳过了元素 0,条件应该i--不是--i此外,该测试if (i==0)...是多余的,因为如果i == 0同时循环将永远不会被输入。Math.floor使用 可以更快地调用...| 0任一拍子tempj可以被移除并且该值被直接分配给myArray的[I]Ĵ适当。
2021-02-22 16:07:24
@RobG 上面的实现在功能上是正确的。在 Fisher-Yates 算法中,循环并不意味着为数组中的第一个元素运行。查看维基百科,那里有其他也跳过第一个元素的实现。另请查看这篇文章,其中讨论了为什么循环不为第一个元素运行很重要。
2021-03-05 16:07:24
如果您要在繁忙的循环中进行解构赋值,请务必进行转译——分配对象的成本很高。
2021-03-05 16:07:24

这是Durstenfeld shuffle的 JavaScript 实现,这是 Fisher-Yates 的优化版本:

/* Randomize array in-place using Durstenfeld shuffle algorithm */
function shuffleArray(array) {
    for (var i = array.length - 1; i > 0; i--) {
        var j = Math.floor(Math.random() * (i + 1));
        var temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
}

它为每个原始数组元素挑选一个随机元素,并将其从下一次抽奖中排除,就像从一副纸牌中随机挑选一样。

这种巧妙的排除将选择的元素与当前元素交换,然后从余数中选择下一个随机元素,向后循环以获得最佳效率,确保随机选择被简化(它总是可以从 0 开始),从而跳过最后一个元素。

算法运行时是O(n). 请注意,shuffle 是就地完成的,因此如果您不想修改原始数组,请先使用.slice(0).


编辑:更新到 ES6 / ECMAScript 2015

新的 ES6 允许我们一次分配两个变量。这在我们想要交换两个变量的值时特别方便,因为我们可以在一行代码中完成。这是使用此功能的相同功能的较短形式。

function shuffleArray(array) {
    for (let i = array.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [array[i], array[j]] = [array[j], array[i]];
    }
}
这是相同的功能,但已压缩: function shuffle(a){for(var j,i=a.length-1;i>0;i--){j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]]}}
2021-02-18 16:07:24
此答案中的实现有利于数组的下端。发现了困难的方式Math.random() should not be multiplied with the loop counter + 1, but with 数组长度()`。请参阅在 JavaScript 中生成特定范围内的随机整数?一个非常全面的解释。
2021-02-19 16:07:24
@MarjanVenema 不确定您是否仍在观看此空间,但此答案正确的,并且您建议的更改实际上引入了偏见。请参阅blog.codinghorror.com/the-danger-of-naivete以了解有关此错误的精彩报道
2021-02-19 16:07:24
用引用重复 user94559 的评论en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle要交换的元素 (j)应该在 0 和当前数组索引 (i) 之间
2021-02-22 16:07:24

您可以使用 map 和 sort 轻松完成:

let unshuffled = ['hello', 'a', 't', 'q', 1, 2, 3, {cats: true}]

let shuffled = unshuffled
  .map((value) => ({ value, sort: Math.random() }))
  .sort((a, b) => a.sort - b.sort)
  .map(({ value }) => value)
  1. 我们把数组中的每个元素放在一个对象中,并给它一个随机排序键
  2. 我们使用随机键排序
  3. 我们取消映射以获取原始对象

您可以对多态数组进行 shuffle,排序与 Math.random 一样随机,这对于大多数用途来说已经足够了。

由于元素根据每次迭代都不会重新生成的一致键进行排序,并且每次比较都从相同的分布中提取,因此 Math.random 分布中的任何非随机性都被抵消了。

速度

时间复杂度为 O(N log N),与快速排序相同。空间复杂度为 O(N)。这不如 Fischer Yates shuffle 有效,但在我看来,代码明显更短且功能更多。如果你有一个大数组,你当然应该使用 Fischer Yates。如果你有一个包含几百个项目的小数组,你可以这样做。

出于多种原因,这是此处(对于短数组)的最佳答案。对我来说,它真的很有用,因为我在 2021 年使用 React,它最适合这样的函数式方法。
2021-02-14 16:07:24
非常好。这是js 中Schwartzian 变换
2021-02-21 16:07:24

警告!不推荐
使用这种算法,因为它效率低下且有很强的偏见看评论。它被留在这里以备将来参考,因为这种想法并不罕见。

[1,2,3,4,5,6].sort( () => .5 - Math.random() );

这个https://javascript.info/array-methods#shuffle-an-array教程直接解释了差异。

投反对票,因为这并不是那么随机。我不知道为什么它有这么多赞成票。不要使用这种方法。它看起来很漂亮,但并不完全正确。以下是 10,000 次迭代后的结果,关于数组中每个数字命中索引 [0] 的次数(我也可以给出其他结果):1 = 29.19%, 2 = 29.53%, 3 = 20.06%, 4 = 11.91%, 5 = 5.99%, 6 = 3.32%
2021-02-12 16:07:24
问题是它不是确定性的,这会给出错误的结果(如果 1 > 2 和 2 > 3,应该给出 1 > 3,但这不能保证。这会混淆排序,并给结果注释来自@radtad)。
2021-02-12 16:07:24
2021-02-17 16:07:24
如果您需要随机化相对较小的数组而不处理加密事物,那很好。我完全同意,如果您需要更多随机性,则需要使用更复杂的解决方案。
2021-02-22 16:07:24

人们可以(或应该)将其用作 Array 的原型:

来自克里斯托夫:

Array.prototype.shuffle = function() {
  var i = this.length, j, temp;
  if ( i == 0 ) return this;
  while ( --i ) {
     j = Math.floor( Math.random() * ( i + 1 ) );
     temp = this[i];
     this[i] = this[j];
     this[j] = temp;
  }
  return this;
}