水塘抽樣演算法(Reservoir Sampling)
阿新 • • 發佈:2022-01-16
簡介:
水塘抽樣是一系列的隨機演算法,其目的在於從包含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),是一種非常巧妙的演算法,可以運用在大資料中隨機取數上