假若有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太浪費空間。針對這個問題,布隆提出了一種基於二進制向量和一系列隨機函數的數據結構-布隆過濾器。它的空間利用率和時間效率是不少算法沒法企及的,可是它也有一些缺點,就是會有必定的誤判率而且不支持刪除操做。
html
下面來討論一下布隆過濾器的原理和它的應用。 ios
一.布隆過濾器的原理 算法
布隆過濾器須要的是一個位數組(這個和位圖有點相似)和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) google
k=(m/n)*ln2=0.7*(m/n) url
能夠驗證若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; }