Fisher–Yates shuffle 洗牌算法
Fisher-Yates shuffle 是一种生成有限序列的随机排列的算法——简单地说,该算法可以对序列进行混排.本人能力有限,且懒.不会扒论文去研究该算法在数学上的证明,只能抄袭网上的博客总结一遍的算法的步骤,并分析一下Lodash对该方法的简单实现.
1.原始算法步骤
Fisher–Yates shuffle 算法之所以有这个👻命名,当然是由Fisher和Yates这两个人的发明的,一开始只是用来人工混排一组数字序列,原始算法的步骤非常容易理解.
比如为了产生数字1-N之间的一组混排,可按如下步骤:
- 写下从 1 到 N 的数字
- 取一个从 1 到剩下的数字(包括这个数字)的随机数 k
- 从低位开始,得到第 k 个数字(这个数字还没有被取出),把它写在独立的一个列表的最后一位
- 重复第 2 步,直到所有的数字都被取出
- 第 3 步写出的这个序列,现在就是原始数字的随机排列
前面说过,原始算法是用来人工混排的,如果计算机严格按照此步骤执行,第三步中从低位开始计数,加上第四部的"重复"动作决定了最坏情况下,原始方法的时间复杂度是O(n2).
2.经典的算法
对于稍微懂点编程思想的人来说,原始的算法很容易改进,因为我们不会傻到第三步真的去从低位开始数数,对于数组来说是可以直接取到的,下面我们稍微改进一下.
- 1.给定一组待混排的有限序列P
- 2.新初始化一个空的序列Q
- 3.从P中随机选取一个元素
- 4.将该元素放到序列Q的最后面,并从序列P中移除该元素
- 重复3-4的步骤,直到序列P中元素全部选取到了序列Q中,得到的序列Q即为一组P的混排序列
算法的步骤,也符合一般人的认知,既然是洗牌嘛,每次从一个序列中随机选一个元素放到另一个序列中,得到的序列就是随机混排的嘛..这么容易想到的算法我们就称为经典算法把

简单的实现一下经典的算法
function shuffle(arr) {
if(!Array.isArray(arr) && arr.length) {
return []
}
const res = []
for(let i = arr.length; i > 0; i --) {
const idx = Math.floor(Math.random() * i)
res.push(arr[idx])
arr.splice(idx, 1)
}
return res
}
3.流行的算法
经典的算法貌似满足我们大多数的需求了,但是现代人精益求精,又提出了现代算法,与经典算法不同的是,现代算法在操作过程中不需要借助一个新的序列.而可以直接在当前序列中完成.算法步骤大致相同:
- 1.给定一组待混排的有限序列P
- 2.从P中随机选取一个未混排的元素
- 3.将该元素与序列P的最后一个未混排的元素交换
- 重复2-3的步骤,直到序列P中元素全部混排过

简单的实现一下现代算法吧
function shuffle (arr) {
if(!Array.isArray(arr) && arr.length) {
return []
}
for (let i = arr.length; i > 0; i--){
const idx= Math.floor(Math.random() * i)
if(idx !== (i-1)) {
const tmp = arr[idx];
arr[idx] = arr[i-1]
arr[i-1] = tmp
}
}
return arr
}
比较经典算法和现代算法,可以发现,前者是返回新的数组,后者会改变原数组.Lodash库中Shuffle就是现代算法的实现,不同的是其交换的元素是从数组首位开始的,并且返回一个新数组
import copyArray from './.internal/copyArray.js'
function shuffle(array) {
const length = array == null ? 0 : array.length
if (!length) {
return []
}
let index = -1
const lastIndex = length - 1
const result = copyArray(array)
while (++index < length) {
const rand = index + Math.floor(Math.random() * (lastIndex - index + 1))
const value = result[rand]
result[rand] = result[index]
result[index] = value
}
return result
}
export default shuffle