做用:水塘抽樣算法是一種抽樣算法,對於一個很大的集合,抽取的樣本值可以保證隨機.java
特色:其複雜度並不很高O(n)
,而且可以很大程度地節省內存.面試
不少大公司的面試題都考察過這個算法,以谷歌爲例,有一道關於水塘抽樣的例題算法
我有一個長度爲N的鏈表,N的值很是大,我不清楚N的確切值.我怎樣能寫一個儘量高效地算法來返回K個徹底隨機的數.數組
這道題有兩個限制:數據結構
1.高效,即節省內存的使用dom
2:儘可能隨機地返回值函數
假如咱們去掉限制1,能夠很簡單地作出來,將全部數據加載進內存,計算鏈表長度,而後經過random函數來求取幾個隨機數.大數據
這樣的效率並不高,把全部數據加載到內存,若是數據很是大可能會致使沒法計算.spa
注意題目中有一個小tip,就是鏈表.鏈表這種數據結構是經過數據節點首尾相連造成的鏈式存儲結構.code
既然是鏈表,那麼能夠一個一個節點處理,不須要將全部數據加載到內存.一個節點一個節點去處理,這還不夠形象,將題目換個形式來表述:
咱們有1T的文本文件存在硬盤中,想隨機抽取幾行,保證儘量少得使用內存而且可以徹底隨機.
以前想到的加載到內存就不太適合了,可是還能夠想到別的辦法,好比每次讀取一行記錄加載到內存,記數+1,清空內存中行數據,直到最後統計一共多少行,而後根據總行數來計算K個隨機數.如何再取回行對應的數據呢?咱們能夠再遍歷一遍,一邊遍歷一邊記錄這一行的號碼是否是在k個隨機數中,若是是,則將該行內容保留.
這樣的話遍歷兩次應該能夠作到,可是1T數據遍歷兩次的時間消耗是很是高的.
因此還有更好的方案嗎,那就是水塘抽樣算法.
咱們先從具體案例中理解水塘抽樣算法的實現,再從抽象的角度來理解.
假如10000個數,咱們要抽取十個隨機數.
一萬個數的樣本集合數組記做S
.
十個隨機數的數組記做R
,表明result
.
先取數組S
中前十個數填充進數組R
.
算法的第一次迭代流程是這樣的:
j
,若是j<10
(假如J=4
),咱們就將數組R
中的第5項(R[4]
)替換成S
數組中的第11項(S[10]
).遍歷完成生成的R數組,就是咱們要求的隨機數組.
$ S[N] $記做:樣本集合
$ R[K] $記做:結果集合
$ N $記做:S數組大小
\(J\)記做:每次的隨機數
\(K\)記做:前K個隨機數
\(i\)記做:迭代次數.
步驟
取\(S\)集合中前\(K\)個數填入\(R\)集合
從\(S[K]\)開始遍歷
生成隨機數\(J\),範圍是\(0->K+i-1\).由於數組下標從0開始,因此-1.
若是\(J<K\),則替換\(R\)中的值->\(R[j] = S[i]\).
遍歷結束,生成結果數組\(R\).
int[] S = new int[10000]; int N = S.length; Random random = new Random(); //生成一萬個數的數組 for (int r = 0;r < N; r ++){ S[r] = random.nextInt(10000); } int k = 10; int[] R = new int[k]; //S前K個數填充R數組 for (int f = 0;f < k; f++){ R[f] = S[f]; } int j ; //遍歷數組S,根據算法,替換R數組中的元素,最終生成結果R數組. for (int i = k;i < S.length;i++){ j = random.nextInt(i); if (j < k) R[j] = S[i]; } //打印R數組的結果 for (int i =0;i < R.length;i++) { System.out.println(R[i]); }
總結一下這種算法.經過一遍遍歷就得到了K個隨機數,在很大數據的狀況下效率是很是高的,很是適合咱們的應用場景.
可是爲何這樣生成的數是徹底隨機的呢?
就剛纔的具體例子來說,第一次遍歷時,i=10,隨機數的範圍是0到10共11個數,那麼不替換的機率是\(10/11\),等到第二次迭代時,不替換的機率變成\(10/12\),第三次\(10/13\),第四次\(10/14\).......
這樣看來好像每一次的機率並不相等,其實並非這樣,咱們要看的是最終進入數組\(R\)中的機率,雖然第十一個數進入\(R\)的機率比較大,可是到最後他被替換的機率也很大,因此每一個數最終保留在\(R\)中的機率究竟是多少呢?
能夠參考一下維基百科中的證實,我以爲很是清晰.
在循環內第n行被抽取的機率爲k/n,以\(P_n\)表示。若是檔案共有N行,任意第n行(注意這裏n是序號,而不是總數).
被抽取的機率爲:
咱們能夠求得每行被抽取的機率是相同的,等於\(k/N\).
很是巧妙,因此當咱們面對這種情景時,能夠考慮使用水塘抽樣進行隨機抽取.