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