本文对解释洗牌算法比较简略,主要在于证明其随机性
本文是学习洗牌算法的心得,表达不当或理解不对的地方,感谢您点评指正~
打乱一个数组
打乱一个没有重复元素的数组。
比如 const arr = [1, 2, 3, 4, 5], shuffle(arr) 后返回一个随机的数组。
随机的意思是数组上的每个数字出现在每一个位置的可能性都是随机的
Array.prototype.sort 做得到吗?代码如下
function shuffle(arr) {
return arr.slice().sort(function(){ return Math.random() >= 0.5 ? -1 : 1;});
}
由于浏览器内核各自实现不同 Array.protype.sort 可能使用快排、归并,甚至冒泡等,所以无法保证其随机性
洗牌算法
上面我们解释了随机的意思,数组上的每个数字出现在每一个位置的可能性都是随机的。有一种比较常用的算法,洗牌算法。(参阅 维基百科-洗牌算法)下面我们使用 JavaScript 实现它
function shuffle() {
const arr = this.nums.slice();
let choice = arr.length; // 这个 choice 有两层含义
while (choice > 0) { // choice > 0 表示可以选择的个数大于 0
let j = Math.floor(Math.random() * choice); // 第一层含义:表示在多少个数(选择)里随机选一个,获取随机索引 [0, choice)
choice--; // choice-- 得到的是另一个含义,也就是索引、所在位置;由于数组是以 0 作为下标,需要往左偏移,
[arr[j], arr[choice]] = [arr[choice], arr[j]]; // 交换这两个数,完成了索引为 choice 的 arr[choice] 的随机
}
return arr;
}
根据代码分析,while (choice > 0) {}
时间复杂度 O(n)
, 交换位置时间复杂度 O(1)
, 所以它的时间复杂度 T(n) = O(n)
; this.nums.slice();
得出它的空间复杂是 O(n)
推导它超出我了我的认知范围,如果有小伙伴懂的,可以在留言区告诉我哈~
证明洗牌算法的随机性
我们要证明的是
假设总共有 n 个数,其中数 a 出现在第 i 个位置的可能性,为 1/n。
这里的 i 是任意自然数、n 是任意正整数、a 是取自 n 的一个数。
根据上面的假设,假设是从后往前考虑,因为上面洗牌算法代码也是从后往前的
可以得到,数 a 不会出现在第 n-1
个位置,也不会出现在第 n-2
个位置,... 也不会出现在第 i+1
个位置,在 第 i
个位置出现了,数 a 只有一个,后面的情况,已经与 a 无关了。
上面提到的几种情况,第 n-1
个位置到第 i
个位置,它们对应着什么,关系又是什么?
我们一步步地看
-
数 a 不会出现在第
n-1
个位置,也就是从未被挑选的数(有n
个)里,挑选不等于 a 的数(有n-1
个),放在第 n-1 个位置,可能性(n-1)/n
,假设 b 被挑选出对应的是代码第一次循环
let choice = n; // 表示在 n 个数(选择)里随机选一个 while (choice > 0) { // choice > 0 表示可以选择的个数大于 0 let j = Math.floor(Math.random() * choice); // 索引是随机的,arr[j] 刚好不等于 数 a ,可能性是 (n-1)/n , 换一种说法就是 数 a 不在这个 choose-- 这个位置上 choice--; // 由于数组是以 0 作为下标,需要往左偏移 [arr[j], arr[choice]] = [arr[choice], arr[j]]; }
-
数 a 也不会出现在第
n-2
个位置,一样的从未被挑选的数里,挑选不等于 a 的数,放在第 n-2 个位置,这里建立在第一步成立的条件下,因为 b 已经被挑选出了,未被挑选的数个数是n-1
个, 不等于 a和b 的数(有n-2
个),所以这一步的可能性是(n-1)/n * (n-2)/(n-1)
对应的是代码第二次循环
while (choice > 0) { // choice > 0 表示可以选择的个数大于 0, 这里 choice = n-1 let j = Math.floor(Math.random() * choice); // 索引是随机的,arr[j] 刚好不等于 数 a ,可能性是 (n-2)/(n-1) , 换一种说法就是 数 a 不在这个 choose-- 这个位置上 choice--; // 由于数组是以 0 作为下标,需要往左偏移 [arr[j], arr[choice]] = [arr[choice], arr[j]]; }
...(中间过程用...表示)
- 最后一步,数 a 在 第 i 个位置出现了,从第
n-1
到第i
个位置(包含n-1
和i
),总共有n-i
个位置,对应着n-i
个数。可以得到,前面被挑选过的数,是n-i-1
个(总共n-i
个减去当前即将被挑选数 a)。总共有n
个,减去前面被挑选过的数,得到未被挑选的个数是n-(n-i-1)
即i+1
。刚好就是 a,只能是 a,单独这一步可能性就是1/(i+1)
最后一步,是建立在前面的每一步的成立的基础,所以得出数 a 出现在第 i 个位置的可能性是
(n-1)/n
* (n-2)/(n-1)
* ... * 1/(i+1)
前一项的分子和后一项的分母两两消去,最后得到了 1/n
。
即有 n 个数,其中数 a 出现在第 i 个位置的可能性,为 1/n。
洗牌算法随机性得到了证明。
参考资料
谢谢您的阅读。
您的鼓励和点赞,将成为我持续学习、写文章的动力