数组乱序就是把数组存储值的顺序都打乱。
通常我们在做抽奖系统或者发牌等游戏时,会遇到数组乱序的问题。
举个例子:将 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
乱序。
通常我们最快想到的方法是利用 sort
。
function shuffle(arr) {
return arr.sort(() => (Math.random() - 0.5))
}
console.log(shuffle([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]))
// => [3, 5, 9, 10, 7, 6, 4, 8, 1, 2]
乍一看没问题,但是运行次数多了我们就会发现,末尾的数字为大数的概率较大,开始的数字为小数的概率较大。
我们可以验证下,10000 次随机测试,10 个位置,每个位置的平均值应该是一样的,即 (1+10)*10/2/10 = 5.5
:
const arr = [1, 2, 3, 4, 5, 6 ,7, 8, 9, 10]
function shuffle(arr) {
return arr.sort(() => (Math.random() - 0.5))
}
let resultArr = Array(10).fill(0)
for (let i = 0; i < 10000; i++) {
// sort 会改变原数组,必须用新数组来进行乱序
let newArr = [].concat(arr)
const tmp = shuffle(newArr)
resultArr.forEach((item, index) => {
// 不能直接改变 item 的值, item += tmp[index], 因为 forEach 不会改变原数组
resultArr[index] += tmp[index]
})
}
console.log(resultArr)
const average = resultArr.map(i => i/ 10000)
console.log(average)
// => [48544, 48860, 55333, 56927, 56797, 53396, 53790, 56762, 58967, 60624]
// => [4.8544, 4.886, 5.5333, 5.6927, 5.6797, 5.3396, 5.379, 5.6762, 5.8967, 6.0624]
可以看到,每个位置平均值有比较明显的误差。那么这是什么原因呢?
原来,在Chrome v8引擎源码中,处理sort方法时,使用了插入排序和快速排序两种方案。当目标数组长度小于10时,使用插入排序;反之,使用快速排序和插入排序的混合排序。
所以用 sort
方法乱序不准确的原因就在于:理想的方案是数组中每两个元素都要进行比较,这个比较有50%的交换位置概率。而在插入排序的算法中,当待排序元素跟有序元素进行比较时,一旦确定了位置,就不会再跟位置前面的有序元素进行比较,所以就乱序的不彻底。
可以利用洗牌算法来进行彻底的乱序。
洗牌算法的思路是:
先从数组末尾开始,选取最后一个元素,与数组中随机一个位置的元素交换位置;
然后在已经排好的最后一个元素以外的位置中,随机产生一个位置,让该位置元素与倒数第二个元素进行交换;
以此类推,打乱整个数组的顺序。
function shuffle(a) {
for (let i = a.length; i; i--) {
let j = Math.floor(Math.random() * i);
[a[i - 1], a[j]] = [a[j], a[i - 1]];
}
return a;
}
这时再测试下:
const arr = [1, 2, 3, 4, 5, 6 ,7, 8, 9, 10]
function shuffle(a) {
for (let i = a.length; i; i--) {
let j = Math.floor(Math.random() * i);
[a[i - 1], a[j]] = [a[j], a[i - 1]];
}
return a;
}
let resultArr = Array(10).fill(0)
for (let i = 0; i < 10000; i++) {
// sort 会改变原数组,必须用新数组来进行乱序
let newArr = [].concat(arr)
const tmp = shuffle(newArr)
resultArr.forEach((item, index) => {
// 不能直接改变 item 的值, item += tmp[index], 因为 forEach 不会改变原数组
resultArr[index] += tmp[index]
})
}
console.log(resultArr)
const average = resultArr.map(i => i/ 10000)
console.log(average)
// => [55070, 54854, 54588, 55169, 55458, 54670, 55311, 54944, 55030, 54906]
// => [5.507, 5.4854, 5.4588, 5.5169, 5.5458, 5.467, 5.5311, 5.4944, 5.503, 5.4906]
我们可以看到,已经是真正的乱序啦。