Bloom Filter(BF) 是由Bloom在1970年提出的一种多哈希函数映射的快速查找算法,用于快速查找某个元素是否属于集合, 但不要求百分百的准确率。 Bloom filter通常用于爬虫的url去重,即判断某个url是否已经被爬过。
为了说明Bloom Filter存在的重要意义,举一个实例:
假设要你写一个网络蜘蛛(web crawler)。由于网络间的链接错综复杂,蜘蛛在网络间爬行很可能会形成“环”。为了避免形成“环”,就需要知道蜘蛛已经访问过那些URL。给一个URL,怎样知道蜘蛛是否已经访问过呢?稍微想想,就会有如下几种方案:
1. 将访问过的URL保存到数据库。
2. 用HashSet将访问过的URL保存起来。那只需接近O(1)的代价就可以查到一个URL是否被访问过了。
3. URL经过MD5或SHA-1等单向哈希后再保存到HashSet或数据库。
4. Bit-Map方法。建立一个BitSet,将每个URL经过一个哈希函数映射到某一位。
方法1~3都是将访问过的URL完整保存,方法4则只标记URL的一个映射位。
以上方法在数据量较小的情况下都能完美解决问题,但是当数据量变得非常庞大时问题就来了。
方法1的缺点:数据量变得非常庞大后关系型数据库查询的效率会变得很低。而且每来一个URL就启动一次数据库查询是不是太小题大做了?
方法2的缺点:太消耗内存。随着URL的增多,占用的内存会越来越多。就算只有1亿个URL,每个URL只算50个字符,就需要5GB内存。
方法3:由于字符串经过MD5处理后的信息摘要长度只有128Bit,SHA-1处理后也只有160Bit,因此方法3比方法2节省了好几倍的内存。
方法4消耗内存是相对较少的,但缺点是单一哈希函数发生冲突的概率太高。还记得数据结构课上学过的Hash表冲突的各种解决方法么?若要降低冲突发生的概率到1%,就要将BitSet的长度设置为URL个数的100倍。
实质上上面的算法都忽略了一个重要的隐含条件:允许小概率的出错,不一定要100%准确!也就是说少量url实际上没有没网络蜘蛛访问,而将它们错判为已访问的代价是很小的——大不了少抓几个网页呗。
(1)哈希函数选择
哈希函数的选择对性能的影响应该是很大的,一个好的哈希函数要能近似等概率的将字符串映射到各个Bit。选择k个不同的哈希函数比较麻烦,一种简单的方法是选择一个哈希函数,然后送入k个不同的参数。
(2)Bit数组大小选择
哈希函数个数k、位数组大小m、加入的字符串数量n的关系可以参考参考文献1。该文献证明了对于给定的m、n,当 k = ln(2)* m/n 时出错的概率是最小的。
同时该文献还给出特定的k,m,n的出错概率。例如:根据参考文献,哈希函数个数k取10,位数组大小m设为字符串个数n的20倍时,false positive发生的概率是0.0000889 ,这个概率基本能满足网络爬虫的需求了。
c++实现实例:
#include <iostream>
#include <cstdio>
#include <bitset>
#include <cstdlib>
#include <cmath>
#include <vector>
#include <cstring>
using namespace std;
using std::bitset;
typedef unsigned int uint;
#define N 1<<20
int array[] = {5, 7, 11, 13, 31, 37, 61};
class BloomFilter
{
public:
BloomFilter(int m, int n):m_m(m), m_n(n)
{
m_k = ceil((m/n) * log(2));
}
virtual ~BloomFilter()
{}
uint RSHash(const char *str, int seed);
void SetKey(const char* str);
int VaryExist(const char* str);
private:
int m_k, m_m, m_n; //k:number of the hash functions //m:the size of bitset //n:number of strings to hash (k = [m/n]*ln2)
bitset<N> bit;
};
uint BloomFilter::RSHash(const char *str, int seed)
{
//unsigned int b = 378551;
uint a = 63689;
uint hash = 0;
while (*str)
{
hash = hash * a + (*str++);
a *= seed;
}
return (hash & 0x7FFFFFFF);
}
void BloomFilter::SetKey(const char* str)
{
int *p = new int[m_k+1];
memset(p, 0, sizeof(p)/sizeof(int));
for(int i=0;i<m_k;++i)
{
p[i] = static_cast<int>(RSHash(str, array[i]))%1000000;
}
for(int j=0;j<m_k; ++j)
{
bit[p[j]] = 1;
}
delete[] p;
}
int BloomFilter::VaryExist(const char* str)
{
int res = 1;
int *p = new int[m_k+1];
memset(p, 0, sizeof(p)/sizeof(int));
for(int i=0;i<m_k;++i)
{
p[i] = static_cast<int>(RSHash(str, array[i]))%1000000;
}
for(int j=0;j<m_k;++j)
{
res &= bit[p[j]];
if(!res)
{
delete[] p;
return 0;
}
}
delete[] p;
return 1;
}
int main(int argc, char *argv[])
{
BloomFilter bf(5, 2);
string str = "hahahehe";
string str2 = "sdfasfa";
bf.SetKey(str.c_str());
int res = bf.VaryExist(str.c_str());
if(res)
cout << "exist" << endl;
else
cout << "not exist" << endl;
//bf.SetKey(str2.c_str());
res = bf.VaryExist(str2.c_str());
if(res)
cout << "exist" << endl;
else
cout << "not exist" << endl;
return 0;
}