布隆過濾器
布隆過濾器(英語:Bloom Filter)是1970年由布隆提出的。它實際上是一個很長的二進制向量和一系列隨機映射函數(shù)。布隆過濾器可以用于檢索一個元素是否在一個集合中。它的優(yōu)點是空間效率和查詢時間都遠遠超過一般的算法,缺點是有一定的誤識別率和刪除困難。
布隆過濾器 (Bloom Filter)是一種space efficient的概率型數(shù)據(jù)結(jié)構(gòu),在垃圾郵件過濾的黑白名單方法、爬蟲(Crawler)的網(wǎng)址判重模塊中等等經(jīng)常被用到。哈希表也能用于判斷元素是否在集合中,但是布隆過濾器只需要哈希表的1/8或1/4的空間復(fù)雜度就能完成同樣的問題。布隆過濾器可以插入元素,但不可以刪除已有元素。其中的元素越多,false positive rate(誤報率)越大,但是false negative (漏報)是不可能的。
基本概念
如果想判斷一個元素是不是在一個集合里,一般想到的是將集合中所有元素保存起來,然后通過比較確定。鏈表、樹、散列表(又叫哈希表,Hash table)等等數(shù)據(jù)結(jié)構(gòu)都是這種思路。但是隨著集合中元素的增加,我們需要的存儲空間越來越大。同時檢索速度也越來越慢,上述三種結(jié)構(gòu)的檢索時間復(fù)雜度分別為 O(n),O(log n),O(n/k)
。
布隆過濾器的原理是,當一個元素被加入集合時,通過K個散列函數(shù)將這個元素映射成一個位數(shù)組中的K個點,把它們置為1。檢索時,我們只要看看這些點是不是都是1就(大約)知道集合中有沒有它了:如果這些點有任何一個0,則被檢元素一定不在;如果都是1,則被檢元素很可能在。這就是布隆過濾器的基本思想。
算法描述
一個empty bloom filter是一個有m bits的bit array,每一個bit位都初始化為0。并且定義有k個不同的hash function,每個都以uniform random distribution將元素hash到m個不同位置中的一個。在下面的介紹中n為元素數(shù),m為布隆過濾器或哈希表的slot數(shù),k為布隆過濾器重hash function數(shù)。
為了add一個元素,用k個hash function將它hash得到bloom filter中k個bit位,將這k個bit位置1。
為了query一個元素,即判斷它是否在集合中,用k個hash function將它hash得到k個bit位。若這k bits全為1,則此元素在集合中;若其中任一位不為1,則此元素比不在集合中(因為如果在,則在add時已經(jīng)把對應(yīng)的k個bits位置為1)。
不允許remove元素,因為那樣的話會把相應(yīng)的k個bits位置為0,而其中很有可能有其他元素對應(yīng)的位。因此remove會引入false negative,這是絕對不被允許的。
當k很大時,設(shè)計k個獨立的hash function是不現(xiàn)實并且困難的。對于一個輸出范圍很大的hash function(例如MD5產(chǎn)生的128 bits數(shù)),如果不同bit位的相關(guān)性很小,則可把此輸出分割為k份。或者可將k個不同的初始值(例如0,1,2, … ,k-1)結(jié)合元素,feed給一個hash function從而產(chǎn)生k個不同的數(shù)。
當add的元素過多時,即n/m過大時(n是元素數(shù),m是bloom filter的bits數(shù)),會導(dǎo)致false positive過高,此時就需要重新組建filter,但這種情況相對少見。
優(yōu)點
相比于其它的數(shù)據(jù)結(jié)構(gòu),布隆過濾器在空間和時間方面都有巨大的優(yōu)勢。布隆過濾器存儲空間和插入/查詢時間都是常數(shù)(O(k))。另外,散列函數(shù)相互之間沒有關(guān)系,方便由硬件并行實現(xiàn)。布隆過濾器不需要存儲元素本身,在某些對保密要求非常嚴格的場合有優(yōu)勢。
布隆過濾器可以表示全集,其它任何數(shù)據(jù)結(jié)構(gòu)都不能;
缺點
但是布隆過濾器的缺點和優(yōu)點一樣明顯。誤算率是其中之一。隨著存入的元素數(shù)量增加,誤算率隨之增加。但是如果元素數(shù)量太少,則使用散列表足矣。
另外,一般情況下不能從布隆過濾器中刪除元素。我們很容易想到把位數(shù)組變成整數(shù)數(shù)組,每插入一個元素相應(yīng)的計數(shù)器加1, 這樣刪除元素時將計數(shù)器減掉就可以了。然而要保證安全地刪除元素并非如此簡單。首先我們必須保證刪除的元素的確在布隆過濾器里面。這一點單憑這個過濾器是無法保證的。另外計數(shù)器回繞也會造成問題。
在降低誤算率方面,有不少工作,使得出現(xiàn)了很多布隆過濾器的變種。
舉例說明布隆過濾器的空間優(yōu)勢
先來一個結(jié)論:對于一個有1%誤報率和一個最優(yōu)k值的布隆過濾器來說,無論元素的類型及大小,每個元素只需要9.6 bits來存儲。這個優(yōu)點一部分繼承自array的緊湊性,一部分來源于它的概率性。如果你認為1%的誤報率太高,那么對每個元素每增加4.8 bits,我們就可將誤報率降低為原來的1/10。add和query的時間復(fù)雜度都為O(k),與集合中元素的多少無關(guān),這是其他數(shù)據(jù)結(jié)構(gòu)都不能完成的。k是hash函數(shù)的個數(shù)。
舉例: 現(xiàn)有1億個email的黑名單,元素的數(shù)量(即email列表)為 108。若采用布隆過濾器,取k=8(k為hash函數(shù)個數(shù))。因為n為1億,所以總共需要8*108。又因為在保證誤判率低(后面解釋)且k和m選取合適時,空間利用率為50%(后面會解釋),所以總空間為
所需空間比上述哈希結(jié)構(gòu)或者數(shù)組小得多,并且誤判率在萬分之一以下。為什么可以這樣算,可以看下面。
誤判概率的證明和計算
該過程的詳細說明來自于這個文章http://www.cnblogs.com/allensun/archive/2011/02/16/1956532.html,為了看懂求導(dǎo)過程,需要復(fù)習數(shù)學知識。
對某一特定bit位在一個元素由某特定hash function插入時沒有被置位為1的概率為:
則k個hash function中沒有一個對其置位的概率為:
現(xiàn)在考慮query階段,若對應(yīng)某個待query元素的k bits全部置位為1,則可判定其在集合中。因此將某元素誤判的概率為:
,并且1/m 當m很大時趨近于0,所以現(xiàn)在計算對于給定的m和n,k為何值時可以使得誤判率最低。設(shè)誤判率為k的函數(shù)為:
因為等式右邊的底數(shù)上是函數(shù),指數(shù)上也是函數(shù),沒有方法求這樣組合函數(shù)的導(dǎo)數(shù),只能取對數(shù)之后,變成乘法。我們有兩個函數(shù)相乘的求導(dǎo)方法,求導(dǎo)的幾個方法可以看參考資料,有很好的視頻說明。
兩邊對k求導(dǎo)得,這邊涉及到乘法求導(dǎo),對數(shù)求導(dǎo),冪函數(shù)求導(dǎo):
下面求最值,
時誤判率最低,此時誤判率為:紅圈中的等式是把兩邊看成xln(x)這種形式得到的,和該函數(shù)的單調(diào)性相關(guān)。數(shù)學上能不能這么操作我還不太清楚。數(shù)學好的大神可以留言解釋一下。
可以看出若要使得誤判率≤1/2,則:
這說明了若想保持某固定誤判率不變,布隆過濾器的bit數(shù)m與被add的元素數(shù)n應(yīng)該是線性同步增加的。
設(shè)計和應(yīng)用布隆過濾器的方法
應(yīng)用時首先要先由用戶決定要add的元素數(shù)n和希望的誤差率P。這也是一個設(shè)計完整的布隆過濾器需要用戶輸入的僅有的兩個參數(shù),之后的所有參數(shù)將由系統(tǒng)計算,并由此建立布隆過濾器。
系統(tǒng)首先要計算需要的內(nèi)存大小m bits:
再由m,n得到hash function的個數(shù):
至此系統(tǒng)所需的參數(shù)已經(jīng)備齊,接下來add n個元素至布隆過濾器中,再進行query。
根據(jù)公式,當k最優(yōu)時:
因此可驗證當P=1%時,存儲每個元素需要9.6 bits:
而每當想將誤判率降低為原來的1/10,則存儲每個元素需要增加4.8 bits:
這里需要特別注意的是,9.6 bits/element不僅包含了被置為1的k位,還把包含了沒有被置為1的一些位數(shù)。此時的 從而使得P(error)最小時,我們注意到: 中的
此概率為某bit位在插入n個元素后未被置位的概率。因此,想保持錯誤率低,布隆過濾器的空間使用率需為50%。
Neo中的布隆過濾器
上面的內(nèi)容大部分抄襲http://www.cnblogs.com/allensun/archive/2011/02/16/1956532.html,原作者寫的太好了,我只是加上一些我的理解,方便數(shù)學不好的道友理解。下面我們看看Neo中的Bloom Filter。
using System.Collections;
using System.Linq;
namespace Neo.Cryptography
{
public class BloomFilter
{
private readonly uint[] seeds;
private readonly BitArray bits;
public int K => seeds.Length;
public int M => bits.Length;
public uint Tweak { get; private set; }
public BloomFilter(int m, int k, uint nTweak, byte[] elements = null)
{
this.seeds = Enumerable.Range(0, k).Select(p => (uint)p * 0xFBA4C795 + nTweak).ToArray();
this.bits = elements == null ? new BitArray(m) : new BitArray(elements);
this.bits.Length = m;
this.Tweak = nTweak;
}
public void Add(byte[] element)
{
foreach (uint i in seeds.AsParallel().Select(s => element.Murmur32(s)))
bits.Set((int)(i % (uint)bits.Length), true);
}
public bool Check(byte[] element)
{
foreach (uint i in seeds.AsParallel().Select(s => element.Murmur32(s)))
if (!bits.Get((int)(i % (uint)bits.Length)))
return false;
return true;
}
public void GetBits(byte[] newBits)
{
bits.CopyTo(newBits, 0);
}
}
}
前面講了這么多,代碼竟然這么短,分析分析。
- 構(gòu)造函數(shù)傳入了m(多少位),k(hash函數(shù)種類),這個和我們前面分析根據(jù)p(錯誤率),和n(要插入的元素)來構(gòu)造的思路不一樣。所以Neo的這個版本應(yīng)該是一個簡化版本,輸入的數(shù)據(jù)n應(yīng)該是有范圍的,具體的范圍我們后面運行整個區(qū)塊鏈的時候在觀察,現(xiàn)在不知道n的個數(shù)有多大。
- hash函數(shù)使用了Murmur32,然后傳入不同的seed模擬不同的hash函數(shù),這個是可以的。
- 使用linq,函數(shù)式編程代碼非常簡潔,這也是C#的一個優(yōu)勢啊。
- add,check函數(shù)都很容易看懂,確實實現(xiàn)很簡潔。
總結(jié)
Bloom Filter是牛逼的數(shù)據(jù)結(jié)構(gòu),因為有很多數(shù)學知識在里面,雖然代碼不長,但是能看完這篇文章的人,會感受到代碼之美。
參考資料
布隆過濾器
知乎里面關(guān)于e的討論
An Intuitive Guide To Exponential Functions & e
Bloom Filters - the math
布隆過濾器 (Bloom Filter) 詳解
What is MurmurHash3 seed parameter?
數(shù)學學習資料
各種字符串Hash函數(shù)
怎樣在 Markdown 中使用數(shù)學公式
對數(shù)求導(dǎo)公式
導(dǎo)數(shù)法求函數(shù)最值