BloomFilter(布隆過濾器)

布隆過濾器 (Bloom Filter)是由Burton Howard Bloom於1970年提出,它是一種space efficient的概率型數據結構,用於判斷一個元素是否在集合中。在垃圾郵件過濾的黑白名單方法、爬蟲(Crawler)的網址判重模塊中等等經常被用到。哈希表也能用於判斷元素是否在集合中,但是布隆過濾器只需要哈希表的1/8或1/4的空間複雜度就能完成同樣的問題。布隆過濾器可以插入元素,但不可以刪除已有元素。其中的元素越多,false positive rate(誤報率)越大,但是false negative (漏報)是不可能的。

 

本文將詳解布隆過濾器的相關算法和參數設計,在此之前希望大家可以先通過谷歌黑板報的數學之美系列二十一 - 布隆過濾器(Bloom Filter)來得到些基礎知識。

 

一. 算法描述

一個empty bloom filter是一個有m bits的bit array,每一個bit位都初始化爲0。並且定義有k個不同的hash function,每個都以uniform random distribution將元素hash到m個不同位置中的一個。在下面的介紹中n爲元素數,m爲布隆過濾器或哈希表的slot數,k爲布隆過濾器重hash function數。

 

爲了add一個元素,用k個hash function將它hash得到bloom filter中k個bit位,將這k個bit位置1。

 

爲了query一個元素,即判斷它是否在集合中,用k個hash function將它hash得到k個bit位。若這k bits全爲1,則此元素在集合中;若其中任一位不爲1,則此元素比不在集合中(因爲如果在,則在add時已經把對應的k個bits位置爲1)。

 

不允許remove元素,因爲那樣的話會把相應的k個bits位置爲0,而其中很有可能有其他元素對應的位。因此remove會引入false negative,這是絕對不被允許的。

 

當k很大時,設計k個獨立的hash function是不現實並且困難的。對於一個輸出範圍很大的hash function(例如MD5產生的128 bits數),如果不同bit位的相關性很小,則可把此輸出分割爲k份。或者可將k個不同的初始值(例如0,1,2, … ,k-1)結合元素,feed給一個hash function從而產生k個不同的數。

 

當add的元素過多時,即n/m過大時(n是元素數,m是bloom filter的bits數),會導致false positive過高,此時就需要重新組建filter,但這種情況相對少見。

 

二. 時間和空間上的優勢

當可以承受一些誤報時,布隆過濾器比其它表示集合的數據結構有着很大的空間優勢。例如self-balance BST, tries, hash table或者array, chain,它們中大多數至少都要存儲元素本身,對於小整數需要少量的bits,對於字符串則需要任意多的bits(tries是個例外,因爲對於有相同prefixes的元素可以共享存儲空間);而chain結構還需要爲存儲指針付出額外的代價。對於一個有1%誤報率和一個最優k值的布隆過濾器來說,無論元素的類型及大小,每個元素只需要9.6 bits來存儲。這個優點一部分繼承自array的緊湊性,一部分來源於它的概率性。如果你認爲1%的誤報率太高,那麼對每個元素每增加4.8 bits,我們就可將誤報率降低爲原來的1/10。add和query的時間複雜度都爲O(k),與集合中元素的多少無關,這是其他數據結構都不能完成的。

 

如果可能元素範圍不是很大,並且大多數都在集合中,則使用確定性的bit array遠遠勝過使用布隆過濾器。因爲bit array對於每個可能的元素空間上只需要1 bit,add和query的時間複雜度只有O(1)。注意到這樣一個哈希表(bit array)只有在忽略collision並且只存儲元素是否在其中的二進制信息時,纔會獲得空間和時間上的優勢,而在此情況下,它就有效地稱爲了k=1的布隆過濾器。

 

而當考慮到collision時,對於有m個slot的bit array或者其他哈希表(即k=1的布隆過濾器),如果想要保證1%的誤判率,則這個bit array只能存儲m/100個元素,因而有大量的空間被浪費,同時也會使得空間複雜度急劇上升,這顯然不是space efficient的。解決的方法很簡單,使用k>1的布隆過濾器,即k個hash function將每個元素改爲對應於k個bits,因爲誤判度會降低很多,並且如果參數k和m選取得好,一半的m可被置爲爲1,這充分說明了布隆過濾器的space efficient性。

 

三. 舉例說明

以垃圾郵件過濾中黑白名單爲例:現有1億個email的黑名單,每個都擁有8 bytes的指紋信息,則可能的元素範圍爲  clip_image002 ,對於bit array來說是根本不可能的範圍,而且元素的數量(即email列表)爲 clip_image002[6] ,相比於元素範圍過於稀疏,而且還沒有考慮到哈希表中的collision問題。

 

若採用哈希表,由於大多數採用open addressing來解決collision,而此時的search時間複雜度爲 :

clip_image002[8]

即若哈希表半滿(n/m = 1/2),則每次search需要probe 2次,因此在保證效率的情況下哈希表的存儲效率最好不超過50%。此時每個元素佔8 bytes,總空間爲:

clip_image002[10]

若採用Perfect hashing(這裏可以採用Perfect hashing是因爲主要操作是search/query,而並不是add和remove),雖然保證worst-case也只有一次probe,但是空間利用率更低,一般情況下爲50%,worst-case時有不到一半的概率爲25%。

 

若採用布隆過濾器,取k=8。因爲n爲1億,所以總共需要 clip_image002[12] 被置位爲1,又因爲在保證誤判率低且k和m選取合適時,空間利用率爲50%(後面會解釋),所以總空間爲:

clip_image002[14]

所需空間比上述哈希結構小得多,並且誤判率在萬分之一以下。

 

四. 誤判概率的證明和計算

假設布隆過濾器中的hash function滿足simple uniform hashing假設:每個元素都等概率地hash到m個slot中的任何一個,與其它元素被hash到哪個slot無關。若m爲bit數,則對某一特定bit位在一個元素由某特定hash function插入時沒有被置位爲1的概率爲:

clip_image002[16]

則k個hash function中沒有一個對其置位的概率爲:

clip_image002[18]

如果插入了n個元素,但都未將其置位的概率爲:

clip_image002[20]

則此位被置位的概率爲:

clip_image002[22]

 

現在考慮query階段,若對應某個待query元素的k bits全部置位爲1,則可判定其在集合中。因此將某元素誤判的概率爲:

clip_image002[24]

由於 clip_image002[26],並且 clip_image002[28]  當m很大時趨近於0,所以

clip_image002[30]

從上式中可以看出,當m增大或n減小時,都會使得誤判率減小,這也符合直覺。

 

現在計算對於給定的m和n,k爲何值時可以使得誤判率最低。設誤判率爲k的函數爲:

clip_image002[32]

設  clip_image002[34] , 則簡化爲

clip_image002[36],兩邊取對數

clip_image002[38]  , 兩邊對k求導

clip_image002[40]

下面求最值

clip_image002[42]

clip_image002[44] clip_image004

clip_image002[44] clip_image006

clip_image002[44] clip_image008

clip_image002[44] clip_image010

clip_image002[44] clip_image012

clip_image002[44] clip_image014

clip_image002[44] clip_image002[52]

因此,即當 clip_image002[54]  時誤判率最低,此時誤判率爲:

clip_image002[56]

可以看出若要使得誤判率≤1/2,則:

clip_image002[58]

這說明了若想保持某固定誤判率不變,布隆過濾器的bit數m與被add的元素數n應該是線性同步增加的。

 

五. 設計和應用布隆過濾器的方法

應用時首先要先由用戶決定要add的元素數n和希望的誤差率P。這也是一個設計完整的布隆過濾器需要用戶輸入的僅有的兩個參數,之後的所有參數將由系統計算,並由此建立布隆過濾器。

 

系統首先要計算需要的內存大小m bits:

clip_image002[60]

 

再由m,n得到hash function的個數:

clip_image002[52]

 

至此係統所需的參數已經備齊,接下來add n個元素至布隆過濾器中,再進行query。

 

根據公式,當k最優時:

clip_image002[66]

clip_image004[8]

因此可驗證當P=1%時,存儲每個元素需要9.6 bits:

clip_image002[70]

而每當想將誤判率降低爲原來的1/10,則存儲每個元素需要增加4.8 bits:

clip_image002[72]

 

這裏需要特別注意的是,9.6 bits/element不僅包含了被置爲1的k位,還把包含了沒有被置爲1的一些位數。此時的

clip_image002[74]

纔是每個元素對應的爲1的bit位數。

 

clip_image002[76]   從而使得P(error)最小時,我們注意到:

clip_image002[78] 中的 clip_image002[80]  ,即

clip_image002[82]

此概率爲某bit位在插入n個元素後未被置位的概率。因此,想保持錯誤率低,布隆過濾器的空間使用率需爲50%。

 

下面給出自己的實現代碼,代碼如下:

複製代碼
  1 #include<cstdio>
  2 #include <cstdlib>
  3 #include <string>
  4 #include <iostream>
  5 #include <cassert>
  6 using namespace std;
  7 
  8 const unsigned char masks[8]={0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80};
  9 
 10 const int HashNum=2;        //hash函數的個數
 11 unsigned hash1(const char* s,unsigned sLength);
 12 unsigned hash2(const char* s,unsigned sLength);
 13 
 14 //函數指針,將字符串buffer作hash映射
 15 typedef unsigned (*hash_func_ptr)(const char* buffer,unsigned sLength);
 16 
 17 struct BloomFilter
 18 {
 19     unsigned n;    //BF的bit位數
 20     char *bits;    //使用char數組和存儲BF
 21     unsigned size;    //char數組的大小
 22     hash_func_ptr hashFun[HashNum];        //存儲hash函數的函數指針數組
 23 };
 24 
 25 BloomFilter* bloom_init(unsigned n)
 26 {
 27     BloomFilter* bf=new BloomFilter;
 28     assert(bf);
 29     bf->n=n;
 30     bf->size=(n+7)>>3;    //相當於(n+7)/8,求出需要多少個char存儲n個bit
 31     bf->hashFun[0]=hash1;
 32     bf->hashFun[1]=hash2;
 33     bf->bits=new char[bf->size];
 34     for (int i=0;i<bf->size;i++)    //bits所有位置零
 35     {
 36         bf->bits[i] &=0;
 37     }
 38     return bf;
 39 }
 40 
 41 //插入操作
 42 //data需要插入的數據,作hash映射之後插入
 43 //dLength data的數組長度
 44 bool bloom_insert(BloomFilter* bf,const char* data,unsigned dLength)
 45 {
 46     unsigned h1=bf->hashFun[0](data,dLength) % (bf->n);    //在哪一位置1
 47     unsigned h2=bf->hashFun[1](data,dLength) % (bf->n);
 48     unsigned idx1=h1>>3;    //具體到char數組的哪個下標
 49     unsigned idx2=h2>>3;
 50     bf->bits[idx1] |= masks[h1%8];        //將相應位置1
 51     bf->bits[idx2] |= masks[h2%8];
 52     return true;
 53 }
 54 
 55 //判斷檢查操作
 56 bool bloom_check(BloomFilter* bf,const char* data, unsigned dLength)
 57 {
 58     unsigned h1=bf->hashFun[0](data,dLength) % (bf->n);
 59     unsigned h2=bf->hashFun[1](data,dLength) % (bf->n);
 60     unsigned idx1=h1>>3;
 61     unsigned idx2=h2>>3;
 62 
 63     //只有當所有位都爲1時,返回true
 64     return ((bf->bits[idx1] & masks[h1%8])
 65         && (bf->bits[idx2] & masks[h2%8]));
 66 }
 67 
 68 void bloom_destroy(BloomFilter* bf)
 69 {
 70     if (bf)
 71     {
 72         delete[] bf->bits;
 73         delete bf;
 74     }
 75 }
 76 unsigned hash1(const char* s, unsigned sLength)
 77 {
 78     int hash=1315423911;
 79     unsigned len=0;
 80     while (len<sLength)
 81     {
 82         hash ^=(hash<<5)+s[len]+(hash>>2);
 83         len++;
 84     }
 85     return (hash & 0x7fffffff);
 86 }
 87 unsigned hash2(const char* s,unsigned sLength)
 88 {
 89     int hash=0;
 90     unsigned len=0;
 91     while (len<sLength)
 92     {
 93         hash=(hash<<6) + (hash<<16)-hash+s[len];
 94         len++;
 95     }
 96     return (hash & 0x7fffffff);
 97 }
 98 
 99 int main()
100 {
101     const int weblength=5;
102     const char* website[weblength]={"www.baidu.com","www.google.com"
103         ,"www.sina.com","www.renren.com","www.sohu.com"};
104     const int n=655371;
105     BloomFilter* bf=bloom_init(n);
106     for (int i=0;i<weblength;i++)
107     {
108         bloom_insert(bf,website[i],strlen(website[i]));
109     }
110     for (int i=0;i<weblength;i++)
111     {
112         cout<<bloom_check(bf,website[i],strlen(website[i]))<<"  ";
113     }
114     cout<<endl;
115     const int weblength1=4;
116     const char* website1[weblength1]={"www.yahoo.com","www.dangdang.com","www.360buy.com","www.meizu.com"};
117     for (int i=0;i<weblength1;i++)
118     {
119         cout<<bloom_check(bf,website1[i],strlen(website1[i]))<<"  ";
120     }
121     cout<<endl;
122     bloom_destroy(bf);
123 }
124 
125 
126 //輸出結果:
127 //1  1  1  1  1
128 //0  0  0  0
129 //請按任意鍵繼續. . .
複製代碼

注意:代碼實現主要是要熟悉位操作

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章