function shuffle(arr) {
let random
const newArr = []
while (arr.length) {
random = Math.floor(Math.random() * arr.length)
newArr.push(arr[random])
arr.splice(random, 1)
}
return newArr
}
举例
假设我们有 1 ~ 8 的数字
表格每列分别表示:范围、随机数(被移除数的位置)、剩余未删除的数、已随机排列的数。
Range
Roll
Scratch
Result
1 2 3 4 5 6 7 8
现在,我们从 1 ~ 8 中随机选择一个数,得到随机数 k 为 3,然后在 Scratch 上删除第 k 个数字(即数字 3),并将其放到 Result 中:
Range
Roll
Scratch
Result
1 - 8
3
1 2 3 4 5 6 7 8
3
现在我们从 1 ~ 7 选择第二个随机数 k 为 4,然后在 Scratch 上删除第 k 个数字(即数字 5),并将其放到 Result 中:
-- To shuffle an array a of n elements (indices 0..n-1):
for i from n−1 downto 1 do // 数组从 n-1 到 0 循环执行 n 次
j ← random integer such that 0 ≤ j ≤ i // 生成一个 0 到 n-1 之间的随机索引
exchange a[j] and a[i] // 将交换之后剩余的序列中最后一个元素与随机选取的元素交换
本文介绍三种数组乱序的方式:
若要实现随机打乱数组的需求,不建议使用
arr.sort(() => Math.random() - 0.5)
。目前用得较多的是 Knuth-Durstenfeld Shuffle 算法(洗牌算法),前端常用的 Lodash 库里面的_.shuffle()
也是使用了这种算法。Array.prototype.sort 排序
最简单的乱序实现:
但实际上这种方法并不能真正的随机打乱数组。在多次执行后,每个元素有很大几率还在它原来的位置附近出现。详见:常用的 sort 打乱数组方法真的有用?
Fisher–Yates Shuffle(Fisher and Yates' original method)
由 Ronald Fisher 和 Frank Yates 提出的 Fisher–Yates shuffle 算法思想,大致如下:
实现
举例
假设我们有 1 ~ 8 的数字
现在,我们从 1 ~ 8 中随机选择一个数,得到随机数 k 为 3,然后在 Scratch 上删除第 k 个数字(即数字 3),并将其放到 Result 中:
34 5 6 7 8现在我们从 1 ~ 7 选择第二个随机数 k 为 4,然后在 Scratch 上删除第 k 个数字(即数字 5),并将其放到 Result 中:
3456 7 8现在我们从 1 ~ 6 选择下一个随机数,然后从 1 ~ 5 选择依此类推,总是重复上述过程:
345678345678345678123456781234567812345678Knuth-Durstenfeld Shuffle(The modern algorithm)
Richard Durstenfeld 于 1964 年推出了现代版本的 Fisher–Yates shuffle,并由 Donald E. Knuth 在 The Art of Computer Programming 以 “Algorithm P (Shuffling)” 进行了推广。Durstenfeld 所描述的算法与 Fisher 和 Yates 所给出的算法有很小的差异,但意义重大。
Durstenfeld 的解决方案是将“删除”的数字移至数组末尾,即将每个被删除数字与最后一个未删除的数字进行交换。
实现
Knuth-Durstenfeld Shuffle 将算法的时间复杂度降低到
O(n)
,而 Fisher–Yates shuffle 的时间复杂度为O(n2)
。后者在计算机实现过程中,将花费不必要的时间来计算每次剩余的数字(可以理解成数组长度)。举例
同样,假设我们有 1 ~ 8 的数字
我们从 1 ~ 8 中随机选择一个数,得到随机数 k 为 6,然后交换 Scratch 中的第 6 和第 8 个数字:
接着,从 1 ~ 7 中随机选择一个数,得到随机数 k 为 2,然后交换 Scratch 中的第 2 和第 7 个数字:
继续,下一个随机数是1 ~ 6,得到的随机数恰好是 6,这意味着我们将列表中的第 6 个数字保留下来(经过上面的交换,现在是 8),然后移到下一个步。同样,我们以相同的方式进行操作,直到完成排列:
因此,结果是
7 5 4 3 1 8 2 6
。References