集合資料結構一般都有這麼一個方法:contains。其作用就是判斷給定的元素是否存在集合中,這是一個常用的方法。其最簡單的內部實現即遍曆集合內的元素,一個個的判斷是否與給定元素相等。為了更高效點我們甚至可以採用“更好的(好是相對的)”演算法實現。比如如果該集合是已經排序的,那麼我們用二分尋找來實現contains肯定更好。但是,如果集合的資料量龐大到一定程度,大部分我們熟知的演算法不再有什麼用了。即使可以使用,但是機器記憶體也不允許。
而Bloom Filter就是這麼一個空間利用率非常高的演算法。我們先來看看這個演算法的原理:
1 首先我們有一個長度為n的位元數組,開始的時候將這個位元數組裡所有的元素都初始化為0
00000000000000000000
上面的位元數組n為20
2 然後選取k個雜湊函數,這k個雜湊函數產生的結果的值的範圍在0到n-1之間(對於上面的位元數組,即0到19) 。對每個要添加進集合的對象進行雜湊運算,然後將雜湊計算結果作為數組的索引,將索引位置的位元位設定為1(不管該位元位原先為0還是為1)。
比如我們選取三個雜湊函數,對於對象A雜湊值為0,5,7。那麼位元數組就為:
10000101000000000000
對象B的值為2,8,13,那麼添加B後的位元數組為:
10100101100001000000
對象C為0,4,7(對象C的第一個雜湊函數的值與對象A的相同了,沒關係我們還是設定為1就可以了):
10101101100001000000
現在我們的Bloom Filter裡已經有3個元素了。現在我們要判斷某元素X是否在該集合中。就相當於我們要實現一個contains方法。那麼這個方法如何?呢?
對元素X採用相同的三個雜湊函數雜湊,然後以這三個雜湊值為索引去位元數組裡找。如果三個索引位置的位元位都為1我們就認為該元素在集合中,否則不是。
我們可以用虛擬碼簡單的描述一下這個演算法:
public class BloomFilter{
private bit[] bitSet = new bit[N];
public void add(Object element){
int[] hashValues = getHashValues(element);
for(int i : hashValues){
bitSet[i] = 1;
}
}
public boolean contains(Object element){
int[] hashValues = getHashValues(element);
for(int i : hashValues){
if(bitSet[i] != 1) return false;
}
return true;
}
}
演算法還是挺直觀的,對不。想想,一個很大的對象,經過一雜湊,然後就變成了Bloom Filter裡面的一個位元,這個空間利用效率是多麼高啊。如果雜湊函數的實現效率也很高的話那麼不僅空間利用率高,時間複雜度也低啊。這真是一個神奇的演算法對吧。
可能你想,以後我就把我們那個啥數組的contains方法替換成Bloom Filter的實現吧。
不過你仔細驗證過這個演算法沒,它存在一些問題。這個演算法有以下這麼幾個特徵:
1 如果該元素真的在集合中,那麼Bloom Filter的contains方法肯定會返回true,這就是Bloom Filter不會漏報的特性。
2 如果該元素不在集合中,但Bloom Filter的contains方法有可能返回true。因為不同的元素經過雜湊之後雜湊值可能發生碰撞。這是Bloom Filter有可能誤判的特性。但是這個誤判的幾率並不高。
根據這兩個特性Bloom Filter在大量資料時還是挺有用的。比如假設我們有一個快取服務器叢集,叢集裡的不同的伺服器承擔的緩衝也不盡相同。如果一個使用者請求過來了,我們如何能快速的判斷出使用者請求的這個url在叢集裡哪台伺服器上呢?因為每台伺服器上緩衝的url對應的頁面非常龐大,我們全部弄到記憶體裡代價也很高。我們就可以在每台伺服器上放一個Bloom Filter,裡面添加的都是本伺服器上有緩衝的那些url。這樣即使Bloom Filter誤判了,那就是把一個url發到了一個並不持有該url對應的緩衝的伺服器上,結果就是緩衝未命中,快取服務器只需要將該url打到後端的上遊伺服器就好了。
根據Bloom Filter的特徵我們可以看到不是所有的情境都可以用的,只有在一些能容許少量的誤判的情況下使用才行。該演算法用很低的誤判率卻換來了大量的儲存空間,實在是是一個很巧妙的演算法。
Bloom Filter演算法:http://en.wikipedia.org/wiki/Bloom_filter