假如有1亿个不重复的正整数(大致范围已知),但是只有1G的内存可用,如何判断该范围内的某个数是否出现在这1亿个数中?最常用的处理办法是利用位图,1*108/1024*1024*8=11.9,也只需要申请12M的内存。但是如果是1亿个邮件地址,如何确定某个邮件地址是否在这1亿个地址中?这个时候可能大家想到的最常用的办法就是利用Hash表了,但是大家可以细想一下,如果利用Hash表来处理,必须开辟空间去存储这1亿个邮件地址,因为在Hash表中不可能避免的会发生碰撞,假设一个邮件地址只占8个字节,为了保证Hash表的碰撞率,所以需要控制Hash表的装填因子在0.5左右,那么至少需要2*8*108/1024*1024*1024=1.5G的内存空间,这种情况下利用Hash表是无法处理的。这个时候要用到另外一种数据结构-布隆过滤器(Bloom Filter),它是由Burton Howard Bloom在1970年提出的,它结合了位图和Hash表两者的优点,位图的优点是节省空间,但是只能处理整型值一类的问题,无法处理字符串一类的问题,而Hash表却恰巧解决了位图无法解决的问题,然而Hash太浪费空间。针对这个问题,布隆提出了一种基于二进制向量和一系列随机函数的数据结构-布隆过滤器。它的空间利用率和时间效率是很多算法无法企及的,但是它也有一些缺点,就是会有一定的误判率并且不支持删除操作。
下面来讨论一下布隆过滤器的原理和它的应用。
一.布隆过滤器的原理
布隆过滤器需要的是一个位数组(这个和位图有点类似)和k个映射函数(和Hash表类似),在初始状态时,对于长度为m的位数组array,它的所有位都被置为0,如下图所示:
对于有n个元素的集合S={s1,s2......sn},通过k个映射函数{f1,f2,......fk},将集合S中的每个元素sj(1<=j<=n)映射为k个值{g1,g2......gk},然后再将位数组array中相对应的array[g1],array[g2]......array[gk]置为1:
如果要查找某个元素item是否在S中,则通过映射函数{f1,f2.....fk}得到k个值{g1,g2.....gk},然后再判断array[g1],array[g2]......array[gk]是否都为1,若全为1,则item在S中,否则item不在S中。这个就是布隆过滤器的实现原理。
当然有读者可能会问:即使array[g1],array[g2]......array[gk]都为1,能代表item一定在集合S中吗?不一定,因为有这个可能:就是集合中的若干个元素通过映射之后得到的数值恰巧包括g1,g2,.....gk,那么这种情况下可能会造成误判,但是这个概率很小,一般在万分之一以下。
很显然,布隆过滤器的误判率和这k个映射函数的设计有关,到目前为止,有很多人设计出了很多高效实用的hash函数,具体可以参考:《常见的Hash算法》这篇博文,里面列举了很多常见的Hash函数。并且可以证明布隆过滤器的误判率和位数组的大小以及映射函数的个数有关,相关证明可参考这篇博文:《布隆过滤器 (Bloom Filter) 详解》。假设误判率为p,位数组大小为m,集合数据个数为n,映射函数个数为k,它们之间的关系如下:
p=2-(m/n)*ln2 可得 m=(-n*lnp)/(ln2)2=-2*n*lnp=2*n*ln(1/p)
k=(m/n)*ln2=0.7*(m/n)
可以验证若p=0.1,(m/n)=9.6,即存储每个元素需要9.6bit位,此时k=0.7*(m/n)=6.72,即存储每个元素需要9.6个bit位,其中有6.72个bit位被置为1了,因此需要7个映射函数。从这里可以看出布隆过滤器的优越性了,比如上面例子中的,存储一个邮件地址,只需要10个bit位,而用hash表存储需要8*8=64个bit位。
一般情况下,p和n由用户设定,然后根据p和n的值设计位数组的大小和所需的映射函数的个数,再根据实际情况来设计映射函数。
尤其要注意的是,布隆过滤器是不允许删除元素的,因为若删除一个元素,可能会发生漏判的情况。不过有一种布隆过滤器的变体Counter Bloom Filter,可以支持删除元素,感兴趣的读者可以查阅相关文献资料。
二.布隆过滤器的应用
布隆过滤器在很多场合能发挥很好的效果,比如:网页URL的去重,垃圾邮件的判别,集合重复元素的判别,查询加速(比如基于key-value的存储系统)等,下面举几个例子:
1.有两个URL集合A,B,每个集合中大约有1亿个URL,每个URL占64字节,有1G的内存,如何找出两个集合中重复的URL。
很显然,直接利用Hash表会超出内存限制的范围。这里给出两种思路:
第一种:如果不允许一定的错误率的话,只有用分治的思想去解决,将A,B两个集合中的URL分别存到若干个文件中{f1,f2...fk}和{g1,g2....gk}中,然后取f1和g1的内容读入内存,将f1的内容存储到hash_map当中,然后再取g1中的url,若有相同的url,则写入到文件中,然后直到g1的内容读取完毕,再取g2...gk。然后再取f2的内容读入内存。。。依次类推,知道找出所有的重复url。
第二种:如果允许一定错误率的话,则可以用布隆过滤器的思想。
2.在进行网页爬虫时,其中有一个很重要的过程是重复URL的判别,如果将所有的url存入到数据库中,当数据库中URL的数量很多时,在判重时会造成效率低下,此时常见的一种做法就是利用布隆过滤器,还有一种方法是利用berkeley db来存储url,Berkeley db是一种基于key-value存储的非关系数据库引擎,能够大大提高url判重的效率。
布隆过滤器的简易版本实现:
/*布隆过滤器简易版本 2012.11.10*/ #include#include #include<string> #define MAX 2<<24 using namespace std; bitset bloomSet; //简化了由n和p生成m的过程 int seeds[7]={3, 7, 11, 13, 31, 37, 61}; //使用7个hash函数 int getHashValue(string str,int n) //计算Hash值 { int result=0; int i; for(i=0;i ) { result=seeds[n]*result+(int)str[i]; if(result > 2<<24) result%=2<<24; } return result; } bool isInBloomSet(string str) //判断是否在布隆过滤器中 { int i; for(i=0;i<7;i++) { int hash=getHashValue(str,i); if(bloomSet[hash]==0) return false; } return true; } void addToBloomSet(string str) //添加元素到布隆过滤器 { int i; for(i=0;i<7;i++) { int hash=getHashValue(str,i); bloomSet.set(hash,1); } } void initBloomSet() //初始化布隆过滤器 { addToBloomSet("http://www.baidu.com"); addToBloomSet("http://www.cnblogs.com"); addToBloomSet("http://www.google.com"); } int main(int argc, char *argv[]) { int n; initBloomSet(); while(scanf("%d",&n)==1) { string str; while(n--) { cin>>str; if(isInBloomSet(str)) cout<<"yes"<<endl; else cout<<"no"<<endl; } } return 0; }