這個Kata其實幾天前就完成了,不過最近一直在忙留學申請的事,沒顧上寫文章。
這次的內容就一個:實現(xiàn)一個布隆過濾器。
布隆過濾器(Bloom Filter)
什么是布隆過濾器呢?簡單來說,布隆過濾器可以告訴你一個元素是否在一個集合中。
有同學可能就會說了,這很簡單啊,集合中本來就有所有的元素,我只要檢查輸入內容是否在里面不就行了。
沒錯,這種方法成功率最高(100%),但是效率最低。
如果只是處理十個字符串、一百個字符串,這都沒問題,但是如果要處理成百上千萬的字符串,顯然是不可行的,空間和時間都無法接受。
這時候該主角出場了——布隆過濾器。
布隆過濾器的核心思想就是用盡可能少的空間來存儲盡可能多的內容,同時盡量保證準確率。布隆過濾器本質上很像哈希表,把一個大的空間映射到一個小的空間,那么問題就來了,如果兩個不同元素映射到了同一個位置怎么辦呢?哈希表有各種算法可以解決沖突,但是布隆過濾器非常簡單粗暴——我才不管你,映射到同一個位置那就存同一個位置。
因此,布隆過濾器的準確率并不是100%。準確地說,布隆過濾器具有假陽性,也就是說如果它告訴你一個元素在集合中,那這個結果可能是錯誤的,因為有可能這個元素和集合中另一個不同的元素映射到了同一位置。但是反過來,如果它告訴你一個元素不在集合中,那這個元素就肯定不在集合中。所以布隆過濾器在某些場景下是非常合適的。
為了進一步壓縮空間同時提高準確率,布隆過濾器有很多種改進方法,其中比較常用的是把一個元素映射到結果集中的多個位(bit)。舉例來說,假設每個元素會被映射到5個點,那么兩個元素只要有一個點不相同就可以判斷出來;但是如果只映射到一個點,那么這個點只要相同就無法準確判斷了,因此映射到多個點可以增加準確度。此外,這個方法還可以壓縮空間,因為映射一個點的時候,為了減少碰撞我們必須使用很大的空間,但是映射多個點的時候,多個點全部相同的概率很小,所以我們就可以使用比較小的空間。
代碼實現(xiàn)
我用Python實現(xiàn)了一個簡易的布隆過濾器,代碼如下
import hashlib
import struct
import math
def readWords(filename):
result = []
with open(filename) as f:
for i in f.readlines():
result.append(i.strip())
return result
def getNBits(hashedWord, mapLen, bitNumber):
binaryHash = "".join(["0" * (4 - len(bin(int(i, 16))[2:])) + bin(int(i, 16))[2:] for i in hashedWord])
wordLen = len(binaryHash)
if bitNumber > wordLen:
bitNumber = wordLen
gap = min(wordLen / bitNumber, mapLen)
return [int(binaryHash[i * gap: (i + 1) * gap], 2) for i in range(bitNumber)]
def buildBloomFilter(all_words, mapLen, bitNumber):
bitmap = [0 for i in range(2**mapLen)]
def assignBitMap(i):
bitmap[i] = 1
for i in all_words:
map(assignBitMap, getNBits(hashlib.md5(i).hexdigest(), mapLen, bitNumber))
return bitmap
def lookUpBitMap(words, bitmap, mapLen, bitNumber):
for i in getNBits(hashlib.md5(words).hexdigest(), mapLen, bitNumber):
if bitmap[i] == 0:
return False
return True
mapLen = 20
bitNumber = 4
bitmap = buildBloomFilter(readWords("/usr/share/dict/words"), mapLen, bitNumber)
print lookUpBitMap("asdasd", bitmap, mapLen, bitNumber) # false
print lookUpBitMap("chinanb", bitmap, mapLen, bitNumber) # false
說實話用Python進行位操作確實不太方便。。。
代碼倒數(shù)四五行的mapLen
和bitNumber
這兩個參數(shù)是核心,mapLen
是最終存儲空間的長度(位),bitNumber
是映射點的數(shù)量。代碼的思路很簡單,先把字符串用md5編碼,然后轉換成二進制并取bitNumber
段二進制數(shù),最后把bitmap
中這幾段二進制數(shù)對應的十進制下標設置為1。檢測的時候,先計算出bitNumber
段二進制數(shù),然后檢查bitmap
,如果這幾個位置全是1說明(可能)在集合中,只要有一個不是1就說明肯定不在集合中。
壓縮率計算
通過代碼大家也可以看出,布隆過濾器的思路其實很簡單,關鍵是參數(shù)調優(yōu),這里我們是手工選擇了幾個參數(shù)進行測試,這方面肯定有很多相關論文和算法,大家感興趣的話可以去搜一下。
在我們的代碼中,mapLen
是20,也就是說存儲空間是2的20次方位,等于2的17次方字節(jié),也就是128KB。而/usr/share/dict/words
這個文本文件一共2.4MB,也就是2400KB,壓縮率達到95%!!!
不過這里還是提一下,我們的參數(shù)只是手工調整出來的,也沒有嚴格地測量準確率,所以只是一個參考。可能實際使用中壓縮率沒有這么高,也可能比這個還要高,我們只是進行一些定性的測試。
總之,布隆過濾器實在是太強了,在特定場景下可以起到極大的作用!