隨機打亂數組及Fisher–Yates shuffle算法詳解

介紹幾種隨機打亂數組的方法,及其利弊。html

1、Array.prototype.sort 排序

注意一下,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 打亂數組方法真的有用?

2、Fisher–Yates shuffle 經典洗牌算法

這種算法思想,目前有兩種稍有不一樣的實現方式,這裏我把它們都算入 Fisher–Yates shuffle。分別是 Fisher–Yates shuffleKnuth-Durstenfeld Shufflegithub

著名的 Lodash 庫的方法 _.shuffle() 也是使用了該算法。算法

1. Fisher–Yates shuffle(Fisher and Yates' original method)

由 Ronald Fisher 和 Frank Yates 提出的 Fisher–Yates shuffle 算法思想,通俗來講是這樣的:數組

假設有一個長度爲 N 的數組dom

  1. 從第 1 個到剩餘的未刪除項(包含)之間選擇一個隨機數 k。
  2. 從剩餘的元素中將第 k 個元素刪除並取出,放到新數組中。
  3. 重複第 一、2 步直到全部元素都被刪除。
  4. 最終將新數組返回

實現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 4 5 6 7 8 3 

如今咱們從 1 ~ 7 選擇第二個隨機數 k 爲 4,而後在 Scratch 上刪除第 k 個數字(即數字 5),並將其放到 Result 中:

Range Roll Scratch Result
1 - 7  4 1 2 3 4 5 6 7 8 3 5 

如今咱們從 1 ~ 6 選擇下一個隨機數,而後從 1 ~ 5 選擇依此類推,老是重複上述過程:

Range Roll Scratch Result
1–6 5 1 2 3 4 5 6 7 8 3 5 7
1–5 3 1 2 3 4 5 6 7 8 3 5 7 4
1–4 4 1 2 3 4 5 6 7 8 3 5 7 4 8
1–3 1 1 2 3 4 5 6 7 8 3 5 7 4 8 1
1–2 2 1 2 3 4 5 6 7 8 3 5 7 4 8 1 6
    1 2 3 4 5 6 7 8 3 5 7 4 8 1 6 2
2. Knuth-Durstenfeld Shuffle(The modern algorithm)

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

繼續,下一個隨機數是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

3、總結

若要實現隨機打亂數組的需求,不要再使用 arr.sort(() => Math.random() - 0.5) 這種方法了。目前用得較多的是 Knuth-Durstenfeld Shuffle 算法。

4、參考

相關文章
相關標籤/搜索