1. 程式人生 > 其它 >水塘抽樣演算法(Reservoir Sampling)

水塘抽樣演算法(Reservoir Sampling)

簡介:

水塘抽樣是一系列的隨機演算法,其目的在於從包含n個專案的集合S中選取k個樣本,其中n為一很大或未知的數量,尤其適用於不能把所有n個專案都存放到記憶體的情況。

問題:

以谷歌為例,有一道關於水塘抽樣的例題

我有一個長度為N的連結串列,N的值非常大,我不清楚N的確切值.我怎樣能寫一個儘可能高效地演算法來返回K個完全隨機的數

我們從題目可以知道三個條件:

  • 資料流長度N很大且不可知,所以不能一次性存入記憶體。
  • 時間複雜度為O(N)。
  • 隨機選取K個數,每個數被選中的概率為K/N。

如果按照常規解法:

將所有資料全部載入完,然後計算連結串列長度,然後通過random函式來求取K個隨機數,這顯然是不行的

因此常規的解決的辦法無法使用,這是就需要用到---水塘抽樣演算法

讓我們先從隨機取一個值來舉例

/* 返回連結串列中一個隨機節點的值 */
int getRandom(ListNode head) {
    Random r = new Random();
    int i = 0, res = 0;
    ListNode p = head;
    // while 迴圈遍歷連結串列
    while (p != null) {
        // 生成一個 [0, i) 之間的整數
        // 這個整數等於 0 的概率就是 1/i
        if (r.nextInt(++i) == 0
) { res = p.val; } p = p.next; } return res; }

證明:我們要證明上面的程式碼正確,只要證明這樣取任何一個值的概率是1/n即可

假設總共有 n 個元素,那麼對於第 i 個元素,它被選擇的概率就是:

第 i 個元素被選擇的概率是 1/i,第 i+1次不被替換的概率是 1-1 / (i+1),以此類推,相乘就是第 i 個元素最終被選中的概率,就是 1/n。

當我們要隨機取K個值時

演算法思路如下

  • 如果接收的資料量小於K,則依次放入水池。
  • 當接收到第i個數據時,且i >= m,在[0, i]範圍內取以隨機數d,若d的落在[0, m-1]範圍內,則用接收到的第i個數據替換水池中的第d個數據。
  • 重複步驟2
/* 返回連結串列中 k 個隨機節點的值 */
int[] getRandom(ListNode head, int k) {
    Random r = new Random();
    int[] res = new int[k];
    ListNode p = head;

    // 前 k 個元素先預設選上
    for (int j = 0; j < k && p != null; j++) {
        res[j] = p.val;
        p = p.next;
    }

    int i = k;
    // while 迴圈遍歷連結串列
    while (p != null) {
        // 生成一個 [0, i) 之間的整數
        int j = r.nextInt(++i);
        // 這個整數小於 k 的概率就是 k/i
        if (j < k) {
            res[j] = p.val;
        }
        p = p.next;
    }
    return res;
}

對於此的數學證明差別不大

我們證得,對於所有數的概率都是k/n

總結

水塘抽樣演算法的時間複雜的為O(n),空間複雜度為O(K),是一種非常巧妙的演算法,可以運用在大資料中隨機取數上