你真的会数组乱序吗?|洗牌算法

1,193 阅读2分钟

大家都知道,数组的排序是一个非常重要的算法,除此之外,数组的乱序也是一种算法哦!数组的乱序也是非常有讲究的,它要求数组中的数打乱后,每个数出现在任意位置的概率相同,话不多说,直接开始试一下吧...

首先 来看一种很可能写的错误算法

let arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
function shuffle(arr) {
    // 两个数交换的概率50%
    return arr.sort(() => Math.random() - 0.5);
}

得出结果

这样看数组好像已经乱序了,验证一下试试。

根据数组乱序的要求,我们将函数执行10000次,查看每个位置上的平均值

let t = 10000;
let arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
let res = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for(let i = 0; i < t; i++) {
    let sorted = shuffle(arr.slice(0));
    for( let i = 0; i < sorted.length; i++) {
        res[i] = sorted[i] + res[i];
    }
}
console.log(res.map(sum => sum / t))

我们可以看到,结果并不都在4.5附近,有些甚至小于4或大于5了,与平均值4.5差距很大,说明,这并没有将数字真正地打乱,得到的也不是真正的随机序列。

这是为什么呢?问题就出在sort这个API上,对于chrome浏览器而言,当数组长度在10以内时,sort()采用插入排序,反之,则混合使用快速排序和插入排序,这样会导致选取的两个交换位置的数不随机,导致数组也就没有真正打乱。

那么,正确的乱序算法是怎样的呢?下面重点来咯: 洗牌算法 先理一下思路:

  1. 取数组中未洗牌的最后一个数;
  2. 从未洗牌的数中,随机选取一个;
  3. 交换两数位置;
  4. 重复执行1-3,直至所有数洗牌完成洗牌。

代码如下:

let arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
function shuffle(arr) {
    let len = arr.length;
    for(let i = 0; i < len; i++) {
        let idx = Math.floor(Math.random() * (len - i)); //Math.floor 向下取整 Math.random [0,1)
        [arr[len - 1 - i], arr[idx]] = [arr[idx], arr[len - 1 - i]]; //交换两数
    }
    return arr;
}

得出结果

同样,验证一下每个数出现在任意位置的概率相同是否相等呢

let t = 10000;
let arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
let res = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for(let i = 0; i < t; i++) {
    let sorted = shuffle(arr.slice(0));
    for( let i = 0; i < sorted.length; i++) {
        res[i] = sorted[i] + res[i];
    }
}
console.log(res.map(sum => sum / t))

以上平均值都在4.5附近,也就是说,每个数出现在任意位置的概率相同,那么也就可以说,数组真正乱序啦。