一、面試題
問:有一個長度為 100 的數組,如何從中隨機挑選 50 個元素,組成一個新的數組?
答:這個...那個...emmmmmm
問:那先不挑 50 個,就挑一個數,知道怎么做嗎?
答:這個我知道!隨機生成一個 0 ~ 99 的數,然后去原數組取對應位置的元素就可以了~
let randomIndex = arr[Math.floor(Math.random() * arr.length)];
問:好,回到最初的問題,怎么挑選 50 個元素?
答:我知道了,在 0 ~ 99 的范圍內,隨機生成 50 個不重復的數字!
問:是這個思路,具體的實現呢?記得保證效率哦。
答:(吧啦吧啦吧啦)
問:現在假設數組的元素都是 String 類型,如果要把這個數組元素的順序打亂,有什么辦法么?
答:數組的 sort() 方法可以傳入一個函數作為參數,這個函數的返回值可以決定排列順序。在這個函數中寫一個隨機數,然后就能亂序了。
問:這是一個思路,但這只是偽隨機。
答:啊咧?
問:聽說過“洗牌算法”嗎?
二、隨機取數
按照上面隨機挑選一個數的思路,從原數組中隨機抽取一個數,然后使用 splice 刪掉該元素
function getRandomArrElement(arr, count) { let res = [] while (res.length < count) { // 生成隨機 index
let randomIdx = (Math.random() * arr.length) >> 0; // splice 返回的是一個數組
res.push(arr.splice(randomIdx, 1)[0]); } return res }
上面生成隨機 index 用到了按位右移操作符 >>
當后面的操作數是 0 的時候,該語句的結果就和 Math.floor() 一樣,是向下取整
但位操作符是在數值表示的最底層執行操作,因此速度更快
// 按位右移
(Math.random() * 100) >> 0
// Math.floor
Math.floor(Math.random() * 100) /* 這兩種寫法的結果是一樣的,但位操作的效率更高 */
三、通過 sort 亂序
首先認識一下 Array.prototype.sort()
這個方法可以傳入一個參數 compareFunction,這個參數必須是函數
同時 sort() 會暴露出 Array 中的兩個元素 (a, b) 作為參數傳給 compareFunction
sort() 會根據 compareFunction(a, b) 的返回值,來決定 a 和 b 的相對位置:
- 如果 compareFunction(a, b) 小於 0 ,那么 a 會被排列到 b 之前;
- 如果 compareFunction(a, b) 大於 0 ,那么 b 會被排列到 a 之前;
- 如果 compareFunction(a, b) 等於 0 , a 和 b 的相對位置不變(不穩定!)
根據以上規則,可以在 compareFunction 中生成一個隨機數,然后根據隨機數做運算,返回一個正負未知的 Number,從而實現亂序
function randomSort(a,b) { return .5 - Math.random(); } let arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; arr.sort(randomSort);
但這並不是真正的亂序,計算機的 random 函數因為循環周期的存在,無法生成真正的隨機數
四、Fisher–Yates shuffle 洗牌算法
洗牌算法的思路是:
先從數組末尾開始,選取最后一個元素,與數組中隨機一個位置的元素交換位置
然后在已經排好的最后一個元素以外的位置中,隨機產生一個位置,讓該位置元素與倒數第二個元素進行交換
以此類推,打亂整個數組的順序
function shuffle(arr) { let len = arr.length; while (len) { let i = (Math.random() * len--) >> 0;
// 交換位置 let temp = arr[len]; arr[len] = arr[i]; arr[i] = temp; } return arr; }
再結合 ES6 的解構賦值,使用洗牌算法就更方便了:
Array.prototype.shuffle = function() { let m = this.length, i; while (m) { i = (Math.random() * m--) >>> 0; [this[m], this[i]] = [this[i], this[m]] } return this; }
五、用洗牌算法隨機取數
再回到從長度為 100 的數組中取 50 個數的問題
之前用的是 splice 修改原數組,如果結合洗牌算法,又會有別的思路
最好是自己先思考一下,然后再展開代碼進行比較

function getRandomArrElement(arr, count) { let shuffled = arr.slice(0), i = arr.length, min = i - count, temp, index; while (i > min) { index = Math.floor((i--) * Math.random()); temp = shuffled[index]; shuffled[index] = shuffled[i]; shuffled[i] = temp; } return shuffled.slice(min); }
最后放個彩蛋,關於兩種隨機取數的性能孰優孰劣
我用 Array.form 生成了一個長度為一百萬的數組,然后從中隨機取十萬個數
首先是使用 splice 的方案:
然后是洗牌算法:
喵喵喵?!!
附錄:
補充一個在范圍內生成隨機數的方法:
setRangeRandom(min: number, max: number) { //在范圍內生成隨機數 let n = max - min; if (n == 0) { return max } else if (n < 0) { [max, min] = [min, max]; n = Math.abs(n); } return ((Math.random() * ++n) >> 0) + min; }
參考資料: