海量数据处理(一) :位图与布隆过滤器的概念以及实现
目錄
- 位圖
- 位圖概念
- 位圖的應用
- 位圖的實現思路
- set
- reset
- test
- 完整代碼
- 布隆過濾器
- 布隆過濾器概念
- 布隆過濾器的優缺點
- 優點
- 缺點
- 布隆過濾器的實現思路
- 哈希沖突的問題
- 如何選擇哈希函數個數和布隆過濾器長度
- 插入
- 查找
- 刪除
- 完整代碼
位圖
位圖概念
位圖其實就是哈希的變形,他同樣通過映射來處理數據,只不過位圖本身并不存儲數據,而是存儲標記。通過一個比特位來標記這個數據是否存在,1代表存在,0代表不存在。
位圖通常情況下用在數據量龐大,且數據不重復的情景下判斷某個數據是否存在。
例如下面這道十分經典的題目
給40億個不重復的無符號整數,沒排過序。給一個無符號整數,如何快速判斷一個數是否在這40億個數
中。
關于這道題目,解法其實有很多。
1.快速排序后二分搜索。(內存可能不夠,要16G內存)
2.位圖處理,(40億無符號整數用位圖標記只需要512M的內存)
位圖的解法差不多是這道題的最優解,只需要將所有數據讀入后將對應位置置1,然后再查找那個數據所儲的位置是否為1即可。
位圖的應用
位圖的實現思路
為了方便實現,位圖的底層可以使用一個vector。而開空間并不根據數據的個數來開,而是根據數據的范圍來開(如果開的空間不夠,可能有位置無法映射到)。并且一個整型具有32個字節,所以如果我們要存N個數據,就只需要開N / 32 + 1的空間即可(+1是為了防止數據小于32和向上取整)。
當要操作一個數據時,先將其除以32來判斷它應該處于數組中哪一個整型中。再對其%32,來判斷它位于這個整型中的哪一個位上,此時再進行對應的位運算即可。
set
set即將對應標識位置1
可以通過將1左移pos個位置,再讓對應位置與這個數據相或即可實現。
reset
reset即將對應標識位置0
首先讓1左移pos個位置,再對這個數據進行取反。然后讓對應位置數據與這個數據相與即可。
//數據的對應標識位置0 void reset(size_t x) {size_t index = x >> 5;size_t pos = x % 32;//對應位置數據置零_bits[index] &= ~(1 << pos);++_size; }test即判斷這個數據在不在,只需要讓1左移pos個位置,再用對應位置進行與運算,如果為1則說明存在,0則說明不存在
test
bool test(size_t x) const {size_t index = x >> 5;size_t pos = x % 32;return _bits[index] & (1 << pos); }完整代碼
#pragma once #include<vector>namespace lee {class bitset{public://每一個位標識一個數據,一個整型4個字節,可存儲32個位, 所以需要/32(或者右移五位)。+1是為了取整bitset(size_t size = 32) : _bits((size >> 5) + 1, 0), _size(0){}//數據的對應標識位置1void set(size_t x){//計算出在數組中哪一個整型中size_t index = x >> 5;//計算出在該整型的哪一個位上size_t pos = x % 32;//對應位置 置1_bits[index] |= (1 << pos);++_size;}//數據的對應標識位置0void reset(size_t x){size_t index = x >> 5;size_t pos = x % 32;//對應位置數據置零_bits[index] &= ~(1 << pos);++_size;}//判斷數據是否存在bool test(size_t x) const{size_t index = x >> 5;size_t pos = x % 32;return _bits[index] & (1 << pos);}size_t size() const{return _size;}private:std::vector<int> _bits;size_t _size;}; };布隆過濾器
我們在使用新聞客戶端看新聞時,它會給我們不停地推薦新的內容,它每次推薦時要去重,去掉那些已經看
過的內容。問題來了,新聞客戶端推薦系統如何實現推送去重的? 用服務器記錄了用戶看過的所有歷史記
錄,當推薦系統推薦新聞時會從每個用戶的歷史記錄里進行篩選,過濾掉那些已經存在的記錄。 如何快速查
找呢?
布隆過濾器概念
布隆過濾器是由布隆(Burton Howard Bloom)在1970年提出的 一種緊湊型的、比較巧妙的概率型數據結 構,特點是高效地插入和查詢,可以用來告訴你 “某樣東西一定不存在或者可能存在”,它是用多個哈希函
數,將一個數據映射到位圖結構中。此種方式不僅可以提升查詢效率,也可以節省大量的內存空間。
布隆過濾器的優缺點
優點
缺點
布隆過濾器的實現思路
這里底層使用的數據結構是前面實現的位圖,所以對應操作可以直接到上面看。
哈希沖突的問題
之前在哈希那一章說過,當字符串使用哈希時,無可避免的會出現哈希沖突的問題,而位圖又是一個不能解決哈希沖突的數據結構,所以這就導致了一個問題,對于一個數據不能只有一個位置來標記,需要用到多個位置。于是我們需要用到多個哈希函數,來將數據映射到多個位置上面,才能確保數據的準確性。
例如下面的baidu,分別通過三種哈希函數映射到了1,4,7。將這三個位置全部置1
這里我使用了三個字符串哈希函數,分別是BKDR,SDBM,RS。
如何選擇哈希函數個數和布隆過濾器長度
而如果一個數據要映射多個位置,如果布隆過濾器較小,則會導致數據馬上全部映射滿,此時無論進行什么操作,都會存在大量的誤報率。也就是說,布隆過濾器的長度與誤報率成反比,與空間利用率成反比。
并且哈希函數的個數也值得思考,哈希函數越多,映射的位置也就越多,此時準確性也就越高,但隨之帶來的問題就是效率的降低。也就是說,哈希函數的個數與效率成反比,準確率成正比
這張圖則是各種長度以及哈希函數的效率對比圖。
那么該如何選擇哈希函數的個數以及布隆過濾器的長度呢?
這里引用一位大佬計算出的公式,具體求解鏈接我放在了文章末尾
k 為哈希函數個數,m 為布隆過濾器長度,n 為插入的元素個數,p 為誤報率。
所以根據公式,我這里使用的哈希函數為3個,空間就應該開插入元素個數的五倍。
插入
數據分別映射到三個位置上,將三個位置全部置1
void set(const K& key) {//為了減少錯誤率,用多個哈希函數將同一個數據映射到多個位置size_t pos1 = Hash1()(key) % _capacity;size_t pos2 = Hash2()(key) % _capacity;size_t pos3 = Hash3()(key) % _capacity;_bs.set(pos1);_bs.set(pos2);_bs.set(pos3);++_size; }查找
布隆過濾器的查找即分別查找映射位,一旦有任何一個為0,則說明數據不存在。如果全部為1,此時說明數據可能存在,因為可能存在將別人映射的位置誤判進來,所以布隆過濾器的查找是不夠準確的。所以可以這么說,布隆過濾器只提供模糊查詢,如果需要精確查詢,只能使用別的方法。
布隆過濾器如果說某個元素不存在時,該元素一定不存在,如果該元素存在時,該元素可能存在,因
為有些哈希函數存在一定的誤判。
刪除
布隆過濾器是不支持刪除操作的,因為一旦進行刪除,很可能就會將別人映射的位置也置為0,導致出現錯誤。
但是如果非要刪除的話,也不是不行。
可以將每一個比特位拓展為一個計數器,每當有數據插入時對應位置的計數器+1,數據刪除是對應位的計數器-1。一個位肯定無法完成計數,需要用到多個位,此時就會導致存儲空間的大量增加,使得效率下降,而本身選擇布隆過濾器也是為了節省空間,這樣就本末倒置了。
完整代碼
#pragma once #include"bitset.hpp" #include<string> namespace lee {struct _BKDRHash{//BKDRHashsize_t operator()(const std::string& key){ size_t hash = 0;for (size_t i = 0; i < key.size(); i++){hash *= 131;hash += key[i];}return hash;}};struct _SDBMHash{//SDBMHashsize_t operator()(const std::string& key){size_t hash = 0;for (size_t i = 0; i < key.size(); i++){hash *= 65599;hash += key[i];}return hash;}};struct _RSHash{//RSHashsize_t operator()(const std::string& key){size_t hash = 0;size_t magic = 63689;for (size_t i = 0; i < key.size(); i++){hash *= magic;hash += key[i];magic *= 378551;}return hash;}};template<class K = std::string, class Hash1 = _BKDRHash, class Hash2 = _SDBMHash, class Hash3 = _RSHash>class BloomFilter{public:BloomFilter(size_t num): _bs(num), _capacity(num), _size(0){}void set(const K& key){//為了減少錯誤率,用多個哈希函數將同一個數據映射到多個位置size_t pos1 = Hash1()(key) % _capacity;size_t pos2 = Hash2()(key) % _capacity;size_t pos3 = Hash3()(key) % _capacity;_bs.set(pos1);_bs.set(pos2);_bs.set(pos3);++_size;}bool test(const K& key){size_t pos1 = Hash1()(key) % _capacity;size_t pos2 = Hash2()(key) % _capacity;size_t pos3 = Hash3()(key) % _capacity;if (!_bs.test(pos1) || !_bs.test(pos2) || !_bs.test(pos3)){return false;}return true;}size_t size() const{return _size;}private:lee::bitset _bs;size_t _size;size_t _capacity;}; };參考文章:
- 詳解布隆過濾器的原理、使用場景和注意事項
總結
以上是生活随笔為你收集整理的海量数据处理(一) :位图与布隆过滤器的概念以及实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C++ STL : 模拟实现STL中的关
- 下一篇: 高级数据结构与算法 | 并查集(Unio