1、面試題javascript
問:有一個長度爲 100 的數組,如何從中隨機挑選 50 個元素,組成一個新的數組?html
答:這個...那個...emmmmmm前端
問:那先不挑 50 個,就挑一個數,知道怎麼作嗎?java
答:這個我知道!隨機生成一個 0 ~ 99 的數,而後去原數組取對應位置的元素就能夠了~面試
let randomIndex = arr[Math.floor(Math.random() * arr.length)];
問:好,回到最初的問題,怎麼挑選 50 個元素?算法
答:我知道了,在 0 ~ 99 的範圍內,隨機生成 50 個不重複的數字!segmentfault
問:是這個思路,具體的實現呢?記得保證效率哦。數組
答:(吧啦吧啦吧啦)dom
問:如今假設數組的元素都是 String 類型,若是要把這個數組元素的順序打亂,有什麼辦法麼?ide
答:數組的 sort() 方法能夠傳入一個函數做爲參數,這個函數的返回值能夠決定排列順序。在這個函數中寫一個隨機數,而後就能亂序了。
問:這是一個思路,但這只是僞隨機。
答:啊咧?
問:據說過「洗牌算法」嗎?
2、隨機取數
按照上面隨機挑選一個數的思路,從原數組中隨機抽取一個數,而後使用 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) /* 這兩種寫法的結果是同樣的,但位操做的效率更高 */
3、經過 sort 亂序
首先認識一下 Array.prototype.sort()
這個方法能夠傳入一個參數 compareFunction,這個參數必須是函數
同時 sort() 會暴露出 Array 中的兩個元素 (a, b) 做爲參數傳給 compareFunction
sort() 會根據 compareFunction(a, b) 的返回值,來決定 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 函數由於循環週期的存在,沒法生成真正的隨機數
4、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; }
5、用洗牌算法隨機取數
再回到從長度爲 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; }
參考資料: