因為要做過濾器相關內容,最近讀了一些過濾器方面的文章,准備從中提取主要思想寫幾篇博客。
作為這系列的第一篇文章,首先得講一下過濾器是干什么用的。從歷史發展來看,過濾器最早出現是作為散列表的替代品,那么功能就要和散列表差不多,主要是查詢當前的元素是否在我已知的集合里。但是隨着數據量不斷增大,散列表相對來說占用空間過大,而空間占用小的查找樹的\(O(logn)\)時間復雜度又太高。於是有人想出來能否用正確率做代價,換取較高的查詢速度和較小的存儲占用,這就是過濾器。當然,這里所允許的錯誤僅限假陽性,例如我們做一個關於代理ip地址的過濾器,當有一個不是代理的ip地址發來,我們也許會把它錯認成是代理ip,但是我們不會允許一個代理ip被錯認成非代理ip,簡單的說,就是寧可錯殺,不可放過。
作為第一篇,按照歷史角度,先說布隆過濾器(bloom filter)。原版的布隆過濾器很朴素,只支持插入和查詢兩個操作,下面我們看它的原理。
首先,布隆過濾器申請了一片空間,存了一個數組,每個元素都只有1個bit,共有N個元素,初始化每個值都為0。如下圖所示。(實際並沒有index這一行,僅僅是為了方便觀看)
插入操作
下一步就是如何插入數據。布隆過濾器要求你事先定義K個Hash函數,這K個Hash函數都是從定義域映射到上圖中的index空間(即N)。通過這K個Hash函數,我們對一條新的數據x,計算出\(h_0(x),h_1(x),....h_{k-1}(x)\),這樣就得到了K個地址。我們將這K個地址的比特位置1.這里就有值得注意的地方,因為我們的過濾器的大小遠遠小於數據集大小,那么常常會有Hash之后映射到同一個位置的數據,不要擔心,照常置1。
下面的例子是K=3,\(h_0(x)=2,h_1(x)=5,h_2(x)=7\)。如圖所示
查找操作
當插入其他一些數據后,過濾器可能變成下圖所示,我們不關心中間經歷了什么。
我們現在查找剛才第一次插入的數據是否在過濾器中,那么同樣計算\(h_0(x),h_1(x),h_2(x)\),算出3個地址,2,5,7,去表中查找,若3個地址的數據都為1,則判斷在過濾器中,否則判斷不在過濾器中。
算法和數據結構都很簡單,我們下面說的是對布隆過濾器的一些分析和題外話,有興趣的讀者可以繼續閱讀。
我們在過濾器上很關注三個指標,一個是操作的時間復雜度,一個是平均每條數據占用的比特數,最后是錯誤率。下面我們分析一下。
時間復雜度
布隆過濾器上的兩個操作,插入和查詢,都只是計算一下K個Hash函數的值,然后進行K次訪存操作。那么時間上很明顯是\(O(K)\),其實不算也知道,一個替代Hash表的過濾器,操作代價必須是常數級別。
平均每條數據占用的比特數 and 錯誤率
直覺上,很容易得出這兩個衡量指標其實是矛盾的,當想要較低錯誤率時就要增大空間;想要減小占用空間時,那么由於Hash碰撞的次數變多,錯誤率也會提高。我們在這里將錯誤率作為已知來計算平均每條數據占用的比特數。為什么這么做?因為在實際應用中我們可以對過濾器設定一個錯誤率作為標准,通常情況下我們對這一點要求更嚴格。
我們設數組總大小為\(N\),插入n條數據后表中還為0的數據占全部的比例為\(\phi\)。那么
\(\phi = (1-K / N)^n\)-------------------------(1)
讀者可以想想為什么不是\(K * n / N\),在這里,我們其實省略了Hash函數默認是隨機分布到全空間的。
設錯誤率為\(P\),
\(P = (1-\phi)^K\) ----------------------------(2)
錯誤只發生隨機分布到K個地址,結果在K個地址都有數據用了,那么不管你是否在過濾器中,布隆過濾器都會判斷你在其中,這就是錯誤來源。
然后我們對(1)式兩邊取對數
\(log_2^\phi = log_2^{(1-K/N)^n}\)
使用換底公式
\(log_2^\phi = log_2^{(1-K/N)^n} = log_e^{(1-d/N)^n} * log_2^e = -n * K / N *log_2^e\) ---(3)
我們要求的平均每條數據占用的比特數\(N(bit) / n = log_2^{1/P} * log_2^e / (log_2^\phi * log_2^{(1-\phi)})\),通過極值點計算可以得到分母最大時,\(\phi=0.5\),分母為1,則結果為\(N/n = log_2^{1/P} / ln2\)
可以看到,每條數據占用的比特數與錯誤率的對數成反比。
之后我會先把幾個不同思想的過濾器介紹一遍,最后會有關於布隆過濾器的一些變形