C+學習——淺談位圖與布隆過濾器

1.位圖

所謂位圖,就是用每一位來存放某種狀態,適用於大規模數據,但數據狀態又不是很多的情況。通常是用來判斷某個數據存不存在的。

題目:
給40億個不重複的無符號整數,沒排過序。給一個無符號整數,如何快 速判斷一個數是否在這40億個數中。 【騰訊】

思路:
這道題首先要判斷40億個不重複的無符號整數究竟佔多大的內存,因爲太大的內存我們無法加載到現有的計算機中。

一個整數是4個字節,40億個整數就是160億個字節,也就相當於16G內存,就一般的計算機而言很難實現這個加載,所以我們可以採取以下兩種方案,一種是分割,一種是位圖。

方法:
①分割
採用分割處理,把40億個數分批次處理完畢,當然可以實現我們最終的目標,但是這樣做時間複雜度未免優點太高。

②位圖BitMap
在介紹這種方法前我首先來介紹一下什麼是位圖。

位圖BitMap:位圖是一個數組的每一個數據的每一個二進制位表示一個數據,0表示數據不存在,1表示數據存在。
在這裏插入圖片描述
如上所示,當我們需要存放一個數據的時候,我們需要安裝以下方法:

1.首先確定這個數字在整個數據的哪一個數據(區間)。
2.確定這個數據(區間)的哪一個Bit位上。
3.在這個位上置1即可。

位圖的實現代碼

#include <iostream>
#include <vector>
using namespace std;

class BitMap
{
public:
    BitMap(size_t range)
    {
        //此時多開闢一個空間
        _bits.resize(range / 32 + 1); //size爲要處理的的數據總數,
        //一個整型可以存放32個數據狀態,所以這裏vector的size爲size/32

    }
    void Set(size_t x)
    {
        int index = x / 32;//確定哪個數據(區間)
        int temp = x % 32;//確定哪個Bit位
        _bits[index] |= (1 << temp);//位操作即可
    }
    void Reset(size_t x)
    {
        int index = x / 32;
        int temp = x % 32;
        _bits[index] &= ~(1 << temp);//取反
    }
    bool Test(size_t x)
    {
        int index = x / 32;
        int temp = x % 32;
        if (_bits[index]&(1<<temp))
            return 1;
        else
            return 0;
    }

// 位圖中比特爲1的個數
size_t Count()const
 {
 const char* pCount = "\0\1\1\2\1\2\2\3\1\2\2\3\2\3\3\4";
 size_t size = _bit.size();
 size_t count = 0;
 for(size_t i = 0; i < size; ++i)
 {
 int value = _bit[i];
 int j = 0;
 while(j < sizeof(_bit[0]))
 {
 char c = value;
 count += pCount[c&0x0f];
 c >>= 4;
 count += pCount[c&0x0f];
 ++j;
 value >>= 8;
 }
 }
 return count;
}

private:
    vector<int> _bits;
};

2.布隆過濾器

1.布隆過濾器的提出
我們在使用新聞客戶端看新聞時,它會給我們不停地推薦新的內容,它每次推薦時要去重,去掉那些已經看過的內容。問題來了,新聞客戶端推薦系統如何實現推送去重的? 用服務器記錄了用戶看過的所有歷史記錄,當推薦系統推薦新聞時會從每個用戶的歷史記錄裏進行篩選,過濾掉那些已經存在的記錄。 如何快速查找呢?

  1. 用哈希表存儲用戶記錄,缺點:浪費空間
  2. 用位圖存儲用戶記錄,缺點:不能處理哈希衝突
  3. 將哈希與位圖結合,即布隆過濾器

2.什麼是布隆過濾器
本質上布隆過濾器是一種數據結構,比較巧妙的概率型數據結構(probabilistic data structure),特點是高效地插入和查詢,可以用來告訴你 “某樣東西一定不存在或者可能存在”。相比於傳統的 List、Set、Map 等數據結構,它更高效、佔用空間更少,但是缺點是其返回的結果是概率性的,而不是確切的。

3.布隆過濾器數據結構
布隆過濾器是一個 bit 向量或者說 bit 數組,長這樣:
在這裏插入圖片描述
如果我們要映射一個值到布隆過濾器中,我們需要使用多個不同的哈希函數生成多個哈希值,並對每個生成的哈希值指向的 bit 位置 1,例如針對值 “baidu” 和三個不同的哈希函數分別生成了哈希值 1、4、7,則上圖轉變爲:
在這裏插入圖片描述
Ok,我們現在再存一個值 “tencent”,如果哈希函數返回 3、4、8 的話,圖繼續變爲:在這裏插入圖片描述
值得注意的是,4 這個 bit 位由於兩個值的哈希函數都返回了這個 bit 位,因此它被覆蓋了。現在我們如果想查詢 “dianping” 這個值是否存在,哈希函數返回了 1、5、8三個值,結果我們發現 5 這個 bit 位上的值爲 0,說明沒有任何一個值映射到這個 bit 位上,因此我們可以很確定地說 “dianping” 這個值不存在。而當我們需要查詢 “baidu” 這個值是否存在的話,那麼哈希函數必然會返回 1、4、7,然後我們檢查發現這三個 bit 位上的值均爲 1,那麼我們可以說 “baidu” 存在了麼?答案是不可以,只能是 “baidu” 這個值可能存在。

這是爲什麼呢?答案跟簡單,因爲隨着增加的值越來越多,被置爲 1 的 bit 位也會越來越多,這樣某個值 “taobao” 即使沒有被存儲過,但是萬一哈希函數返回的三個 bit 位都被其他值置位了 1 ,那麼程序還是會判斷 “taobao” 這個值存在。

4.下面就結合位圖和哈希思想,我們實現布隆過濾器:

// 假設布隆過濾器中元素類型爲K,每個元素對應5個哈希函數
template<class K, class KToInt1 = KeyToInt1, 
 class KToInt2 = KeyToInt2,
 class KToInt3 = KeyToInt3,
 class KToInt4 = KeyToInt4,
 class KToInt5 = KeyToInt5>
class BloomFilter
{
public:
 BloomFilter(size_t size) // 布隆過濾器中元素個數
 : _bmp(10*size)
 , _size(0)
 {}
 
private:
 BitMap _bmp;
 size_t _size; // 實際元素的個數
}

布隆過濾器的插入

bool Insert(const K& key)
{
 size_t bitCount = _bmp.Size();
 size_t index1 = KToInt1()(key)%bitCount;
 size_t index2 = KToInt2()(key)%bitCount;
 size_t index3 = KToInt3()(key)%bitCount;
 size_t index4 = KToInt4()(key)%bitCount;
 size_t index5 = KToInt5()(key)%bitCount;
 _bmp.Set(index1);
 _bmp.Set(index2);
 _bmp.Set(index3);
 _bmp.Set(index4);
 _bmp.Set(index5);
 _size++;
}

布隆過濾器的查找

布隆過濾器的思想是將一個元素用多個哈希函數映射到一個位圖中,因此被映射到的位置的比特位一定爲1。所以可以按照以下方式進行查找:分別計算每個哈希值對應的比特位置存儲的是否爲零,只要有一個爲零,代表該元素一定不在哈希表中,否則可能在哈希表中。

注意:布隆過濾器如果說某個元素不存在時,該元素一定不存在,如果該元素存在時,該元素可能存在,因爲有些哈希函數存在一定的誤判。

bool IsInBloomFilter(const K& key)
{
 size_t bitCount = _bmp.Size();
 size_t index1 = KToInt1()(key)%bitCount;
 if(!_bmp.Test(index1))
 return false;
 size_t index2 = KToInt2()(key)%bitCount;
 if(!_bmp.Test(index2))
 return false;
 size_t index3 = KToInt3()(key)%bitCount;
 if(!_bmp.Test(index3))
 return false;
 size_t index4 = KToInt4()(key)%bitCount;
 if(!_bmp.Test(index4))
 return false;
 size_t index5 = KToInt5()(key)%bitCount;
 if(!_bmp.Test(index5))
 return false;
 return true; // 有可能在
 }

布隆過濾器支持刪除麼

目前我們知道布隆過濾器可以支持 add 和 isExist 操作,那麼 delete 操作可以麼,答案是不可以,例如上圖中的 bit 位 4 被兩個值共同覆蓋的話,一旦你刪除其中一個值例如 “tencent” 而將其置位 0,那麼下次判斷另一個值例如 “baidu” 是否存在的話,會直接返回 false,而實際上你並沒有刪除它。
如何解決這個問題,答案是計數刪除。但是計數刪除需要存儲一個數值,而不是原先的 bit 位,會增大佔用的內存大小。這樣的話,增加一個值就是將對應索引槽上存儲的值加一,刪除則是減一,判斷是否存在則是看值是否大於0。

如何選擇哈希函數個數和布隆過濾器長度

很顯然,過小的布隆過濾器很快所有的 bit 位均爲 1,那麼查詢任何值都會返回“可能存在”,起不到過濾的目的了。布隆過濾器的長度會直接影響誤報率,布隆過濾器越長其誤報率越小。
另外,哈希函數的個數也需要權衡,個數越多則布隆過濾器 bit 位置位 1 的速度越快,且布隆過濾器的效率越低;但是如果太少的話,那我們的誤報率會變高。

最佳實踐

常見的適用常見有,利用布隆過濾器減少磁盤 IO 或者網絡請求,因爲一旦一個值必定不存在的話,我們可以不用進行後續昂貴的查詢請求。

另外,既然你使用布隆過濾器來加速查找和判斷是否存在,那麼性能很低的哈希函數不是個好選擇,推薦 MurmurHash、Fnv 這些。

這裏推薦一篇文章寫得不錯,大家可以作爲參考:https://www.jianshu.com/p/2104d11ee0a2

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章