拜託,面試官別問我「布隆」了(補充修訂版)

程式設計師吳師兄發表於2019-03-29

前言

在之前的 拜託,面試官別問我「布隆」了 一文中,很多小夥伴留言說並不能看出布隆過濾器有比點陣圖更方便,今天的文章就補充更詳細一點。

題目描述

一個網站有 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。

圖 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,以及布隆過濾器的基本原理,最後你再談談它的使用場景那就更好了。

相關文章