參考:
https://blog.csdn.net/pipisorry/article/details/62443757
https://www.cnblogs.com/liyulong1982/p/6013002.html
https://blog.csdn.net/pipisorry/article/details/64127666
BitMap算法
http://blog.csdn.net/pipisorry/article/details/62443757
BitMap
BitMap從字面的意思,很多人認為是位圖,其實准確的來說,翻譯成基於位的映射。
在所有具有性能優化的數據結構中,大家使用最多的就是hash表,是的,在具有定位查找上具有O(1)的常量時間,多么的簡潔優美。但是數據量大了,內存就不夠了。
當然也可以使用類似外排序來解決問題的,由於要走IO所以時間上又不行。
所謂的Bit-map就是用一個bit位來標記某個元素對應的Value, 而Key即是該元素。由於采用了Bit為單位來存儲數據,因此在存儲空間方面,可以大大節省。
其實如果你知道計數排序的話(算法導論中有一節講過),你就會發現這個和計數排序很像。
bitmap應用
1)可進行數據的快速查找,判重,刪除,一般來說數據范圍是int的10倍以下。
2)去重數據而達到壓縮數據
還可以用於爬蟲系統中url去重、解決全組合問題。
BitMap應用:排序示例
假設我們要對0-7內的5個元素(4,7,2,5,3)排序(這里假設這些元素沒有重復)。那么我們就可以采用Bit-map的方法來達到排序的目的。要表示8個數,我們就只需要8個Bit(1Bytes),首先我們開辟1Byte的空間,將這些空間的所有Bit位都置為0(如下圖:)

然后遍歷這5個元素,首先第一個元素是4,那么就把4對應的位置為1(可以這樣操作 p+(i/8)|(0×01<<(i%8)) 當然了這里的操作涉及到Big-ending和Little-ending的情況,這里默認為Big-ending。不過計算機一般是小端存儲的,如intel。小端的話就是將倒數第5位置1),因為是從零開始的,所以要把第五位置為一(如下圖):

然后再處理第二個元素7,將第八位置為1,,接着再處理第三個元素,一直到最后處理完所有的元素,將相應的位置為1,這時候的內存的Bit位的狀態如下:

然后我們現在遍歷一遍Bit區域,將該位是一的位的編號輸出(2,3,4,5,7),這樣就達到了排序的目的。
bitmap排序復雜度分析
Bitmap排序需要的時間復雜度和空間復雜度依賴於數據中最大的數字。
bitmap排序的時間復雜度不是O(N)的,而是取決於待排序數組中的最大值MAX,在實際應用上關系也不大,比如我開10個線程去讀byte數組,那么復雜度為:O(Max/10)。也就是要是讀取的,可以用多線程的方式去讀取。時間復雜度方面也是O(Max/n),其中Max為byte[]數組的大小,n為線程大小。
空間復雜度應該就是O(Max/8)bytes吧
BitMap算法流程
假設需要排序或者查找的最大數MAX=10000000(lz:這里MAX應該是最大的數而不是int數據的總數!),那么我們需要申請內存空間的大小為int a[1 + MAX/32]。
其中:a[0]在內存中占32為可以對應十進制數0-31,依次類推:
bitmap表為:
a[0]--------->0-31
a[1]--------->32-63
a[2]--------->64-95
a[3]--------->96-127
..........

我們要把一個整數N映射到Bit-Map中去,首先要確定把這個N Mapping到哪一個數組元素中去,即確定映射元素的index。我們用int類型的數組作為map的元素,這樣我們就知道了一個元素能夠表示的數字個數(這里是32)。於是N/32就可以知道我們需要映射的key了。所以余下來的那個N%32就是要映射到的位數。
1.求十進制數對應在數組a中的下標:
先由十進制數n轉換為與32的余可轉化為對應在數組a中的下標。
如十進制數0-31,都應該對應在a[0]中,比如n=24,那么 n/32=0,則24對應在數組a中的下標為0。又比如n=60,那么n/32=1,則60對應在數組a中的下標為1,同理可以計算0-N在數組a中的下標。
i = N>>K % 結果就是N/(2^K)
Note: map的范圍是[0, 原數組最大的數對應的2的整次方數-1]。
2.求十進制數對應數組元素a[i]在0-31中的位m:
十進制數0-31就對應0-31,而32-63則對應也是0-31,即給定一個數n可以通過模32求得對應0-31中的數。
m = n & ((1 << K) - 1) %結果就是n%(2^K)
3.利用移位0-31使得對應第m個bit位為1
如a[i]的第m位置1:a[i] = a[i] | (1<<m)
如:將當前4對應的bit位置1的話,只需要1左移4位與B[0] | 即可。

Note: 1 p+(i/8)|(0×01<<(i%8))這樣也可以?
2 同理將int型變量a的第k位清0,即a=a&~(1<<k)
[編程珠璣]
BitMap算法評價
優點:
1. 運算效率高,不進行比較和移位;
2. 占用內存少,比如最大的數MAX=10000000;只需占用內存為MAX/8=1250000Byte=1.25M。
缺點:
1. 所有的數據不能重復,即不可對重復的數據進行排序。(少量重復數據查找還是可以的,用2-bitmap)。
2. 當數據類似(1,1000,10萬)只有3個數據的時候,用bitmap時間復雜度和空間復雜度相當大,只有當數據比較密集時才有優勢。
BitMap算法的拓展
Bloom filter可以看做是對bit-map的擴展。更大數據量的有一定誤差的用來判斷映射是否重復的算法。[Bloom Filter布隆過濾器]
問題及應用實例
1 使用位圖法判斷整形數組是否存在重復
判斷集合中存在重復是常見編程任務之一,當集合中數據量比較大時我們通常希望少進行幾次掃描,這時雙重循環法就不可取了。
位圖法比較適合於這種情況,它的做法是按照集合中最大元素max創建一個長度為max+1的新數組,然后再次掃描原數組,遇到幾就給新數組的第幾位置上1,如遇到 5就給新數組的第六個元素置1,這樣下次再遇到5想置位時發現新數組的第六個元素已經是1了,這說明這次的數據肯定和以前的數據存在着重復。這種給新數組初始化時置零其后置一的做法類似於位圖的處理方法故稱位圖法。它的運算次數最壞的情況為2N。如果已知數組的最大值即能事先給新數組定長的話效率還能提高一倍。
2 在2.5億個整數中找出不重復的整數,注,內存不足以容納這2.5億個整數
解法一:將bit-map擴展一下,采用2-Bitmap(每個數分配2bit,00表示不存在,01表示出現一次,10表示多次,11無意義)進行,共需內存2^32 * 2 bit=1 GB內存,還可以接受。然后掃描這2.5億個整數,查看Bitmap中相對應位,如果是00變01,01變10,10保持不變。所描完事后,查看bitmap,把對應位是01的整數輸出即可。
[c++直接實現代碼大數據:查找不重復的整數 ]
或者我們不用2bit來進行表示,我們用兩個bit-map即可模擬實現這個2bit-map,都是一樣的道理。
解法二:也可采用與第1題類似的方法,進行划分小文件的方法。然后在小文件中找出不重復的整數,並排序。然后再進行歸並,注意去除重復的元素。
解法三:(lz)類似解法2,只是划分時按照快排partition一樣划分,直到划分到每個塊都可以放入內存中。
[c實現]
2.1 一個序列里除了一個元素,其他元素都會重復出現3次,設計一個時間復雜度與空間復雜度最低的算法,找出這個不重復的元素。
3 已知某個文件內包含一些電話號碼,每個號碼為8位數字,統計不同號碼的個數。
8位最多99 999 999,大概需要99m個bit,大概10幾m字節的內存即可。 (可以理解為從0-99 999 999的數字,每個數字對應一個Bit位,所以只需要99M個Bit==1.2MBytes,這樣,就用了小小的1.2M左右的內存表示了所有的8位數的電話)
lz覺得這個是應該用計數排序類似的算法吧,而不是bitmap?
4 給40億個不重復的unsigned int的整數,沒排過序的,然后再給一個數,如何快速判斷這個數是否在那40億個數當中?
解析:bitmap算法就好辦多了。申請512M的內存,一個bit位代表一個unsigned int值,讀入40億個數,設置相應的bit位;讀入要查詢的數,查看相應bit位是否為1,為1表示存在,為0表示不存在。
Note: unsigned int最大數為2^32 - 1,所以需要2^32 - 1個位,也就是(2^32 - 1) / 8 /10 ^ 9G = 0.5G內存。
逆向思維優化:usinged int只有接近43億(unsigned int最大值為232-1=4294967295,最大不超過43億),所以可以用某種方式存沒有出現過的3億個數(使用數組{大小為3億中最大的數/8 bytes}存儲),如果出現在3億個數里面,說明不在40億里面。3億個數存儲空間一般小於40億個。(xx存儲4294967296需要512MB, 存儲294967296只需要35.16MBxx)
5 給定一個數組a,求所有和為SUM的兩個數。
如果數組都是整數(負數也可以,將所有數據加上最小的負數x,SUM += 2x就可以了)。如a = [1,2,3,4,7,8],先求a的補數組[8,7,6,5,2,1],開辟兩個數組b1,b2(最大數組長度為SUM/8/2{因為兩數滿足和為SUM,一個數<SUM/2,另一個數也就知道了},這樣每個b數組最大內存為SUM/(8*2*1024*1024) = 128M),使用bitmap算法和數組a分別設置b1b2對應的位為1,b1b2相與就可以得到和為SUM的兩個數其中一個數了。

BitMap的實現
Python
lz寫的一個比較好的實現
import os, sys, array
CWD = os.path.split(os.path.realpath(__file__))[0]
sys.path.append(os.path.join(CWD, '../..'))
def power2n(x):
'''
求比x大且是2的n次方的數
'''
for i in (1, 2, 4, 8, 16, 32): # 支持到64位int型,加上64則可以支持到128等等
x |= x >> i
# print(x + 1)
return x + 1
class BitMap():
def __init__(self):
self.K = 5
self.BIT_NUM = 1 << self.K
self.BIT_TYPE = 'I' # 32位unsighed int存儲位。note:可能8位char存儲對小數據更好一丟丟
self.shift = 0 # 如果數組中有<0的數,則所有數都要減去最小的那個負數
def fit(self, x):
'''
將數據讀入bitmap中存儲
'''
MIN_NUM = min(x)
if MIN_NUM < 0:
self.shift = -MIN_NUM # 如果數組中有<0的數,則所有數都要減去最小的那個負數
x = [i + self.shift for i in x]
else:
self.shift = 0
MAX_NUM = max(x)
num_int = power2n(MAX_NUM) >> self.K
num_int = num_int if num_int > 0 else 1 # 至少應該有一個數組
# print(num_int)
self.a = array.array(self.BIT_TYPE, [0] * num_int)
for xi in x:
self.set(xi)
def set(self, xi, value=1):
'''
設置數xi在數組a中對應元素對應的位為1
'''
array_ix = xi >> self.K # 數組的元素位置(從0開始)
bit_ix = xi & ((1 << self.K) - 1) # 數組元素中的bit位置(從0開始),取模
if value == 1:
self.a[array_ix] |= 1 << bit_ix # 對應的第bit_ix位置的2**bit_ix置1
else:
self.a[array_ix] &= ~((1 << bit_ix)) # 對應的第bit_ix位置的2**bit_ix置0
def show_array(self):
for ai in self.a:
print('{:032b}'.format(ai)) # bin(ai)
def search(self, xi):
'''
bitmap查找
'''
if self.shift != 0:
xi += self.shift
array_ix = xi >> self.K
bit_ix = xi & ((1 << self.K) - 1)
if (self.a[array_ix] & (1 << bit_ix)):
flag = True
else:
flag = False
return flag
def sort(self):
'''
bitmap排序
'''
sorted_x = []
for array_ix, ai in enumerate(self.a):
for bit_ix in range(self.BIT_NUM):
# 首先得到該第j位的掩碼(0x01<<j),將內存區中的,位和此掩碼作與操作。最后判斷掩碼是否和處理后的結果相同
if (ai & (1 << bit_ix)) == (1 << bit_ix):
sorted_x.append(self.BIT_NUM * array_ix + bit_ix)
# print(sorted_x)
if self.shift != 0:
sorted_x = [i - self.shift for i in sorted_x]
return sorted_x
def test():
bm = BitMap()
bm.fit([-3, -44, 7, 2, 5, 3, 32])
bm.show_array()
print(bm.search(7))
print(bm.search(6))
print(bm.sort())
test()
00000000000000000000000000000001
00000000000010101100001000000000
00000000000000000001000000000000
00000000000000000000000000000000
True
False
[-44, -3, 2, 3, 5, 7, 32]
Python package[bitsets 0.7.9]
Python 實現類似C++的bitset類:[Python 實現類似C++的bitset類 ]
C/C++
c++有bitset模塊
也可以自己實現:[海量數據處理算法—Bit-Map ]
Java
其實某些語言是對BitMap算法進行了封裝的,比如java中對應BitMap的數據結構就有BitSet類。其使用方法相當簡單,看看API就ok,還是給個例子吧:
import java.util.BitSet;
public class Test{
public static void main(String[] args) {
int [] array = new int [] {1,2,3,22,0,3};
BitSet bitSet = new BitSet(6);
//將數組內容組bitmap
for(int i=0;i<array.length;i++)
{
bitSet.set(array[i], true);
}
System.out.println(bitSet.size());
System.out.println(bitSet.get(3));
}
}
對應的bit位如果有對應整數那么通過bitSet.get(x)會返回true,反之false。其中x為BitMap位置下標。
from: http://blog.csdn.net/pipisorry/article/details/62443757
ref: [經典算法題每日演練——第十一題 Bitmap算法]
布隆過濾器(Bloom Filter)詳解
直觀的說,bloom算法類似一個hash set,用來判斷某個元素(key)是否在某個集合中。
和一般的hash set不同的是,這個算法無需存儲key的值,對於每個key,只需要k個比特位,每個存儲一個標志,用來判斷key是否在集合中。
算法:
1. 首先需要k個hash函數,每個函數可以把key散列成為1個整數
2. 初始化時,需要一個長度為n比特的數組,每個比特位初始化為0
3. 某個key加入集合時,用k個hash函數計算出k個散列值,並把數組中對應的比特位置為1
4. 判斷某個key是否在集合時,用k個hash函數計算出k個散列值,並查詢數組中對應的比特位,如果所有的比特位都是1,認為在集合中。
優點:不需要存儲key,節省空間
缺點:
1. 算法判斷key在集合中時,有一定的概率key其實不在集合中
2. 無法刪除
典型的應用場景:
某些存儲系統的設計中,會存在空查詢缺陷:當查詢一個不存在的key時,需要訪問慢設備,導致效率低下。
比如一個前端頁面的緩存系統,可能這樣設計:先查詢某個頁面在本地是否存在,如果存在就直接返回,如果不存在,就從后端獲取。但是當頻繁從緩存系統查詢一個頁面時,緩存系統將會頻繁請求后端,把壓力導入后端。
這是只要增加一個bloom算法的服務,后端插入一個key時,在這個服務中設置一次
需要查詢后端時,先判斷key在后端是否存在,這樣就能避免后端的壓力。

布隆過濾器[1](Bloom Filter)是由布隆(Burton Howard Bloom)在1970年提出的。它實際上是由一個很長的二進制向量和一系列隨機映射函數組成,布隆過濾器可以用於檢索一個元素是否在一個集合中。它的優點是空間效率和查詢時間都遠遠超過一般的算法,缺點是有一定的誤識別率(假正例False positives,即Bloom Filter報告某一元素存在於某集合中,但是實際上該元素並不在集合中)和刪除困難,但是沒有識別錯誤的情形(即假反例False negatives,如果某個元素確實沒有在該集合中,那么Bloom Filter 是不會報告該元素存在於集合中的,所以不會漏報)。
在日常生活中,包括在設計計算機軟件時,我們經常要判斷一個元素是否在一個集合中。比如在字處理軟件中,需要檢查一個英語單詞是否拼寫正確(也就是要判斷 它是否在已知的字典中);在 FBI,一個嫌疑人的名字是否已經在嫌疑名單上;在網絡爬蟲里,一個網址是否被訪問過等等。最直接的方法就是將集合中全部的元素存在計算機中,遇到一個新 元素時,將它和集合中的元素直接比較即可。一般來講,計算機中的集合是用哈希表(hash table)來存儲的。它的好處是快速准確,缺點是費存儲空間。當集合比較小時,這個問題不顯著,但是當集合巨大時,哈希表存儲效率低的問題就顯現出來 了。比如說,一個象 Yahoo,Hotmail 和 Gmai 那樣的公眾電子郵件(email)提供商,總是需要過濾來自發送垃圾郵件的人(spamer)的垃圾郵件。一個辦法就是記錄下那些發垃圾郵件的 email 地址。由於那些發送者不停地在注冊新的地址,全世界少說也有幾十億個發垃圾郵件的地址,將他們都存起來則需要大量的網絡服務器。如果用哈希表,每存儲一億 個 email 地址, 就需要 1.6GB 的內存(用哈希表實現的具體辦法是將每一個 email 地址對應成一個八字節的信息指紋(詳見:googlechinablog.com/2006/08/blog-post.html), 然后將這些信息指紋存入哈希表,由於哈希表的存儲效率一般只有 50%,因此一個 email 地址需要占用十六個字節。一億個地址大約要 1.6GB, 即十六億字節的內存)。因此存貯幾十億個郵件地址可能需要上百 GB 的內存。除非是超級計算機,一般服務器是無法存儲的[2]。(該段引用谷歌數學之美:http://www.google.com.hk/ggblog/googlechinablog/2007/07/bloom-filter_7469.html)
基本概念
如果想判斷一個元素是不是在一個集合里,一般想到的是將所有元素保存起來,然后通過比較確定。鏈表,樹等等數據結構都是這種思路. 但是隨着集合中元素的增加,我們需要的存儲空間越來越大,檢索速度也越來越慢。不過世界上還有一種叫作散列表(又叫哈希表,Hash table)的數據結構。它可以通過一個Hash函數將一個元素映射成一個位陣列(Bit Array)中的一個點。這樣一來,我們只要看看這個點是不是 1 就知道可以集合中有沒有它了。這就是布隆過濾器的基本思想。
Hash面臨的問題就是沖突。假設 Hash 函數是良好的,如果我們的位陣列長度為 m 個點,那么如果我們想將沖突率降低到例如 1%, 這個散列表就只能容納 m/100 個元素。顯然這就不叫空間有效了(Space-efficient)。解決方法也簡單,就是使用多個 Hash,如果它們有一個說元素不在集合中,那肯定就不在。如果它們都說在,雖然也有一定可能性它們在說謊,不過直覺上判斷這種事情的概率是比較低的。
優點
相比於其它的數據結構,布隆過濾器在空間和時間方面都有巨大的優勢。布隆過濾器存儲空間和插入/查詢時間都是常數。另外, Hash 函數相互之間沒有關系,方便由硬件並行實現。布隆過濾器不需要存儲元素本身,在某些對保密要求非常嚴格的場合有優勢。
布隆過濾器可以表示全集,其它任何數據結構都不能;
k 和 m 相同,使用同一組 Hash 函數的兩個布隆過濾器的交並差運算可以使用位操作進行。
缺點
但是布隆過濾器的缺點和優點一樣明顯。誤算率(False Positive)是其中之一。隨着存入的元素數量增加,誤算率隨之增加。但是如果元素數量太少,則使用散列表足矣。
另外,一般情況下不能從布隆過濾器中刪除元素. 我們很容易想到把位列陣變成整數數組,每插入一個元素相應的計數器加1, 這樣刪除元素時將計數器減掉就可以了。然而要保證安全的刪除元素並非如此簡單。首先我們必須保證刪除的元素的確在布隆過濾器里面. 這一點單憑這個過濾器是無法保證的。另外計數器回繞也會造成問題。
False positives 概率推導
假設 Hash 函數以等概率條件選擇並設置 Bit Array 中的某一位,m 是該位數組的大小,k 是 Hash 函數的個數,那么位數組中某一特定的位在進行元素插入時的 Hash 操作中沒有被置位的概率是:

那么在所有 k 次 Hash 操作后該位都沒有被置 "1" 的概率是:

如果我們插入了 n 個元素,那么某一位仍然為 "0" 的概率是:

因而該位為 "1"的概率是:

現在檢測某一元素是否在該集合中。標明某個元素是否在集合中所需的 k 個位置都按照如上的方法設置為 "1",但是該方法可能會使算法錯誤的認為某一原本不在集合中的元素卻被檢測為在該集合中(False Positives),該概率由以下公式確定:

其實上述結果是在假定由每個 Hash 計算出需要設置的位(bit) 的位置是相互獨立為前提計算出來的,不難看出,隨着 m (位數組大小)的增加,假正例(False Positives)的概率會下降,同時隨着插入元素個數 n 的增加,False Positives的概率又會上升,對於給定的m,n,如何選擇Hash函數個數 k 由以下公式確定:

此時False Positives的概率為:

而對於給定的False Positives概率 p,如何選擇最優的位數組大小 m 呢,

上式表明,位數組的大小最好與插入元素的個數成線性關系,對於給定的 m,n,k,假正例概率最大為:

下圖是布隆過濾器假正例概率 p 與位數組大小 m 和集合中插入元素個數 n 的關系圖,假定 Hash 函數個數選取最優數目:

Bloom Filter 用例
Google 著名的分布式數據庫 Bigtable 使用了布隆過濾器來查找不存在的行或列,以減少磁盤查找的IO次數[3]。
Squid 網頁代理緩存服務器在 cache digests 中使用了也布隆過濾器[4]。
Venti 文檔存儲系統也采用布隆過濾器來檢測先前存儲的數據[5]。
SPIN 模型檢測器也使用布隆過濾器在大規模驗證問題時跟蹤可達狀態空間[6]。
Google Chrome瀏覽器使用了布隆過濾器加速安全瀏覽服務[7]。
在很多Key-Value系統中也使用了布隆過濾器來加快查詢過程,如 Hbase,Accumulo,Leveldb,一般而言,Value 保存在磁盤中,訪問磁盤需要花費大量時間,然而使用布隆過濾器可以快速判斷某個Key對應的Value是否存在,因此可以避免很多不必要的磁盤IO操作,只是引入布隆過濾器會帶來一定的內存消耗,下圖是在Key-Value系統中布隆過濾器的典型使用:

布隆過濾器相關擴展
Counting filters
基本的布隆過濾器不支持刪除(Deletion)操作,但是 Counting filters 提供了一種可以不用重新構建布隆過濾器但卻支持元素刪除操作的方法。在Counting filters中原來的位數組中的每一位由 bit 擴展為 n-bit 計數器,實際上,基本的布隆過濾器可以看作是只有一位的計數器的Counting filters。原來的插入操作也被擴展為把 n-bit 的位計數器加1,查找操作即檢查位數組非零即可,而刪除操作定義為把位數組的相應位減1,但是該方法也有位的算術溢出問題,即某一位在多次刪除操作后可能變成負值,所以位數組大小 m 需要充分大。另外一個問題是Counting filters不具備伸縮性,由於Counting filters不能擴展,所以需要保存的最大的元素個數需要提前知道。否則一旦插入的元素個數超過了位數組的容量,false positive的發生概率將會急劇增加。當然也有人提出了一種基於 D-left Hash 方法實現支持刪除操作的布隆過濾器,同時空間效率也比Counting filters高。
Data synchronization
Byers等人提出了使用布隆過濾器近似數據同步[9]。
Bloomier filters
Chazelle 等人提出了一個通用的布隆過濾器,該布隆過濾器可以將某一值與每個已經插入的元素關聯起來,並實現了一個關聯數組Map[10]。與普通的布隆過濾器一樣,Chazelle實現的布隆過濾器也可以達到較低的空間消耗,但同時也會產生false positive,不過,在Bloomier filter中,某 key 如果不在 map 中,false positive在會返回時會被定義出的。該Map 結構不會返回與 key 相關的在 map 中的錯誤的值。
Compact approximators[11]
Stable Bloom filters[12]
Scalable Bloom filters[13]
Attenuated Bloom filters[14]
Bloom Filter布隆過濾器
http://blog.csdn.net/pipisorry/article/details/64127666
Bloom Filter簡介
Bloom Filter是一種空間效率很高的隨機數據結構,它利用位數組很簡潔地表示一個集合,並能判斷一個元素是否屬於這個集合。布隆過濾器(英語:Bloom Filter)是1970年由布隆提出的。它實際上是一個很長的二進制向量和一系列隨機映射函數。布隆過濾器可以用於檢索一個元素是否在一個集合中。它的優點是空間效率和查詢時間都遠遠超過一般的算法,缺點是有一定的誤識別率和刪除困難。
布隆過濾器的原理是,當一個元素被加入集合時,通過K個散列函數將這個元素映射成一個位數組中的K個點,把它們置為1。檢索時,我們只要看看這些點是不是都是1就(大約)知道集合中有沒有它了:如果這些點有任何一個0,則被檢元素一定不在;如果都是1,則被檢元素很可能在。
Bloom Filter的這種高效是有一定代價的:在判斷一個元素是否屬於某個集合時,有可能會把不屬於這個集合的元素誤認為屬於這個集合(false positive)。因此,Bloom Filter不適合那些“零錯誤”的應用場合。而在能容忍低錯誤率的應用場合下,Bloom Filter通過極少的錯誤換取了存儲空間的極大節省。BF其中的元素越多,false positive rate(誤報率)越大,但是false negative (漏報)是不可能的。
在計算機科學中,我們常常會碰到時間換空間或者空間換時間的情況,即為了達到某一個方面的最優而犧牲另一個方面。Bloom Filter在時間空間這兩個因素之外又引入了另一個因素:錯誤率。在使用Bloom Filter判斷一個元素是否屬於某個集合時,會有一定的錯誤率。也就是說,有可能把不屬於這個集合的元素誤認為屬於這個集合(False Positive),但不會把屬於這個集合的元素誤認為不屬於這個集合(False Negative)。在增加了錯誤率這個因素之后,Bloom Filter通過允許少量的錯誤來節省大量的存儲空間。
Bloom Filter的應用
可以用來實現數據字典,進行數據的判重,或者集合求交集。在垃圾郵件過濾的黑白名單方法、爬蟲(Crawler)的網址判重模塊中等等經常被用到。
Bloom Filter就被廣泛用於拼寫檢查和數據庫系統中。
Bloom Filter在網絡領域獲得了新生,各種Bloom Filter變種和新的應用不斷出現。
集合表示和元素查詢
下面我們具體來看Bloom Filter是如何用位數組表示集合的。初始狀態時,Bloom Filter是一個包含m位的位數組,每一位都置為0。

為了表達S={x1, x2,…,xn}這樣一個n個元素的集合,Bloom Filter使用k個相互獨立的哈希函數(Hash Function),它們分別將集合中的每個元素映射到{1,…,m}的范圍中。對任意一個元素x,第i個哈希函數映射的位置hi(x)就會被置為1(1≤i≤k)。注意,如果一個位置多次被置為1,那么只有第一次會起作用,后面幾次將沒有任何效果。在下圖中,k=3,且有兩個哈希函數選中同一個位置(從左邊數第五位)。

在判斷y是否屬於這個集合時,我們對y應用k次哈希函數,如果所有hi(y)的位置都是1(1≤i≤k),那么我們就認為y是集合中的元素,否則就認為y不是集合中的元素。下圖中y1就不是集合中的元素。y2或者屬於這個集合,或者剛好是一個false positive。

錯誤率f估計
Note: 這個錯誤率f是將不屬於本集合的元素引進來的概率。如果外集有(u-n)個元素,則有(u-n)f個外元素可能被引進來。
假設:布隆過濾器中的hash function滿足simple uniform hashing假設:每個元素都等概率地hash到m個slot中的任何一個,與其它元素被hash到哪個slot無關。
前面我們已經提到了,Bloom Filter在判斷一個元素是否屬於它表示的集合時會有一定的錯誤率(false positive rate),下面我們就來估計錯誤率的大小。在估計之前為了簡化模型,我們假設kn<m且各個哈希函數是完全隨機的。當集合S={x1, x2,…,xn}的所有元素都被k個哈希函數映射到m位的位數組中時,這個位數組中某一位還是0的概率是:

Note: m是bit為單位,而n則是以元素個數為單位(准確的說是不同元素的個數)
其中1/m表示任意一個哈希函數選中這一位的概率(前提是哈希函數是完全隨機的),(1-1/m)表示哈希一次沒有選中這一位的概率。要把S完全映射到位數組中,需要做kn次哈希。某一位還是0意味着kn次哈希都沒有選中它,因此這個概率就是(1-1/m)的kn次方。令p = e-kn/m是為了簡化運算,這里用到了計算e時常用的近似:

令ρ為位數組中0的比例,則ρ的數學期望E(ρ)= p’。在ρ已知的情況下,要求的錯誤率(false positive rate)為:

(1-ρ)為位數組中1的比例,(1-ρ)k就表示k次哈希都剛好選中1的區域,即false positive rate。上式中第二步近似在前面已經提到了,現在來看第一步近似。p’只是ρ的數學期望,在實際中ρ的值有可能偏離它的數學期望值。M. Mitzenmacher已經證明[2] ,位數組中0的比例非常集中地分布在它的數學期望值的附近。因此,第一步的近似得以成立。分別將p和p’代入上式中,得:


相比p’和f’,使用p和f通常在分析中更為方便。
最優的哈希函數個數k
既然Bloom Filter要靠多個哈希函數將集合映射到位數組中,那么應該選擇幾個哈希函數才能使元素查詢時的錯誤率降到最低呢?這里有兩個互斥的理由:如果哈希函數的個數多,那么在對一個不屬於集合的元素進行查詢時得到0的概率就大;但另一方面,如果哈希函數的個數少,那么位數組中的0就多。為了得到最優的哈希函數個數,我們需要根據上一小節中的錯誤率公式進行計算。
先用p和f進行計算。注意到f = exp(k ln(1 − e−kn/m)),我們令g = ln(f),只要讓g取到最小,f自然也取到最小。由於p = e-kn/m,我們可以將g寫成

根據對稱性法則可以很容易看出當p = 1/2,也就是k = ln2· (m/n)時,g取得最小值。在這種情況下,最小錯誤率f等於(1/2)^k = ((1/2)^ln2) ^(m/n)≈ (0.6185)^(m/n)。另外,注意到p是位數組中某一位仍是0的概率,所以p = 1/2對應着位數組中0和1各一半。換句話說,要想保持錯誤率低,最好讓位數組有一半還空着。
需要強調的一點是,p = 1/2時錯誤率最小這個結果並不依賴於近似值p和f。同樣對於f’ = exp(k ln(1 − (1 − 1/m)kn)),g’ = k ln(1 − (1 − 1/m)kn),p’ = (1 − 1/m)kn,我們可以將g’寫成

同樣根據對稱性法則可以得到當p’ = 1/2時,g’取得最小值。
位數組的大小m
下面我們來看看,在不超過一定錯誤率的情況下,Bloom Filter至少需要多少位才能表示全集中任意n個元素的集合。
{表示任意n個元素的集合的最小m數的推導
假設全集中共有u個元素,允許的最大錯誤率為є,下面我們來求位數組的位數m。
假設X為全集中任取n個元素的集合,F(X)是表示X的位數組。那么對於集合X中任意一個元素x,在s = F(X)中查詢x都能得到肯定的結果,即s能夠接受x。顯然,由於Bloom Filter引入了錯誤,s能夠接受的不僅僅是X中的元素,它還能夠接受є (u - n)個false positive。因此,對於一個確定的位數組來說,它能夠接受總共n + є (u - n)個元素。在n + є (u - n)個元素中,s真正表示的只有其中n個,所以一個確定的位數組可以表示

個集合。m位的位數組共有2m個不同的組合,進而可以推出,m位的位數組可以表示

個集合。全集中n個元素的集合總共有

個,因此要讓m位的位數組能夠表示所有n個元素的集合,必須有

即:

上式中的近似前提是n和єu相比很小,這也是實際情況中常常發生的。
根據上式,我們得出結論:在錯誤率不大於є的情況下,m至少要等於n log2(1/є)才能表示任意n個元素的集合。
}
上一小節中我們曾算出當k = ln2· (m/n)時錯誤率f最小,這時f = (1/2)^k = (1/2)^((mln2) / n)。現在令f≤є,可以推出

這個結果比前面我們算得的下界n log2(1/є)大了log2 e ≈ 1.44倍。這說明在哈希函數的個數取到最優時,要讓錯誤率不超過є,m至少需要取到最小值的1.44倍。
總結來說就是,當hash函數個數k=(ln2)*(m/n)時錯誤率最小。在錯誤率不大於є的情況下,m至少要等於n*lg(1/є)才能表示任意n個元素的集合。但m還應該更大些,因為還要保證bit數組里至少一半為0,則m應該>=nlg(1/є)*lge,即m/n = -lnp/(ln2)^2 大概就是nlg(1/E)1.44倍(lg表示以2為底的對數)。
舉個例子
我們假設錯誤率為0.01,則此時m應大概是n的9.6倍。這樣k大概是7個{6.64個}。 (m應大概是n的13倍。這樣k大概是8個。這個計算是錯的應該)。
對於一個有1%誤報率和一個最優k值的布隆過濾器來說,無論元素的類型及大小,每個元素只需要9.6 bits(m/n=9.6)來存儲。這個優點一部分繼承自array的緊湊性,一部分來源於它的概率性。如果你認為1%的誤報率太高,那么對每個元素每增加4.8 bits {m/n = -ln(p/10)/(ln2)^2 = -lnp/(ln2)^2)+ln(10)/(ln2)^2 = -lnp/(ln2)^2)+4.8},我們就可將誤報率降低為原來的1/10。[布隆過濾器 (Bloom Filter) 詳解]
[推導參考wikipedia Bloom filter]
