布隆過濾器
這名詞有沒有聽着好像很 挺高大上的,的確,它也是一種很重要的結構,下面一起看看:
一:說說歷史:
(Bloom Filter)是由布隆(Burton Howard Bloom)在1970年提出的。它實際上是由一個很長的二進制向量和一系列隨機映射函數組成,布隆過濾器可以用於檢索一個元素是否在一個集合中。它的優點是空間效率和查詢時間都遠遠超過一般的算法,缺點是有一定的誤識別率(假正例False positives,即Bloom Filter報告某一元素存在於某集合中,但是實際上該元素並不在集合中)和刪除困難,但是沒有識別錯誤的情形(即假反例False negatives,如果某個元素確實沒有在該集合中,那麼Bloom Filter 是不會報告該元素存在於集合中的,所以不會漏報)。
在日常生活中,包括在設計計算機軟件時,我們經常要判斷一個元素是否在一個集合中。比如在字處理軟件中,需要檢查一個英語單詞是否拼寫正確(也就是要判斷 它是否在已知的字典中);
在 FBI,一個嫌疑人的名字是否已經在嫌疑名單上;在網絡爬蟲裏,一個網址是否被訪問過等等。最直接的方法就是將集合中全部的元素存在計算機中,遇到一個新 元素時,將它和集合中的元素直接比較即可。一般來講,計算機中的集合是用哈希表(hash table)來存儲的。它的好處是快速準確,缺點是費存儲空間。當集合比較小時,這個問題不顯著,但是當集合巨大時,哈希表存儲效率低的問題就顯現出來 了。
比如說,一個象 Yahoo,Hotmail 和 Gmai 那樣的公衆電子郵件(email)提供商,總是需要過濾來自發送垃圾郵件的人(spamer)的垃圾郵件。一個辦法就是記錄下那些發垃圾郵件的 email 地址。由於那些發送者不停地在註冊新的地址,全世界少說也有幾十億個發垃圾郵件的地址,將他們都存起來則需要大量的網絡服務器。如果用哈希表,每存儲一億 個 email 地址, 就需要 1.6GB 的內存(用哈希表實現的具體辦法是將每一個 email 地址對應成一個八字節的信息指紋,然後將這些信息指紋存入哈希表,由於哈希表的存儲效率一般只有 50%,因此一個 email 地址需要佔用十六個字節。一億個地址大約要 1.6GB, 即十六億字節的內存)。因此存貯幾十億個郵件地址可能需要上百 GB 的內存。除非是超級計算機,一般服務器是無法存儲的[2]。(該段引用谷歌數學之美:http://www.google.com.hk/ggblog/googlechinablog/2007/07/bloom-filter_7469.html)
二:概念:
如果想判斷一個元素是不是在一個集合裏,一般想到的是將所有元素保存起來,然後通過比較確定。鏈表,樹等等數據結構都是這種思路.但是隨着集合中元素的增加,我們需要的存儲空間越來越大,檢索速度也越來越慢。不過世界上還有一種叫作散列表(又叫哈希表,Hash table)的數據結構。它可以通過一個Hash函數將一個元素映射成一個位陣列(Bit Array)中的一個點(關於位陣列,即數據結構位圖,詳見位圖見我另一篇博客:位圖BitMap)。這樣一來,我們只要看看這個點是不是 1就知道可以集合中有沒有它了。這就是布隆過濾器的基本思想。
Hash面臨的問題就是衝突。假設 Hash 函數是良好的,如果我們的位陣列長度爲 m 個點,那麼如果我們想將衝突率降低到例如 1%, 這個散列表就只能容納 m/100 個元素。顯然這就不叫空間有效了(Space-efficient)。解決方法也簡單,就是使用多個
Hash(如下圖所示),如果它們有一個說元素不在集合中,那肯定就不在。如果它們都說在,雖然也有一定可能性它們在說謊,不過直覺上判斷這種事情的概率是比較低的。
代碼:
bitmap.h
#ifndef _BIT_MAP_H
#define _BIT_MAP_H
#include<iostream>
#include<vector>
using namespace std;
/*
*一個數據32位,40億個整數,每個整數需用一位表示,40億位就完事
*/
class BitMap
{
public:
BitMap()
:_size(0)
{}
BitMap(size_t size)
:_size(0)
{
_array.resize((size>>5)+1); //多少個數據,一個數據佔32位,加一是至少一個數據
}
bool Set(size_t num)
{
size_t index = num >> 5; //計算在哪個數據上
size_t n = num % 32;
if (_array[index] & (1 << (31 - n))) //移位問題
{
cout << "有數據" << endl;
return false;
}
else
{
size_t a = 1 << (31 - n);
_array[index] |= a;
++_size;
return true;
}
}
bool ReSet(size_t num) //刪除一個數 之後重置
{
size_t index = num >> 5;
size_t n = num % 32;
if (_array[index] & (1 << (31 - n))) //數存在 刪除
{
_array[index] &= (~(1 << (31 - n)));
--_size;
return true;
}
else
{
return false; //不存在這個數
}
}
private:
vector<size_t> _array; //數組
size_t _size; //位圖中數據個數
};
#endif
void Test()
{
BitMap bm(65);
for (int i = 0; i < 32; ++i)
{
bm.Set(i);
}
bm.ReSet(0);
}
HashFun.h#pragma once
template<class T> //各類哈希函數
size_t BKDRHash(const char *str)
{
register size_t hash = 0;
while (size_t ch = (size_t)*str++)
{
hash = hash * 131 + ch;
}
return hash;
}
template<class T>
size_t SDBMHash(const char *str)
{
register size_t hash = 0;
while (size_t ch = (size_t)*str++)
{
hash = 65599 * hash + ch;
}
return hash;
}
template<class T>
size_t RSHash(const char * str)
{
size_t hash = 0;
size_t magic = 63689;
while (size_t ch = (size_t)*str++)
{
hash = hash * magic + ch;
magic *= 378551;
}
return hash;
}
template<class T>
size_t APHash(const char *str)
{
register size_t hash = 0;
size_t ch;
for (long i = 0; ch = (size_t)*str++; i++)
{
if ((i & 1) == 0)
{
hash ^= ((hash << 7) ^ ch ^ (hash >> 3));
}
else
{
hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));
}
}
return hash;
}
template<class T>
size_t JSHash(const char* str)
{
if (!*str)
{
return 0;
}
size_t hash = 1315423911;
while (size_t ch = (size_t)*str++)
{
hash ^= ((hash << 5) + ch + (hash >> 2));
}
return hash;
}
Bloom_Filter.h#pragma once
#include"BitMap.h"
#include"HashFun.h"
template<class T>
struct __HashFun1 //5種哈希函數對應的仿函數
{
size_t operator()(const T& key)
{
return BKDRHash<T>(key.c_str());
}
};
template<class T>
struct __HashFun2
{
size_t operator()(const T& key)
{
return SDBMHash<T>(key.c_str());
}
};
template<class T>
struct __HashFun3
{
size_t operator()(const T& key)
{
return RSHash<T>(key.c_str());
}
};
template<class T>
struct __HashFun4
{
size_t operator()(const T& key)
{
return APHash<T>(key.c_str());
}
};
template<class T>
struct __HashFun5
{
size_t operator()(const T& key)
{
return JSHash<T>(key.c_str());
}
};
template<class K = string,
class HashFun1 = __HashFun1<K>,
class HashFun2 = __HashFun2<K>,
class HashFun3 = __HashFun3<K>,
class HashFun4 = __HashFun4<K>,
class HashFun5 = __HashFun5<K>>
class Bloom_Filter
{
public:
Bloom_Filter(size_t size)
:_capacity(size)
{
_bitmap._array.resize((size >> 5) + 1);
}
void _Set(const K& key)
{
_bitmap.Set(HashFun1()(key) % _capacity);
_bitmap.Set(HashFun2()(key) % _capacity);
_bitmap.Set(HashFun3()(key) % _capacity);
_bitmap.Set(HashFun4()(key) % _capacity);
_bitmap.Set(HashFun5()(key) % _capacity);
}
bool _IsIn(const K& key)
{
if (!_bitmap.Test(HashFun1()(key) % _capacity))
return false;
if (!_bitmap.Test(HashFun1()(key) % _capacity))
return false;
if (!_bitmap.Test(HashFun1()(key) % _capacity))
return false;
if (!_bitmap.Test(HashFun1()(key) % _capacity))
return false;
if (!_bitmap.Test(HashFun1()(key) % _capacity))
return false;
return true;
}
private:
BitMap _bitmap;
size_t _capacity;
};
三、布隆過濾器優缺點:
1.優點:
相比於其它的數據結構,布隆過濾器在空間和時間方面都有巨大的優勢。布隆過濾器存儲空間和插入/查詢時間都是常數。另外, Hash 函數相互之間沒有關係,方便由硬件並行實現。布隆過濾器不需要存儲元素本身,在某些對保密要求非常嚴格的場合有優勢。布隆過濾器可以表示全集,其它任何數據結構都不能;k 和 m 相同,使用同一組 Hash 函數的兩個布隆過濾器的交併差運算可以使用位操作進行。
2.缺點
但是布隆過濾器的缺點和優點一樣明顯。誤算率(False Positive)是其中之一。隨着存入的元素數量增加,誤算率隨之增加。但是如果元素數量太少,則使用散列表足矣。另外,一般情況下不能從布隆過濾器中刪除元素. 我們很容易想到把位列陣變成整數數組,每插入一個元素相應的計數器加1, 這樣刪除元素時將計數器減掉就可以了。然而要保證安全的刪除元素並非如此簡單。首先我們必須保證刪除的元素的確在布隆過濾器裏面. 這一點單憑這個過濾器是無法保證的。另外計數器迴繞也會造成問題。
賜教!