文章出處

有前端題目大概是這樣的:考慮到性能問題,如何快速從一個巨大的數組中隨機獲取部分元素。

比如有個數組有100K個元素,從中不重復隨機選取10K個元素。

為了演示方便我們將數據簡化,先給出方案最后再用大點的數據來測試性能的對比。

常規解法

常規做法倒也不難,生成一個0到數組長度減1的隨機數,這個數也就是被選中元素在原數組中的下標,獲得該元素后將值保存到另一個數組同時通過數組的splice方法將該元素從原數組中刪除,以保證下次不會重復取到。

按以上思路,代碼大概就是這樣的:

//元素總數,為了方便演示這里取個小一點的數目比如5,表示總共5個元素
var TOTAL_NUM = 5,
    //要取得的個數,表示我們要從原數組中隨機取3個元素
    COUNT = 3,
    //用隨機字符串初始化原數組
    arr = new Array(TOTAL_NUM + 1).join('0').split('').map(function() {
        return Math.random().toString(36).substr(2);
    }),
    //保存結果的數組
    result = [];

console.log('原數組:', arr);

//開始我們的選取過程
for (var i = COUNT - 1; i >= 0; i--) {
    //從原數組中隨機取一個元素出來
    var index = Math.floor(Math.random() * arr.length);
    //壓入結果數組
    result.push(arr[index]);
    //將該元素從原數組中刪除
    arr.splice(index, 1);
};
console.log('結果數組:', result);

 

運行結果如下圖:

當然上面例子中為了便于演示,將題目要求的100 000 大數目簡化為總數為5,同時只取3個。

由測試結果看這種做法是完全可行的。

但存在一個問題:為了下次隨機時不重復選取已經選擇過的元素,我們將選擇過的元素從原數組中通過splice方法進行刪除,但這個splice方法操作的過程本身就是數組重新維護其元素索引的過程,這意味著被選擇的元素之后的所有元素需要前移一個位置來重新生成一個緊湊的數組,可以想象如果我們取走了原數組中的第1個元素,那么之后的99 999個元素都需要發生變動來完成重組數組的操作,無疑有點耗時。

利用洗牌算法

另一個思路可以是這樣的,既然要隨機選取,那我可以先把數組的元素打亂先,然后要多少就從開始取多少就行了。一提到隨機,自然想到洗牌算法,而關于洗牌算法已經有一個非常經典且高效的Fisher-Yates算法了,這個算法我之前有寫過一篇博客介紹過。

這個想法較之前的方法有點逆行的感覺,前面著重點是隨機,所以每次都產生一個隨機下標到原數組去取,現在是先將數組元素隨機打亂,再去正常取。由于洗牌算法非常高效且省去了數組的重組,較之前性能應該有所提升。

照這個思路最后實現的代碼大概就是這個樣子的:

//元素總數,為了方便演示這里取個小一點的數目比如5,表示總共5個元素
var TOTAL_NUM = 5,
    //要取得的個數,表示我們要從原數組中隨機取3個元素
    COUNT = 3,
    //用隨機字符串初始化原數組
    arr = new Array(TOTAL_NUM + 1).join('0').split('').map(function() {
        return Math.random().toString(36).substr(2);
    }),
    //保存結果的數組
    result = [];

console.log('原數組:', arr);
//隨機化原數組
arr = shuffle(arr);
//選取元素
result = arr.slice(0, COUNT);
console.log('結果數組:', result);

function shuffle(array) {
    var m = array.length,
        t, i;
    // 如果還剩有元素…
    while (m) {
        // 隨機選取一個元素…
        i = Math.floor(Math.random() * m--);
        // 與當前元素進行交換
        t = array[m];
        array[m] = array[i];
        array[i] = t;
    }
    return array;
}

 

上面代碼中包含了經典的洗牌算法Fisher-Yates Shuffle算法,即shuffle函數。具體可參見我的另一篇博客

運行結果:

從結果來看,此種方法也是可行的。

細想還是存在問題,對于一個比較大的數組來說,不管你的洗牌算法多么高效(即使上面Fisher-Yates算法時間復雜度為O(n)),要隨機整個數組也還是很龐大的工程的吧。

所以對于這個題目的探索還沒有完。當我在stackoverflow上面發問后,雖然沒得到什么驚人的回答,但有個回答卻提醒我可以將上面的方法再次改進。

只取所需

那就是我們沒有必要隨機掉整個數組,在我們取完需要數量的元素后,可以將Fisher-Yates亂序方法中止掉!

思路是非常明顯的了, 這樣可以省下不少無意義的操作。

所以最后的實現大概成了這樣子:

//元素總數,為了方便演示這里取個小一點的數目比如5,表示總共5個元素
var TOTAL_NUM = 5,
    //要取得的個數,表示我們要從原數組中隨機取3個元素
    COUNT = 3,
    //用隨機字符串初始化原數組
    arr = new Array(TOTAL_NUM + 1).join('0').split('').map(function() {
        return Math.random().toString(36).substr(2);
    }),
    //保存結果的數組
    result = [];

console.log('原數組:', arr);

//此段代碼由Fisher-Yates shuflle算法更改而來
var m = arr.length,
    t, i;
while (m && result.length < COUNT) {
    // 隨機選取一個元素…
    i = Math.floor(Math.random() * m--);
    t = arr[m];
    arr[m] = arr[i];
    arr[i] = t;
    result.push(arr[m]);
}

console.log('結果數組:', result);

 

上面代碼將Fisher-Yates算法略做修改,在取得滿足要求的元素之后便停止了,所以較前面的做法更加科學。

運行結果:

 

性能比較

最后給出上面三個方法耗時的比較,這里將需要操作的數組元素個數回歸到題目中要求的100 000來。

下圖是jsperf上運行測試的結果,詳情可點測試頁面重新運行。數值越大越好。由上到下依次是本文中介紹的三種方法。

總結

目前PO主只能想到這些,更優的做法還有待進一步探究。

REFERNCE

由亂序播放說開了去-數組的打亂算法Fisher–Yates Shuffle http://www.cnblogs.com/Wayou/p/fisher_yates_shuffle.html


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()