在實際應用中,我們經常碰到這種情況,即要統計某個對象或者事件獨立出現的次數。對於較小的數據量,這很容易解決,我們可以首先在內存中對序列進行排序,然后掃描有序序列統計獨立元素數目。其中排序時間復雜度為O(n*log(n)),掃描時間復雜度為O(n),所以總的時間復雜度為O(n*log(n))。當內存非常充裕時,我們還可以考慮使用哈希,將時間復雜度降到O(n)。尤其是當元素只能取有限范圍的整數值時,我們還可以使用BitMap節約內存。但是在處理數據流序列時,比如,google的獨立訪問IP統計,由於序列非常長,元素取值范圍可能比較廣,單個元素占用內存可能比較多,導致內存中無法容納整個序列,甚至無法容納整個獨立元素集合。此時,不論是基於排序還是基於哈希的方法都不具備可行性。
Flajolet-Martin(FM)算法能夠較好地解決估算數據流序列中獨立元素數目的問題。
假設我們有1萬個int型數字(可重復的),我們想找出這個數字集合中不重復的數字的個數。怎么辦呢?很簡單,將這1萬個數字讀進內存,存放到hashset中,那么hashset的size就是不重復數字的個數。接下來,問題變得更加的復雜,有100億個數字,怎么辦? 全部讀取到內存中可能會有問題,如果這其中有1億個不重復的數字,那么至少需要內存 100M * sizeof(int),內存也許不夠。 FM算法就是為了解決這個問題。假設n個object,其中有m個唯一的,那么FM算法只需要log(m)的內存占用(實際操作中會是k*log(m)),以及O(n)的運算時間。當然,FM的問題是,它的結果只是一個估計值,不是精確結果。
具體思路如下:
假定哈希函數H(e)能夠把元素e映射到[0, 2^m-1]區間上;再假定函數TailZero(x)能夠計算正整數x的二進制表示中末尾連續的0的個數,譬如TailZero(2) = TailZero(0010) = 1,TailZero(8) = TailZero(1000) = 3,TailZero(10) = TailZero(1010) = 1;我們對每個元素e計算TailZero(H(e)),並求出最大的TailZero(H(e))記為Max,那么對於獨立元素數目的估計為2^Max。
這種估算的理論依據證明參見 原文。
舉例來說,給定序列{e1, e2, e3, e2},獨立元素數目N = 3。假設給定哈希函數H(e),有:
H(e1) = 2 = 0010,TailZero(H(e1)) = 1
H(e2) = 8 = 1000,TailZero(H(e2)) = 3
H(e3) = 10 = 1010,TailZero(H(e3)) = 1
第1步,將Max初始化為0;
第2步,對於序列中第1項e1,計算TailZero(H(e1)) = 1 > Max,更新Max = 1;
第3步,對於序列中第2項e2,計算TailZero(H(e2)) = 3 > Max,更新Max = 3;
第4步,對於序列中第3項e3,計算TailZero(H(e3)) = 1 ≤ Max,不更新Max;
第5步,對於序列中第4項e2,計算TailZero(H(e2)) = 3 ≤ Max,不更新Max;
第6步,估計獨立元素數目為N’ = 2^Max = 2^3 = 8。
在這個簡單例子中,實際值N = 3,估計值N’ = 8,誤差比較大。此外,估計值只能取2的乘方,精度不夠高。
在實際應用中,為了減小誤差,提高精度,我們通常采用一系列的哈希函數H1(e), H2(e), H3(e)……,計算一系列的Max值Max1, Max2, Max3……,從而估算一系列的估計值2^Max1, 2^Max2, 2^Max3……,最后進行綜合得到最終的估計值。具體做法是:首先設計A*B個互不相同的哈希函數,分成A組,每組B個哈希函數;然后利用每組中的B個哈希函數計算出B個估計值;接着求出B個估計值的算術平均數為該組的估計值;最后選取各組的估計值的中位數作為最終的估計值。
舉例來說,對於序列S,使用3*4 = 12個互不相同的哈希函數H(e),分成3組,每組4個哈希函數,使用12個H(e)估算出12個估計值:
第1組的4個估計值為<2, 2, 4, 4>,算術平均值為(2 + 2 + 4 + 4) / 4 = 3;
第2組的4個估計值為<8, 2, 2, 2>,算術平均值為(8 + 2 + 2 + 2) / 4 = 3.5;
第3組的4個估計值為<2, 8, 8, 2>,算術平均值為(2 + 8 + 8 + 2) / 4 = 5;
3個組的估計值分別為<3, 3.5, 5>,中位數為3.5;
因此3.5 ≈ 4即為最終的估計值。
分析FM算法的時間復雜度。假定序列長度為N,哈希函數H(e)的數目為K。初始化K個Max值的時間復雜度為O(K);對N個元素e使用K個哈希函數H(e)計算TailZero(H(e))並更新Max值的時間復雜度為O(N*K);綜合K個Max值給出最終估計值的時間復雜度為O(K)。因此總的時間復雜度為O(N*K)。
分析FM算法的空間復雜度。該算法需要存儲K個Max值,而每個元素e在進行相關計算后就可以丟掉。因此總的空間復雜度為O(K)。
綜上所述,FM算法的時間復雜度為O(N*K),空間復雜度為O(K)。一般來說K比較小,可以認為FM算法的時間復雜度為O(N),空間復雜度為O(1)。
FM算法可以用於估算獨立Cookie數目,獨立URL數目,獨立郵箱地址數目等等。