前言
在之前的 拜託,面試官別問我「布隆」了 一文中,很多小夥伴留言說並不能看出布隆過濾器有比點陣圖更方便,今天的文章就補充更詳細一點。
題目描述
一個網站有 100 億 url 存在一個黑名單中,每條 url 平均 64 位元組。這個黑名單要怎麼存?若此時隨便輸入一個 url,你如何快速判斷該 url 是否在這個黑名單中?
題目解析
這是一道經常在面試中出現的演算法題。憑藉著題目極其容易描述,電面的時候也出現過。
不考慮細節的話,此題就是一個簡單的查詢問題。對於查詢問題而言,使用雜湊表來處理往往是一種效率比較高的方案。
但是,如果你在面試中回答使用雜湊表,接下來面試官肯定會問你:然後呢?如果你不能回答個所以然,面試官就會面無表情的通知你:今天的面試到此結束,我們會在一週內給你答覆。
為什麼不能用雜湊表
100 億是一個很大的數量級,這裡每條 url 平均 64 位元組,全部儲存的話需要 640G 的記憶體空間。又因為使用了雜湊表這種資料結構,而雜湊表是會出現雜湊衝突的。為了讓雜湊表維持較小的裝載因子,避免出現過多的雜湊衝突,需要使用連結串列法來處理,這裡就要儲存連結串列指標。因此最後的記憶體空間可能超過 1000G 了。
只是儲存個 url 就需要 1000G 的空間,老闆肯定不能忍!
點陣圖(BitMap)
這個時候就需要擴充一下思路。首先,先來考慮一個類似但更簡單的問題:現在有一個非常龐大的資料,比如有 1 千萬個整數,並且整數的範圍在 1 到 1 億之間。那麼如何快速查詢某個整數是否在這 1 千萬個整數中呢?
需要判斷該數是否存在,也就是說這個數存在兩種狀態:存在( True )或者不存在(False)。
因此這裡可以使用一個儲存了狀態的陣列來處理。這個陣列特點是大小為 1 億,並且資料型別為布林型別( True 或者 False )。然後將這 1 千萬個整數作為陣列下標,將對應的陣列值設定成 True,比如,整數 233 對應下標為 233 的陣列值設定為 True,也就是 array[ 233 ] = True。
這種操作就是點陣圖法:就是用每一位來存放某種狀態,適用於大規模資料,但資料狀態又不是很多的情況。
另外,點陣圖法有一個優勢就是空間不隨集合內元素個數的增加而增加。它的儲存空間計算方式是找到所有元素裡面最大的元素(假設為 N ),因此所佔空間為:
因此,當 N 為 1 億的時候需要 12MB 的儲存空間。當 N 為 10 億的時候需要 120MB 的儲存空間了。當 N 的數量大到一定量級的時候,比如 N 為 2^64 這個海量級別的時候,需要消耗 2048PB 的儲存空間,這個量級的BitMap,目前硬體上是支援不了的。
也就是說:點陣圖法的所佔空間隨集合內最大元素的增大而增大。這就會帶來一個問題,如果查詢的元素數量少但其中某個元素的值很大,比如數字範圍是 1 到 1000 億,那消耗的空間不容樂觀。
這個就是點陣圖的一個不容忽視的缺點:空間複雜度隨集合內最大元素增大而線性增大。對於開頭的題目而言,使用點陣圖進行處理,實際上記憶體消耗也是不少的。
因此,出於效能和記憶體佔用的考慮,在這裡使用布隆過濾器才是最好的解決方案:布隆過濾器是對點陣圖的一種改進。
布隆過濾器
布隆過濾器(英語:Bloom Filter)是 1970 年由 Burton Bloom 提出的。
它實際上是一個很長的二進位制向量和一系列隨機對映函式。
複製程式碼
它可以用來判斷一個元素是否在一個集合中。它的優勢是隻需要佔用很小的記憶體空間以及有著高效的查詢效率。
對於布隆過濾器而言,它的本質是一個位陣列:位陣列就是陣列的每個元素都只佔用 1 bit ,並且每個元素只能是 0 或者 1。
一開始,布隆過濾器的位陣列所有位都初始化為 0。比如,陣列長度為 m ,那麼將長度為 m 個位陣列的所有的位都初始化為 0。
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
---|---|---|---|---|---|---|---|---|---|
0 | 0 | 1 | 。 | 。 | 。 | 。 | 。 | m-2 | m-1 |
在陣列中的每一位都是二進位制位。
布隆過濾器除了一個位陣列,還有 K 個雜湊函式。當一個元素加入布隆過濾器中的時候,會進行如下操作:
- 使用 K 個雜湊函式對元素值進行 K 次計算,得到 K 個雜湊值。
- 根據得到的雜湊值,在位陣列中把對應下標的值置為 1。
舉個例子,假設布隆過濾器有 3 個雜湊函式:f1, f2, f3 和一個位陣列 arr
。現在要把 2333
插入布隆過濾器中:
- 對值進行三次雜湊計算,得到三個值 n1, n2, n3。
- 把位陣列中三個元素 arr[n1], arr[n2], arr[3] 都置為 1。
當要判斷一個值是否在布隆過濾器中,對元素進行三次雜湊計算,得到值之後判斷位陣列中的每個元素是否都為 1,如果值都為 1,那麼說明這個值在布隆過濾器中,如果存在一個值不為 1,說明該元素不在布隆過濾器中。
很明顯,陣列的容量即使再大,也是有限的。那麼隨著元素的增加,插入的元素就會越多,位陣列中被置為 1 的位置因此也越多,這就會造成一種情況:當一個不在布隆過濾器中的元素,經過同樣規則的雜湊計算之後,得到的值在位陣列中查詢,有可能這些位置因為之前其它元素的操作先被置為 1 了。
如圖 1 所示,假設某個元素通過對映對應下標為4,5,6這3個點。雖然這 3 個點都為 1 ,但是很明顯這 3 個點是不同元素經過雜湊得到的位置,因此這種情況說明這個元素雖然不在集合中,也可能對應的都是 1,這是誤判率存在的原因。
所以,有可能一個不存在布隆過濾器中的會被誤判成在布隆過濾器中。
這就是布隆過濾器的一個缺陷:存在誤判。
但是,如果布隆過濾器判斷某個元素不在布隆過濾器中,那麼這個值就一定不在布隆過濾器中。總結就是:
- 布隆過濾器說某個元素在,可能會被誤判
- 布隆過濾器說某個元素不在,那麼一定不在
用英文說就是:False is always false. True is maybe true。
誤判率
布隆過濾器可以插入元素,但不可以刪除已有元素。其中的元素越多,false positive rate(誤報率)越大,但是false negative (漏報)是不可能的。由於公眾號內對於數學公式的排版不太友好,小吳就不在這貼出來了,具體的計算公式可以在網上查詢到。
補救方法
布隆過濾器存在一定的誤識別率。常見的補救辦法是在建立白名單,儲存那些可能被誤判的元素。 比如你苦等的offer 可能被系統丟在郵件垃圾箱(白名單)了。
使用場景
布隆過濾器的最大的用處就是,能夠迅速判斷一個元素是否在一個集合中。因此它有如下三個使用場景:
- 網頁爬蟲對 URL 的去重,避免爬取相同的 URL 地址
- 進行垃圾郵件過濾:反垃圾郵件,從數十億個垃圾郵件列表中判斷某郵箱是否垃圾郵箱(同理,垃圾簡訊)
- 有的黑客為了讓服務當機,他們會構建大量不存在於快取中的 key 向伺服器發起請求,在資料量足夠大的情況下,頻繁的資料庫查詢可能導致 DB 掛掉。布隆過濾器很好的解決了快取擊穿的問題。
回到問題
回到一開始的問題,如果面試官問你如何在海量資料中快速判斷該 url 是否在黑名單中時,你應該回答使用布隆過濾器進行處理,然後說明一下為什麼不使用 hash 和 bitmap,以及布隆過濾器的基本原理,最後你再談談它的使用場景那就更好了。