特殊的資料結構(布隆過濾器)的原理和實現及探究

深度Linux發表於2020-11-04

一、布隆過濾器的使用價值

有時候我們需要判斷一個元素是否在一個集合中。比如,在字處理軟體中,需要檢查一個單詞是否拼寫正確(也就是要判斷它是否在已知的字典裡);在警察系統中,一個嫌疑人的名字是否出現在嫌疑名單上;在網路爬蟲裡,一個網址是否已經被訪問過,等等。

最直接的方法就是講集合中的元素存在計算機中,遇到一個新元素時,將它和集合中的元素直接比較即可。一般來講,計算機中的集合是用雜湊表(Hash Table)來儲存的。它的好處是快速準確,缺點是耗費儲存空間。

為什麼說耗費儲存空間呢?其根本原因是雜湊表方法需要把實實在在的具有特定長度(每個Email地址對應成一個8位元組的資訊指紋)的元素的資訊指紋儲存在記憶體或硬碟中的雜湊表中,這個儲存量在實際應用中一般是相當大的。比如每儲存一億個Email地址,需要0.8G大小的數字指紋儲存空間,考慮到雜湊表的儲存空間利用率一般只有一半,所以需要1.6G的儲存空間。如果儲存幾十億上百億的Email地址,那就需要百億位元組的記憶體儲存空間。

而布隆過濾器只需要雜湊表1/8到1/4的大小就能解決同樣的問題,它實際上是一個很長的二進位制向量和一系列的隨機對映函式。

下面以WEB頁面地址的儲存為例來說明布隆過濾器的工作原理:

假定儲存一億個WEB頁面地址,先建立一個16億二進位制(位元),即2億位元組的向量,然後將這16億個二進位制位清零。對於每一個WEB頁面地址X,用8個隨機數產生器(f1,f2,…,f8)。再用一個隨機數產生器G把這8個資訊指紋對映到1-16億中的8個自然數g1,g2,…g8。現在把這8個位置的二進位制位都置為1。對著一億個WEB頁面地址都進行這樣的處理後,一個針對WEB頁面的布隆過濾器就建成了,見下圖。

蒲隆地過濾器的對映方法:
現在,讓我們看看如何用布隆過濾器來檢測一個WEB網頁地址Y是否已經被我們收錄。用相同的8個隨機數生成器(f1,f2,…,f8)對這個WEB網頁地址產生8個資訊指紋s1,s2,…s8,然後將這8個指紋對應到布隆過濾器的8個二進位制位,分別是t1,t2,…,t8。如果Y已被收錄,顯然t1,t2,…,t8對應的8個二進位制位一定是1。通過這樣的方式我們能夠很快地確定一個WEB頁面是否已被我們收錄。

二、布隆過濾器的實現

布隆過濾器實現程式碼:

#encoding=UTF-8 
'''
Created on 2014年6月21日
@author: jin
'''
import BitVector  
class MyHash():#雜湊類,根據不同引數初始化後作為不同的雜湊函式 
      
    def __init__(self, cap, seed):  
        self.cap = cap  
        self.seed = seed  
      
    def hash(self, value): #計算雜湊值得過程 
        ret = 0  
        for i in range(len(value)):  
            ret += self.seed*ret + ord(value[i]) #ord()函式計算傳入的url字串中每一個字元在ASCII碼錶中對應的順序值
        return (self.cap-1) & ret  #返回雜湊值,即在位元序列中的位置    
 
class BloomFilter(): 
        
    def __init__(self, BIT_SIZE=1<<31):  
        self.BIT_SIZE = 1 << 31  #不攏過濾器的位元數,
        self.seeds = [5, 7, 11, 13,19, 31, 37, 61]  #8個種子,用於產生hash函式
        self.bitset = BitVector.BitVector(size=self.BIT_SIZE)  
        self.hashFuncList = []           
        for i in range(len(self.seeds)):  
            self.hashFuncList.append(MyHash(self.BIT_SIZE, self.seeds[i]))  #對每個種子,建立一個MyHash物件,一共8個          
    def insert(self, value):  #插入值,這裡並非真正地插入並儲存,而是把該值對應的8個位置的位元位置為1
        for function in self.hashFuncList:  
            locationBit = function.hash(value)  #計算應該置為1的位元位
            self.bitset[locationBit] = 1  
    def isContaions(self, value):  
        if value == None:  
            return False  
        ret = True  
        for f in self.hashFuncList:  
            locationBit = f.hash(value)  
            ret = ret & self.bitset[locationBit]  #可以看出,對8個雜湊函式,只要有一個為0,那麼將返回0,即該值尚未存在
        return ret 
       
def Main(): #主函式
 
    fd = open("urls.txt")  #有重複的網址 http://www.kalsey.com/tools/buttonmaker/
    bloomfilter = BloomFilter()  
    while True:  
        url = fd.readline()  
        if cmp(url, 'exit') == 0:
            print 'complete and exit now'
            break  
        elif bloomfilter.isContaions(url) == False:  
            bloomfilter.insert(url)  
        else:  
            print 'url :%s has exist' % url                 
Main()  

url.txt內部儲存有一系列網址,最後一行是‘exit’,內容如下:

http://sourceforge.net/robots.txt
http://sourceforge.net/
http://sourceforge.net
http://sourceforge.net and https://sourceforge.net
http://sourceforge.net/sitemap.xml
http://sourceforge.net/allura_sitemap/sitemap.xml
http://sourceforge.net/directory_sitemap.xml
http://a.fsdn.com
http://a.fsdn.com/con/img/sftheme/favicon.ico
http://a.fsdn.com/con/js/min/sf.head.js
http://a.fsdn.com/con/js/sftheme/dd_belatedpng.js
http://fonts.googleapis.com
http://fonts.googleapis.com/css
http://a.fsdn.com/con/css/sf.css
http://sourceforge.net/blog/feed/
http://email.playtime.uni.cc/ 
http://services.nexodyne.com/email/ 
http://gizmo967.mgs3.org/Gmail/ 
http://www.hkwebs.net/catalog/tools/gmail/ 
http://sagittarius.dip.jp/~toshi/cgi-bin/designmail/designmail.html 
http://www.eoool.com/
http://sourceforge.netand
https://sourceforge.net
http://a.fsdn.com/con/js/adframe.js
http://sourceforge.net/directory/
http://kalsey.com/tools/buttonmaker/ 
http://www.lucazappa.com/brilliantMaker/buttonImage.php 
http://www.feedforall.com/public/rss-graphic-tool.htm  
http://www.yugatech.com/make.php 
http://www.hkwebs.net/catalog/tools/buttonmaker/index.php
http://phorum.com.tw/Generator.aspx 
http://www.logoyes.com/lc_leftframe.htm 
http://cooltext.com/Default.aspx
exit

執行效果如下,可以看到未發生儲存地址衝突:

complete and exit now

往url.txt裡面再增加一個原來未有的網址:

http://www.kalsey.com/tools/buttonmaker/

再次執行,竟然發生了衝突,如下:

url :http://www.kalsey.com/tools/buttonmaker/
 has exist
complete and exit now

這說明此網址和另外一個網址對應的8個資訊指紋相同,雖然它們本身的值是不同的,這就產生了衝突。

可以看到布隆過濾器有一定的誤識別率。下面我們對其進行分析。

三、誤識別率的問題

假定布隆過濾器有m位元,裡面有n個元素,每個元素對應k個資訊指紋的雜湊函式,當然m個位元里有0也有1。我們假定某個位元為0,在這個布隆過濾器裡插入一個元素,他的第一個雜湊函式會把過濾器中的某個位元置為1,理想情況下,任一位元位被置1的概率是1/m,它依然為0的概率則是1-1/m。

在這裡插入圖片描述

對於過濾器中的一個特定位置,如果這個元素的k個雜湊函式都沒有把它設定成1,其概率是
在這裡插入圖片描述

。如果過濾器插入第二個元素,這個特定位置仍不被置1的概率是
在這裡插入圖片描述

,類似的,插入n個元素其仍為0的概率是
在這裡插入圖片描述

。反過來,一個位元在插入n個元素後,被置1的概率則是

在這裡插入圖片描述

現在假定這n個元素都放到布隆過濾器中了,新來的一個不在集合中的元素,由於它的資訊指紋的雜湊函式都是隨機的,因此,它的第一個雜湊函式正好命中某個值為1的位元的概率就是上述概率。一個不再集合中的元素被誤識別為已經在集合中,需要所有的雜湊函式對應的位元值均為1,其概率為

在這裡插入圖片描述

在這裡插入圖片描述

我們下面對簡化的誤識別率的公式進行研究。

在這裡插入圖片描述

圖2 誤識別率與m/n的關係

圖2的程式碼:

k=8
r =  np.linspace(1,50,1000)
p = np.power(1-np.exp(-k/r),k)
plt.title('misjudgment & m/n')
plt.plot(r,p)
plt.xlabel('m/n')  
plt.ylabel('misjudgment')  
plt.show()
 

在這裡插入圖片描述

圖3 誤識別率與k的關係
圖3的程式碼:

handlebars
r = 30;
k = np.linspace(1,10,100)
p = np.power(1-np.exp(-k/r),k)
plt.title('misjudgment & k')
plt.xlabel('k')  
plt.ylabel('misjudgment')  
plt.plot(k,p)
plt.show()

在這裡插入圖片描述

本群免費分享學習資料(C/C++,Linux,golang,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,ffmpeg,TCP/IP,協程,DPDK,嵌入式)等。
交流討論領取資料請加群Q:1106675687。

相關文章