布隆過濾器
假如有1億個不重復的正整數(大致范圍已知),但是只有1G的內存可用,如何判斷該范圍內的某個數是否出現在這1億個數中?最常用的處理辦法是利用位圖,1*108/1024*1024*8=11.9,也只需要申請12M的內存。但是如果是1億個郵件地址,如何確定某個郵件地址是否在這1億個地址中?這個時候可能大家想到的最常用的辦法就是利用Hash表了,但是大家可以細想一下,如果利用Hash表來處理,必須開辟空間去存儲這1億個郵件地址,因為在Hash表中不可能避免的會發生碰撞,假設一個郵件地址只占8個字節,為了保證Hash表的碰撞率,所以需要控制Hash表的裝填因子在0.5左右,那么至少需要2*8*108/1024*1024*1024=1.5G的內存空間,這種情況下利用Hash表是無法處理的。這個時候要用到另外一種數據結構-布隆過濾器(Bloom Filter),它是由Burton Howard Bloom在1970年提出的,它結合了位圖和Hash表兩者的優點,位圖的優點是節省空間,但是只能處理整型值一類的問題,無法處理字符串一類的問題,而Hash表卻恰巧解決了位圖無法解決的問題,然而Hash太浪費空間。針對這個問題,布隆提出了一種基於二進制向量和一系列隨機函數的數據結構-布隆過濾器。它的空間利用率和時間效率是很多算法無法企及的,但是它也有一些缺點,就是會有一定的誤判率並且不支持刪除操作。
下面來討論一下布隆過濾器的原理和它的應用。
一.布隆過濾器的原理
布隆過濾器需要的是一個位數組(這個和位圖有點類似)和k個映射函數(和Hash表類似),在初始狀態時,對於長度為m的位數組array,它的所有位都被置為0,如下圖所示:
對於有n個元素的集合S={s1,s2......sn},通過k個映射函數{f1,f2,......fk},將集合S中的每個元素sj(1<=j<=n)映射為k個值{g1,g2......gk},然后再將位數組array中相對應的array[g1],array[g2]......array[gk]置為1:
如果要查找某個元素item是否在S中,則通過映射函數{f1,f2.....fk}得到k個值{g1,g2.....gk},然后再判斷array[g1],array[g2]......array[gk]是否都為1,若全為1,則item在S中,否則item不在S中。這個就是布隆過濾器的實現原理。
當然有讀者可能會問:即使array[g1],array[g2]......array[gk]都為1,能代表item一定在集合S中嗎?不一定,因為有這個可能:就是集合中的若干個元素通過映射之后得到的數值恰巧包括g1,g2,.....gk,那么這種情況下可能會造成誤判,但是這個概率很小,一般在萬分之一以下。
很顯然,布隆過濾器的誤判率和這k個映射函數的設計有關,到目前為止,有很多人設計出了很多高效實用的hash函數,具體可以參考:《常見的Hash算法》這篇博文,里面列舉了很多常見的Hash函數。並且可以證明布隆過濾器的誤判率和位數組的大小以及映射函數的個數有關,相關證明可參考這篇博文:《布隆過濾器 (Bloom Filter) 詳解》。假設誤判率為p,位數組大小為m,集合數據個數為n,映射函數個數為k,它們之間的關系如下:
p=2-(m/n)*ln2 可得 m=(-n*lnp)/(ln2)2=-2*n*lnp=2*n*ln(1/p)
k=(m/n)*ln2=0.7*(m/n)
可以驗證若p=0.1,(m/n)=9.6,即存儲每個元素需要9.6bit位,此時k=0.7*(m/n)=6.72,即存儲每個元素需要9.6個bit位,其中有6.72個bit位被置為1了,因此需要7個映射函數。從這里可以看出布隆過濾器的優越性了,比如上面例子中的,存儲一個郵件地址,只需要10個bit位,而用hash表存儲需要8*8=64個bit位。
一般情況下,p和n由用戶設定,然后根據p和n的值設計位數組的大小和所需的映射函數的個數,再根據實際情況來設計映射函數。
尤其要注意的是,布隆過濾器是不允許刪除元素的,因為若刪除一個元素,可能會發生漏判的情況。不過有一種布隆過濾器的變體Counter Bloom Filter,可以支持刪除元素,感興趣的讀者可以查閱相關文獻資料。
二.布隆過濾器的應用
布隆過濾器在很多場合能發揮很好的效果,比如:網頁URL的去重,垃圾郵件的判別,集合重復元素的判別,查詢加速(比如基於key-value的存儲系統)等,下面舉幾個例子:
1.有兩個URL集合A,B,每個集合中大約有1億個URL,每個URL占64字節,有1G的內存,如何找出兩個集合中重復的URL。
很顯然,直接利用Hash表會超出內存限制的范圍。這里給出兩種思路:
第一種:如果不允許一定的錯誤率的話,只有用分治的思想去解決,將A,B兩個集合中的URL分別存到若干個文件中{f1,f2...fk}和{g1,g2....gk}中,然后取f1和g1的內容讀入內存,將f1的內容存儲到hash_map當中,然后再取g1中的url,若有相同的url,則寫入到文件中,然后直到g1的內容讀取完畢,再取g2...gk。然后再取f2的內容讀入內存。。。依次類推,知道找出所有的重復url。
第二種:如果允許一定錯誤率的話,則可以用布隆過濾器的思想。
2.在進行網頁爬蟲時,其中有一個很重要的過程是重復URL的判別,如果將所有的url存入到數據庫中,當數據庫中URL的數量很多時,在判重時會造成效率低下,此時常見的一種做法就是利用布隆過濾器,還有一種方法是利用berkeley db來存儲url,Berkeley db是一種基於key-value存儲的非關系數據庫引擎,能夠大大提高url判重的效率。
布隆過濾器的簡易版本實現:
/*布隆過濾器簡易版本 2012.11.10*/ #include<iostream> #include<bitset> #include<string> #define MAX 2<<24 using namespace std; bitset<MAX> bloomSet; //簡化了由n和p生成m的過程 int seeds[7]={3, 7, 11, 13, 31, 37, 61}; //使用7個hash函數 int getHashValue(string str,int n) //計算Hash值 { int result=0; int i; for(i=0;i<str.size();i++) { result=seeds[n]*result+(int)str[i]; if(result > 2<<24) result%=2<<24; } return result; } bool isInBloomSet(string str) //判斷是否在布隆過濾器中 { int i; for(i=0;i<7;i++) { int hash=getHashValue(str,i); if(bloomSet[hash]==0) return false; } return true; } void addToBloomSet(string str) //添加元素到布隆過濾器 { int i; for(i=0;i<7;i++) { int hash=getHashValue(str,i); bloomSet.set(hash,1); } } void initBloomSet() //初始化布隆過濾器 { addToBloomSet("http://www.baidu.com"); addToBloomSet("http://www.cnblogs.com"); addToBloomSet("http://www.google.com"); } int main(int argc, char *argv[]) { int n; initBloomSet(); while(scanf("%d",&n)==1) { string str; while(n--) { cin>>str; if(isInBloomSet(str)) cout<<"yes"<<endl; else cout<<"no"<<endl; } } return 0; }