數據結構-05| BitMap位圖 |布隆過濾器


 

位圖 BitMap

存儲結構,位圖(BitMap)。布隆過濾器本身就是基於位圖的,是對位圖的一種改進。

有 1 千萬個整數,整數的范 圍在 1 到 1 億之間。如何快速查找某個整數是否在這 1 千萬個整數中?

當然,這個問題還是可以用散列表來解決。不過,我們可以使用一種比較“特殊”的散列表,那就是位圖。我們申請一個大小為 1 億、數據類型為布爾類型(true 或者 false)的數組。將這 1 千萬個整數作為數組

下標,將對應的數組值設置成 true。比如,整數 5 對應下標為 5 的數組值設置為 true,也就是 array[5]=true。

當查詢某個整數 K 是否在這 1 千萬個整數中時,只需要將對應的數組值 array[K] 取出來,看是否等於 true。如果等於 true,那說明 1 千萬整數中包含這個整數 K;相反,就表示不包含這個整數 K。

不過很多語言中提供的布爾類型,大小是 1 個字節,並不能節省太多內存空間。實際上,表示 true 和 false 兩個值,只需用一個二進制位(bit)就可以了。那如何通過編程語言,來表示一個二進制位呢?

這里就要用到位運算了。我們可以借助編程語言中提供的數據類型,比如 int、long、char 等類型,通過位運算,用其中的某個位表示某個數字。位圖的代碼如下:

public class BitMap { // System.out.println(Character.SIZE); //結果16 Java中char類型占16bit,也即是2個字節 private char[] bytes; private int nbits; public BitMap(int nbits) { this.nbits = nbits; this.bytes = new char[nbits / 16 + 1]; } public void set(int k) { if (k > nbits) return; int byteIndex = k / 16; int bitIndex = k % 16; bytes[byteIndex] |= (1 << bitIndex); } public boolean get(int k) { if (k > nbits) return false; int byteIndex = k / 16; int bitIndex = k % 16; return (bytes[byteIndex] & (1 << bitIndex)) != 0; } }

 

位圖通過數組下標來定位數據,所以,訪問效率非常高。而且,每個數字用一個二進制位來表示,在數字范圍不大的情況下,所需要的內存空間非常節省。

這里有個假設,就是數字所在的范圍不是很大。如果數字的范圍很大,數字范圍不是 1 到 1 億,而是 1 到 10 億,那位圖的大小就是 10 億個二進制位,也就是120MB 的大小,消耗的內存空間,不降反增。

這時布隆過濾器就要出場了。布隆過濾器就是為了解決剛剛這個問題,對位圖這種數據結構的一種改進。

布隆過濾器 Bloom Filter

還是那個例子,數據個數是 1 千萬,數據的范圍是 1 到 10 億。布隆過濾器的做法是,仍然使用一個 1 億個二進制大小的位圖,然后通過哈希函數,對數字進行處理,讓它落在這 1 到 1 億范圍內。比如我們

把哈希函數設計成 f(x)=x%n。其中,x 表示數字,n 表示位圖的大小 (1 億),也就是,對數字跟位圖的大小進行取模求余。

哈希函數會存在沖突的問題啊,一億零一和 1 兩個數字,經過剛剛那個取模求余的哈希函數處理之后,最后的結果都是 1。這樣就無法區分,位圖存儲的是 1 還是一億零一了。

為了降低這種沖突概率,可以設計一個復雜點、隨機點的哈希函數。除此之外,還有其他方法嗎?參看布隆過濾器的處理方法。既然一個哈希函數可能會存在沖突,那用多個哈希函數一塊兒定位一個數據,是否能降低沖突的概率呢?布隆過濾器是怎么做的。

我們使用 K 個哈希函數,對同一個數字進行求哈希值,會得到 K 個不同的哈希值,分別記作 $X_{1}$,$X_{2}$,$X_{3}$,…,$X_{K}$。把這 K 個數字作為位圖中的下標,將對應的

BitMap[$X_{1}$],BitMap[$X_{2}$],BitMap[$X_{3}$],…,BitMap[$X_{K}$] 都設置成 true,也就是說,用 K 個二進制位,來表示一個數字的存在。

當要查詢某個數字是否存在的時候,用同樣的 K 個哈希函數,對這個數字求哈希值, 分別得到 $Y_{1}$,$Y_{2}$,$Y_{3}$,…,$Y_{K}$。我們看這 K 個哈希值,對應位圖中的數 值是否都為 true,

如果都是 true,則說明,這個數字存在,如果有其中任意一個不為 true,就說明這個數字不存在。

  

 

 

 對於兩個不同的數字來說,經過一個哈希函數處理之后,可能會產生相同的哈希值。但是經過 K 個哈希函數處理之后,K 個哈希值都相同的概率就非常低了。盡管采用 K 個哈希函數之后,兩個數字哈希沖突的

概率降低了,但這種處理方式又帶來了新的問題,那就是容易誤判。

布隆過濾器的誤判有一個特點,那就是,它只會對存在的情況有誤判。如果某個數字經過布隆過濾器判斷不存在,那說明這個數字真的不存在,不會發生誤判;

如果某個數字經過布隆過濾器判斷存在,這時才會有可能誤判,有可能並不存在。

不過,只要我們調整哈希函數的個數、位圖大小跟要存儲數字的個數之間的比例,那就可以將這種誤判的概率降到非常低。

盡管布隆過濾器會存在誤判,但是,這並不影響它發揮大作用。很多場景對誤判有一定的容忍度。比如爬蟲判重這個問題,即便一個沒有被爬取過的網頁,被誤判為已經被爬取,對於搜索引擎來說,也並不是什

么大事情,是可以容忍的,畢竟網頁太多了,搜索引擎也 不可能 100% 都爬取到。

 

布隆過濾器和哈希表類似,HashTable + 拉鏈表存儲重復元素:

元素  ---哈希函數---> 映射到一個整數的下標位置index。比如Join Smith和Sandra Dee經過哈希函數都映射到了152的下標,就在152的位置開一個鏈表,把多個元素都存在相同位置的鏈表處,往后邊不斷的積累積累。

  它不僅有哈希函數得到一個index值,且會把整個要素的元素都存儲到哈希表里邊去,這是一個沒有誤差的數據結構,有多少個元素,每個元素有多大,所有這些元素所占的內存空間等在哈希表里邊都要有相應的內存大小給存儲起來。

    

 

 但是在很多工業級應用中,我們並不需要存儲所有的元素本身,而只需要存儲一個信息,即這個元素在這個表里邊有沒有,這時就需要更高效的一種數據結構。

 

Bloom Filter VS HashTable

布隆過濾器即 一個很長的二進制向量和一系列隨機映射函數。布隆過濾器可以用於檢索一個元素是否存在一個集合中。(而哈希表不只是判斷元素是否在一個集合中,同時還可以存元素本身和元素的各種額外信息。布隆過濾器只用於檢索一個元素它是否在還是不在的信息,而不能存其它的額外信息)

優點:空間效率和查詢時間都遠遠超過一般的算法,

缺點是有一定的誤識別率和刪除困難。

布隆過濾器示意圖:

  

 

 

x,y,z不是同時往里邊添加,一個一個的添加; 每一個元素它會分配到一系列的二進制位中。
假設x會分配3個二進制位,藍色線表示, 把x插入布隆過濾器時即把x對應的這三個位置置為1就可以了。

y插入進來,根據它的哈希函數分為紅色的這三條線所對應的二進制位,置為1。
同理z也置為1。
這個二進制的數組用來表示所有的已經存入的xyz是否已經在索引里邊了。
重新插入一個x, 這時x始終會對應這3個藍色的二進制位, 去表里查就查到這三個都是1, 所以我們認為x是存在的。
如果是一個陌生的元素w進來, 它把它分配給通過布隆過濾器的二進制索引的函數, w就得到灰色的這三個二進制位110, 有一個為0說明w未在索引里邊。


只要布隆過濾器中有一個為0就說明這個元素不在布隆過濾器的索引里面, 且我們肯定它不在。
但比如又來一個元素q, 它剛好分配的三個二進制都為1, 我們不一定說它是存在的。 

                          

 

 存儲元素A和E都存入布隆過濾器中,置為1.

測試元素,A查到到它的二進制位為1,可能是有的;C查到它的二進制位有一個為0,則C肯定不在布隆過濾器里邊;B恰好分配的二進制位都為1,但從來沒有存儲過B,則我們會判斷B在這個索引里邊,這個時候對於B的判斷就是有誤差的。

結論: 當布隆過濾器把元素全部插入完之后, 對於測試(新來)的元素要驗證它是否存在 當它驗證這個元素所對應的二進制位是1時, 我們只能說它可能存在布隆過濾器里邊。 但是當這個元素所對應的二進制位只要有一個不為1, 則它肯定不在。 ---一個元素去布隆過濾器里邊查, 如果查到它不存在, 那么它肯定就是不存在的。 如果查到它的二進制都是1,為存在的狀態比如B,則它可能存在。 那我們到底怎么判斷B這種元素是否存在呢? 布隆過濾器只是放在外邊當一個緩存使用,來作為一個很快速的判斷來使用 當B查到了,布隆過濾器里邊是存在的,則B會繼續在這個機器上的數據庫DB中來查詢,去查詢B是否存在。 而C就會直接打到布隆過濾器里邊,這樣C就不用查詢了節省了查詢訪問數據庫的時間。 布隆過濾器只是擋在一台機器前邊的快速查詢的緩存。

 

 

案例
①.比特幣網絡
②.分布式系統(Map-Reduce) -- Hadoop、 Searchengine
③.Redis緩存
④.垃圾郵件、評論等的過濾

https://www.cnblogs.com/cpselvis/p/6265825.html

https://blog.csdn.net/tianyaleixiaowu/article/details/74721877

 

布隆過濾器的Java代碼實現:

https://github.com/lovasoa/bloomfilter/blob/master/src/main/java/BloomFilter.java

https://github.com/Baqend/Orestes-Bloomfilter

 

應用

網頁爬蟲是搜索引擎中的非常重要的系統,負責爬取幾十億、上百億的網頁。爬蟲的工作原理 是,通過解析已經爬取頁面中的網頁鏈接,然后再爬取這些鏈接對應的網頁。而同一個網頁鏈接 有可能被包含在多

個頁面中,這就會導致爬蟲在爬取的過程中,重復爬取相同的網頁。如何避免這些重復的爬取 ?

最容易想到的方法就是,記錄已經爬取的網頁鏈接(也就是 URL),在爬取一個新的網頁 之前,拿它的鏈接,在已經爬取的網頁鏈接列表中搜索。

如果存在,那就說明這個網頁已經被爬取過了;如果不存在,那就說明這個網頁還沒有被爬取過,可以繼續去爬取。等爬取到這個 網頁之后,我們將這個網頁的鏈接添加到已經爬取的網頁鏈接列表了。

思路非常簡單,該如何記錄已經爬取的網頁鏈接呢?要用什么樣的數據結構

這個問題要處理的對象是網頁鏈接,也就是 URL,需要支持的操作有兩個,添加一個 URL 和查詢一個 URL。除了這兩個功能性的要求之外,在非功能性方面,還要求這兩個操作的執行 效率要盡可能高。除此之外,因為處理的是上億的網頁鏈接,內存消耗會非常大,所以在存儲效率上,要盡可能地高效。

滿足這些條件的數據結構有散列表、紅黑樹、跳表這些動態數據 結構,都能支持快速地插入、查找數據,但是對內存消耗方面,是否可以接受呢?

以散列表為例:

假設我們要爬取 10 億個網頁(像 Google、百度這樣的通用搜索引擎, 爬取的網頁可能會更多),為了判重,把這 10 億網頁鏈接存儲在散列表中。估算下大約需要的內存:

假設一個 URL 的平均長度是 64 字節,那單純存儲這 10 億個 URL,需要大約 60GB 的內存空 間。因為散列表必須維持較小的裝載因子,才能保證不會出現過多的散列沖突,導致操作的性能 下降。而且,用鏈

表法解決沖突的散列表,還會存儲鏈表指針。所以,如果將這 10 億個 URL 構建成散列表,那需要的內存空間會遠大於 60GB,有可能會超過 100GB。

當然,對於一個大型的搜索引擎來說,即便是 100GB 的內存要求,其實也不算太高,我們可以采用分治的思想,用多台機器(比如 20 台內存是 8GB 的機器)來存儲這 10 億網頁鏈接。

對於爬蟲的 URL去重這個問題,剛剛講到的分治加散列表的思路,已經是可以實實在在工作的 了。

不過,作為一個有追求的工程師,我們應該考慮,在添加、查詢數據的效率以及內存消耗方 面,我們是否還有進一步的優化空間呢?

散列表中添加、查找數據的時間復雜度已經是 O(1),還能有進一步優化的空間嗎?實際上,時間復雜度並不能完全代表代碼的執行時間。大 O 時間復雜度表示法,會忽略掉常數、系數和低階,並且統計的對象

是語句的頻度。不同的語句,執行時間也 是不同的。時間復雜度只是表示執行時間隨數據規模的變化趨勢,並不能度量在特定的數據規模 下,代碼執行時間的多少。

如果時間復雜度中原來的系數是 10,能夠通過優化,將系數降為 1,那在時間復雜度沒有變化的情況下,執行效率就提高了 10 倍。對於實際的軟件開發來說,10 倍效率的提升, 顯然是一個非常值得的優化。

如果我們用基於鏈表的方法解決沖突問題,散列表中存儲的是 URL,那當查詢的時候,通過哈希函數定位到某個鏈表之后,還需要依次比對每個鏈表中的 URL。這個操作比較耗時,主要有兩點原因。

一方面,鏈表中的結點在內存中不是連續存儲的,所以不能一下子加載到 CPU 緩存中,沒法很好地利用到 CPU 高速緩存,所以數據訪問性能方面會打折扣

另一方面,鏈表中的每個數據都是 URL,而 URL 不是簡單的數字,是平均長度為 64 字節的字 符串。也就是說,我們要讓待判重的 URL,跟鏈表中的每個 URL,做字符串匹配。顯然,這樣 一個字符串匹配操

作,比起單純的數字比對,要慢很多。所以,基於這兩點,執行效率方面肯定 是有優化空間的。

對於內存消耗方面的優化,除了剛剛這種基於散列表的解決方案。實際 上,如果要想內存方面有明顯的節省,那就得換一種解決方案,使用布隆過濾器(Bloom Filter)這種數據結構。

如果用散列表存儲這 1 千萬的數據,數據是 32 位的整型數,也就是需要 4 個字節的存儲空間,那總共至少需要 40MB 的存儲空間。如果通過位圖的話,數字范圍在 1 到 1 億之間,只

需要 1 億個二進制位,也就是 12MB 左右的存儲空間就夠了。

布隆過濾器

我們用布隆過濾器來記錄已經爬取過的網頁鏈接,假設需要判重的網頁有 10 億,那我們可以用 一個 10 倍大小的位圖來存儲,也就是 100 億個二進制位,換算成字節,那就是大約 1.2GB。 之前用散列表判重,

需要至少 100GB 的空間。相比來講,布隆過濾器在存儲空間的消耗上,降低了非常多。

利用布隆過濾器,在執行效率方面,是否比散列表更加高效呢?

布隆過濾器用多個哈希函數對同一個網頁鏈接進行處理,CPU 只需要將網頁鏈接從內存中讀取 一次,進行多次哈希計算,理論上講這組操作是 CPU 密集型的。而在散列表的處理方式中,需要讀取散列沖突拉

鏈的多個網頁鏈接,分別跟待判重的網頁鏈接,進行字符串匹配。這個操作涉 及很多內存數據的讀取,所以是內存密集型的。我們知道 CPU 計算可能是要比內存訪問更快速 的,所以,理論上講,布隆過濾器的

判重方式,更加快速。

總結

關於搜索引擎爬蟲網頁去重問題的解決,從散列表講到位圖,再到布隆過濾器。 隆過濾器非常適合這種不需要 100% 准確的、允許存在小概率誤判的大規模判重場景。

除了爬蟲網頁去重這個例子,還有比如統計一個大型網站的每天的 UV 數,也就是每天有多少用戶訪問了網站,我們就可以使用布隆過濾器,對重復訪問的用戶,進行去重。

布隆過濾器的誤判率,主要跟哈希函數的個數、位圖的大小有關。當我們往布隆 過濾器中不停地加入數據之后,位圖中不是 true 的位置就越來越少了,誤判率就越來越高了。 所以,對於無法事先知道要判重的

數據個數的情況,我們需要支持自動擴容的功能。

當布隆過濾器中,數據個數與位圖大小的比例超過某個閾值的時候,就重新申請一個新的位圖。后面來的新數據,會被放置到新的位圖中。但是,如果我們要判斷某個數據是否在布隆過濾 器中已經存在,我們

就需要查看多個位圖,相應的執行效率就降低了一些。

位圖、布隆過濾器應用如此廣泛,很多編程語言都已經實現了。比如 Java 中的 BitSet 類就是一 個位圖,Redis 也提供了 BitMap 位圖類,Google 的 Guava 工具包提供了 BloomFilter 布隆 過濾器的實現。

 bloom filter: False is always false. True is maybe true.
 

1. 假設我們有 1 億個整數,數據范圍是從 1 到 10 億,如何快速並且省內存地給這 1 億個數 據從小到大排序?

傳統的做法:1億個整數,存儲需要400M空間,排序時間復雜度最優 N×log(N)

使用位圖算法:數字范圍是1到10億,用位圖存儲120M(10億 / 8 / 1024 / 1024)就夠了,然后將1億個數字依次添加到位圖中,然后再將位圖按下標從小到大輸出值為1的下標,排序就完成了,時間復雜度為 N

   要把這一億個整數排序,最簡單的做法,把這1億個整數存到位圖中,位圖大小是10億bit,約120MB,位圖中位的順序即為整數的順序。

數字重復了, 對於重復的可以再維護一個小的散列表記錄出現次數超過1次的數據以及對應的個數。

Bloom filter刪除數據時,不能把bit位置0,解決方案, 一般不用來刪除,如果非要支持刪除,可以再弄個數據結構記錄刪除的數據。

 

2. 還記得我們在哈希函數(下)的利用分治思想,用散列表以及哈希函數,實現海量圖庫中的判重功能嗎?如果我們允許小概率的誤判,那是否可以用今天的布隆過濾器來解決呢? 參照當時的估算方法,重新

估算下,用布隆過濾器需要多少台機器?

  如果采用布隆過濾器,可以用10億bit位存儲1億圖片的信息(包括圖片唯一標識和圖片文件路徑長),10億bit約為120MB, 如果單機的內存容量上限為2GB,那么只需要1台機器就可以存貯。

 

 

    

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM