本文始發於我的公衆號:TechFlow,原創不易,求個關注python
今天的文章和你們一塊兒來學習大數據領域一個常常用到的算法——布隆過濾器。若是看過《數學之美》的同窗對它應該並不陌生,它常常用在集合的判斷上,在海量數據的場景當中用來快速地判斷某個元素在不在一個龐大的集合當中。它的原理不難,可是設計很是巧妙,老實講在看《數學之美》以前,我也沒有據說過這個數據結構,因此這篇文章也是我本身學習的筆記。算法
在我以前的理解當中,若是想要判斷某個元素在不在集合當中,經典的結構應該是平衡樹和hash table。可是不管是哪種方法,都逃不開一點,都須要存儲原值。數組
好比在爬蟲場景當中,咱們須要記錄下以前爬過的網站。咱們要將以前的網址所有都存儲在容器裏,而後在遇到新網站的時候去判斷是否已經爬過了。在這個問題當中,咱們並不關心以前爬過的網站有哪些,咱們只關心如今的網站有沒有在以前出現過。也就是說以前出現過什麼不重要,如今的有沒有出現過才重要。數據結構
咱們利用平衡樹或者是Trie或者是AC自動機等數據結構和算法能夠實現高效的查找,可是都離不開存儲下全部的字符串。想象一下,一個網址大概上百個字符,大約0.1KB,若是是一億個網址,就須要10GB了,若是是一百億一千億呢?顯然這麼大的規模就很麻煩了,今天要介紹的布隆過濾器就能夠解決這個問題,並且不須要存儲下原值,這是一個很是巧妙的作法,讓咱們一塊兒來看下它的原理。app
布隆過濾器自己的結構很是簡單,就是一個一維的bool型的數組,也就是說每一位只有0或者1,是一個bit,這個數組的長度是m。對於每一個新增的項,咱們使用K種不一樣的hash算法對它計算hash值。因此咱們能夠獲得K個hash值,咱們用hash值對m取模,假設是x。剛開始的時候數組內所有都是0,咱們把全部x對應的位置標記爲1。數據結構和算法
舉個例子,假設咱們一開始m是10,K是3。咱們遇到第一個插入的值是」線性代數「,咱們對它hash以後獲得1,3,5,那麼咱們將對應的位置標記成1.學習
而後咱們又遇到了一個值是」高等數學「,hash以後獲得1,8,9,咱們仍是將對應位置賦值成1,會發現1這個位置對應的值已是1了,咱們忽略就好。大數據
若是這個時候咱們想要判斷」機率統計」有沒有出現過,怎麼辦?很簡單,咱們對「機率統計」再計算hash值。假設獲得1,4,5,咱們去遍歷一下對應的位置,發現4這個位置是0,說明以前沒有添加過「機率統計」,顯然「機率統計」沒有出現過。網站
可是若是「機率統計」hash以後的結果是1,3,8呢?咱們判斷它出現過就錯了,答案很簡單,由於雖然1,3,8這個hash組合以前沒有出現過,可是對應的位置都在其餘元素中出現過了,這樣就出現偏差了。因此咱們能夠知道,布隆過濾器對於不存在的判斷是準確的,可是對於存在的判斷是有可能有錯誤的。spa
布隆過濾器的原理很簡單,明白了以後,咱們很容易寫出代碼:
# 插入元素 def BloomFilter(filter, value, hash_functions): m = len(filter) for func in hash_functions: idx = func(value) % m filter[idx] = True return filter # 判斷元素 def MemberInFilter(filter, value, hash_functions): m = len(filter) for func in hash_functions: idx = func(value) % m if not filter[idx]: return False return True
以前的例子當中應該展現得很明白了,布隆過濾器雖然好用,可是會存在bad case,也就是判斷錯誤的狀況。那麼,這種錯誤判斷髮生的機率有多大呢?
這個機率的計算也不難:因爲數組長度是\(m\),因此插入一個bit它被置爲1的機率是\(\frac{1}{m}\),插入一個元素須要插入k個hash值,因此插入一個元素,某一位沒有被置爲1的機率是\((1-\frac{1}{m})^k\)。插入n個元素以後,某一位依舊爲0的機率是\((1-\frac{1}{m})^{nk}\),它變成1的機率是\(1-(1-\frac{1}{m})^{nk}\)。
若是在某次判斷當中,有一個沒有出現過的元素被認爲已經在集合當中了,那麼也就是說它hash獲得的位置均已經在以前被置爲1了,這個時間發生的機率爲:
\[\displaystyle\left[1-(1-\frac{1}{m})^{nk}\right]^k \approx (1-e^{-\frac{kn}{m}})^k\]
這裏用到了一個極限:
\[\displaystyle\lim_{x \to -\infty}(1-\frac{1}{x})^{-x}=e\]
咱們來求一下衝突率最低時k的取值,爲了方便計算,咱們令\(b=e^{\frac{n}{m}}\),代入:
\[f(k) = (1-b^{-k})^k \\ \ln f(k) = k\ln(1-b^{-k})\]
兩邊求導:
\[ \begin{aligned} \frac{1}{f(k)}f'(k)&= ln(1-b^{-k}) + \frac{kb^{-k}\ln b}{1-b^{-k}} \end{aligned} \]
咱們令導數等於0,來求它的極值:
\[ \begin{aligned} \ln(1-b^{-k})(1-b^{-k})&=-kb^{-k}\ln b\\ \ln(1-b^{-k})(1-b^{-k})&=b^{-k}\ln b^{-k}\\ 1-b^{-k} &=b^{-k}\\ b^{-k} &= \frac{1}{2} \end{aligned} \]
咱們將\(b^{-k}=\frac{1}{2}\)代入,能夠求出最值時的\(k=\ln2\cdot\frac{m}{n} \approx 0.7\frac{m}{n}\)
同理,咱們也能夠預設定集合元素n和錯判率p,來求解對應的n,一樣利用上面的公式推算,能夠獲得\(m=-\frac{n\ln p}{(\ln2)^2}\)
若是咱們容許必定的容錯,而且可以大概估計會出現的元素的個數,那麼徹底可使用布隆過濾器來代替傳統的容器判重的方法。這樣不只效率極高,並且對於存儲的要求很是小。
原理也明白了,代碼也看懂了,這個時候咱們來思考一個問題:布隆過濾器能夠刪除元素嗎?
很遺憾,布隆過濾器是不支持刪除的。
由於布隆過濾器的每個bit並非獨佔的,頗有可能多個元素共享了某一位。若是咱們直接刪除這一位的話,會影響其餘的元素。
仍是用上面的例子舉例:咱們刪除線性代數,線性代數對應的位置是1,3,5,雖然咱們並無刪除高等數學,可是因爲咱們移除了高等數學也用到的位置1,若是咱們再去判斷高等數學是否存在就會獲得錯誤的結果,雖然咱們並無刪除它。
固然,在一些必需要有刪除功能的場景下,也是有辦法的。方法也很簡單,就是修改數據結構,將本來每一位一個bit改爲一個int,當咱們插入元素的時候,再也不是將bit設置爲true,而是讓對應的位置自增,而刪除的時候則是對應的位減一。這樣,咱們刪除單個結果就不會影響其餘元素了。
這種方法並非完美的,因爲布隆過濾器存在誤判的狀況,頗有可能咱們會刪除本來就不存在的值,這一樣會對其餘元素產生影響。
布隆過濾器是一個優缺點都很是明顯的數據結構,優勢很是出色:速度足夠快,內存消耗小,代碼實現簡單。可是缺點也很明顯:不支持刪除元素,會有誤判的狀況。這樣特色鮮明的數據結構真的很是吸引人。
今天的文章就是這些,若是以爲有所收穫,請順手點個關注吧,大家的舉手之勞對我來講很重要。