介紹幾種隨機打亂數組的方法,及其利弊。html
注意一下,sort()
方法會改變原數組,看代碼:git
// ES6 寫法 function randomShuffle(arr) { return arr.sort(() => Math.random() - 0.5) } // ES5 寫法 function randomShuffle(arr) { var compareFn = function () { return Math.random() - 0.5 } return arr.sort(compareFn) }
但實際上 這種方法並不能真正的隨機打亂數組。在屢次執行後,每一個元素有很大概率還在它原來的位置附近出現。可看下這篇文章: 經常使用的 sort 打亂數組方法真的有用?
這種算法思想,目前有兩種稍有不一樣的實現方式,這裏我把它們都算入 Fisher–Yates shuffle。分別是 Fisher–Yates shuffle
和 Knuth-Durstenfeld Shuffle
。github
著名的 Lodash 庫的方法 _.shuffle()
也是使用了該算法。算法
由 Ronald Fisher 和 Frank Yates 提出的 Fisher–Yates shuffle 算法思想,通俗來講是這樣的:數組
假設有一個長度爲 N 的數組dom
- 從第 1 個到剩餘的未刪除項(包含)之間選擇一個隨機數 k。
- 從剩餘的元素中將第 k 個元素刪除並取出,放到新數組中。
- 重複第 一、2 步直到全部元素都被刪除。
- 最終將新數組返回
實現prototype
function shuffle(arr) { var random var newArr = [] while (arr.length) { random = Math.floor(Math.random() * arr.length) newArr.push(arr[random]) arr.splice(random, 1) } return newArr }
舉例code
假設咱們有 1 ~ 8 的數字htm
表格每列分別表示:範圍、隨機數(被移除數的位置)、剩餘未刪除的數、已隨機排列的數。
Range | Roll | Scratch | Result |
---|---|---|---|
1 2 3 4 5 6 7 8 |
如今,咱們從 1 ~ 8 中隨機選擇一個數,獲得隨機數 k 爲 3,而後在 Scratch 上刪除第 k 個數字(即數字 3),並將其放到 Result 中:blog
Range | Roll | Scratch | Result |
---|---|---|---|
1 - 8 | 3 | 1 2 |
3 |
如今咱們從 1 ~ 7 選擇第二個隨機數 k 爲 4,而後在 Scratch 上刪除第 k 個數字(即數字 5),並將其放到 Result 中:
Range | Roll | Scratch | Result |
---|---|---|---|
1 - 7 | 4 | 1 2 |
3 5 |
如今咱們從 1 ~ 6 選擇下一個隨機數,而後從 1 ~ 5 選擇依此類推,老是重複上述過程:
Range | Roll | Scratch | Result |
---|---|---|---|
1–6 | 5 | 1 2 |
3 5 7 |
1–5 | 3 | 1 2 |
3 5 7 4 |
1–4 | 4 | 1 2 |
3 5 7 4 8 |
1–3 | 1 | 3 5 7 4 8 1 | |
1–2 | 2 | 3 5 7 4 8 1 6 | |
3 5 7 4 8 1 6 2 |
Richard Durstenfeld 於 1964 年推出了現代版本的 Fisher–Yates shuffle,並由 Donald E. Knuth 在 The Art of Computer Programming 以 「Algorithm P (Shuffling)」 進行了推廣。Durstenfeld 所描述的算法與 Fisher 和 Yates 所給出的算法有很小的差別,但意義重大。
-- To shuffle an array a of n elements (indices 0..n-1): for i from n−1 downto 1 do // 數組從 n-1 到 0 循環執行 n 次 j ← random integer such that 0 ≤ j ≤ i // 生成一個 0 到 n-1 之間的隨機索引 exchange a[j] and a[i] // 將交換以後剩餘的序列中最後一個元素與隨機選取的元素交換
Durstenfeld 的解決方案是將「刪除」的數字移至數組末尾,即將每一個被刪除數字與最後一個未刪除的數字進行交換。
實現
// ES6 寫法 function shuffle(arr) { let i = arr.length while (--i) { let j = Math.floor(Math.random() * i) ;[arr[j], arr[i]] = [arr[i], arr[j]] } return arr } // ES5 寫法 function shuffle(arr) { var i = arr.length var j var t while (--i) { j = Math.floor(Math.random() * i) t = arr[i] arr[i] = arr[j] arr[j] = t } return arr }
Knuth-Durstenfeld Shuffle 將算法的時間複雜度下降到 O(n)
,而 Fisher–Yates shuffle 的時間複雜度爲 O(n2)
。後者在計算機實現過程當中,將花費沒必要要的時間來計算每次剩餘的數字(能夠理解成數組長度)。
舉例
一樣,假設咱們有 1 ~ 8 的數字
表格每列分別表示:範圍、當前隨機數(即隨機交互的位置)、剩餘未交換的數、已隨機排列的數。
Range | Roll | Scratch | Result |
---|---|---|---|
1 2 3 4 5 6 7 8 |
咱們從 1 ~ 8 中隨機選擇一個數,獲得隨機數 k 爲 6,而後交換 Scratch 中的第 6 和第 8 個數字:
Range | Roll | Scratch | Result |
---|---|---|---|
1 - 8 | 6 | 1 2 3 4 5 8 7 | 6 |
接着,從 1 ~ 7 中隨機選擇一個數,獲得隨機數 k 爲 2,而後交換 Scratch 中的第 2 和第 7 個數字:
Range | Roll | Scratch | Result |
---|---|---|---|
1 - 7 | 6 | 1 7 3 4 5 8 | 2 6 |
繼續,下一個隨機數是1 ~ 6,獲得的隨機數剛好是 6,這意味着咱們將列表中的第 6 個數字保留下來(通過上面的交換,如今是 8),而後移到下一個步。一樣,咱們以相同的方式進行操做,直到完成排列:
Range | Roll | Scratch | Result |
---|---|---|---|
1 - 6 | 6 | 1 7 3 4 5 | 8 2 6 |
1 - 5 | 1 | 5 7 3 4 | 1 8 2 6 |
1 - 4 | 3 | 5 7 4 | 3 1 8 2 6 |
1 - 3 | 3 | 5 7 | 4 3 1 8 2 6 |
1 - 2 | 1 | 7 | 5 4 3 1 8 2 6 |
所以,結果是 7 5 4 3 1 8 2 6
。
若要實現隨機打亂數組的需求,不要再使用 arr.sort(() => Math.random() - 0.5)
這種方法了。目前用得較多的是 Knuth-Durstenfeld Shuffle 算法。