BloomFilter原理与Python实现

Bloom Filter是由Bloom在1970年提出的一种多哈希函数映射的快速查找算法。

通常应用在一些需要快速判断某个元素是否属于集合,但是并不严格要求100%正确的场合。

基于一种概率数据结构来实现,是一个有趣且强大的算法

. 实例

为了说明Bloom Filter存在的重要意义,举一个实例,也是为什么我要学习Boomfilter的起因:

假设我们要写一个爬虫程序。由于网络间的链接错综复杂,蜘蛛在网络间爬行很可能会形成“环”,爬虫就会进入一个无限怪圈,找不到出路,程序出现崩溃。

所以为了避免形成“环”,就需要知道蜘蛛已经访问过那些URL,也就是如何判重

给一个URL,怎样知道蜘蛛是否已经访问过呢?按照我们的常识,就会有如下几种方案:

  1. 将访问过的URL保存到数据库,数据库管理系统可以为你去重。

  2. Set将访问过的URL保存起来。那只需接近O(1)的代价就可以查到一个URL是否被访问过了。

  3. URL经过MD5SHA-1等单向哈希后再保存到Set数据库

  4. Bit-Map方法。建立一个BitSet,将每个URL经过一个哈希函数映射到某一位。

方法1~3都是将访问过的URL完整保存,方法4则只标记URL的一个映射位。

以上方法在数据量较小的情况下都能完美解决问题,但是当数据量变得非常庞大时问题就来了。

方法1的缺点:数据量变得非常庞大后关系型数据库查询的效率会变得很低。而且每来一个URL就启动一次数据库查询是不是太小题大做了?

方法2的缺点:太消耗内存。随着URL的增多,占用的内存会越来越多。就算只有1亿个URL,每个URL只算50个字符,至少需要5GB内存,还不包括Set数据结构中的内存浪费。

方法3的缺点:由于字符串经过MD5处理后的信息摘要长度只有128Bit,SHA-1处理后也只有160Bit,因此方法3比方法2节省了好几倍的内存。

方法4的缺点:消耗内存是相对较少的,但缺点是单一哈希函数发生冲突的概率太高。

若要降低冲突发生的概率到1%,有种办法就是就要将BitSet的长度设置为URL个数的100倍。

假设一亿条URL,就得把BitSet长度设为100亿,过于稀疏也是很费内存的

实质上上面的算法都忽略了一个重要的隐含条件:允许小概率的出错,不一定要100%准确!

也就是说少量url实际上没有没网络爬虫访问,而将它们错判为已访问的代价是很小的——大不了少抓几个网页呗。


. Bloom Filter 算法原理

废话说到这里,下面引入本篇的主角——Bloom Filter。其实上面方法4的思想已经很接近Bloom Filter了。

方法四的致命缺点是冲突概率高,为了降低冲突的概念,Bloom Filter使用了多个哈希函数,而不是一个。

为什么可以降低呢?我们知道Hash函数有一定几率出现冲突,概率假设为 p1,我们知道p1是一个很小的几率,但是在数据量大之后冲突就会变多,也就是上面第四种方法的问题。

BoomFilter使用 多个Hash函数 分别冲突概率为 p2 p3 p4 p5 … pn ,我们知道不同 Hash函数处理同一个字符串彼此独立,所以冲突概率通过乘法公式得到为: p1p2p3p4p5p6…pn,是相当相当小的了。

Bloom Filter算法如下:

预操作

创建一个m位BitSet(C++自带,Python为bitarray),先将所有位初始化为0,然后选择k个不同的哈希函数。第i个哈希函数对字符串str哈希的结果记为h(i,str),且h(i,str)的范围是0到m-1 。

BitSet清零

Add操作

下面是每个字符串处理的过程,首先是将字符串str“记录”到BitSet中的过程:

对于字符串str,分别计算h(1,str),h(2,str)…… h(k,str)。然后将BitSet的第h(1,str)、h(2,str)…… h(k,str)位设为1。

Bloom Filter加入字符串过程

很简单吧?这样就将字符串str映射到BitSet中的k个二进制位了。

CHeck操作

根据上图,我们对每个字符串采用同样的算法。

下面是检查字符串str是否被BitSet记录过的过程:

  • 对于字符串str,分别计算h(1,str),h(2,str)…… h(k,str)。然后检查BitSet的第h(1,str)、h(2,str)…… h(k,str)位是否为1,若其中任何一位不为1则可以判定str一定没有被记录过。若全部位都是1,则“认为”字符串str存在。

  • 若一个字符串对应的Bit不全为1,则可以肯定该字符串一定没有被Bloom Filter记录过。(这是显然的,因为字符串被记录过,其对应的二进制位肯定全部被设为1了)

  • 但是若一个字符串对应的Bit全为1,实际上是不能100%的肯定该字符串被Bloom Filter记录过的。(因为有可能该字符串的所有位都刚好是被其他字符串所对应)这种将该字符串划分错的情况,称为wrong position

Delete操作

字符串加入了就被不能删除了,因为删除会影响到其他字符串。实在需要删除字符串的可以使用Counting bloomfilter(CBF),这是一种基本Bloom Filter的变体,CBF将基本Bloom Filter每一个Bit改为一个计数器,这样就可以实现删除字符串的功能了。

Bloom Filter跟单哈希函数Bit-Map不同之处在于:Bloom Filter使用了k个哈希函数,每个字符串跟k个bit对应。从而降低了冲突的概率。


. Bloom Filter 优化

考虑到BoomFilter上面的指标,总结一下有以下几个

m : BitSet 位数

n : 插入字符串个数

k :hash函数个数

当然,哈希函数也是影响的重要因素

从表格来看 m/n越大越准,k越大越准。

但是具体怎么设计呢?

哈希函数选择

  • 哈希函数的选择对性能的影响应该是很大的,一个好的哈希函数要能近似等概率的将字符串映射到各个Bit。

  • 选择k个不同的哈希函数比较麻烦,一种简单的方法是选择一个哈希函数,然后送入k个不同的参数。

参数设计

相信大家对于 Bloom Filter 的工作原理都有了一个基本的了解,现在我们来看看在Bloom Filter 中涉及到的一些参数指标:

  • 欲插入Bloom Filter中的元素数目: n
  • Bloom Filter误判率: P(true)
  • BitArray数组的大小: m
  • Hash Function的数目: k

欲插入Bloom Filter中的元素数目 n 是我们在实际应用中可以提前获取或预估的;Bloom Filter的误判率 P(true) 则是我们提前设定的可以接受的容错率。所以在设计Bloom Filter过程中,最关键的参数就是BitArray数组的大小 m 和 Hash Function的数目 k,下面将给出这两个关键参数的设定依据、方法

误判率 P(true)

向Bloom Filter插入一个元素时,其一个Hash Function会将BitArray中的某Bit置为1,故对于任一Bit而言,其被置为1的概率 P 1 = 1 m P{1}=\frac{1}{m} P1=m1,那么其依然是0的概率 $ P0=1−P1=1−\frac{1}{m}$;易知插入一个元素时,其 k 个Hash Function 都未将该Bit置为1的概率 P 0 1 = ( 1 − 1 m ) k P{0}^1=(1−\frac{1}m)^k P01=(1m1)k。则向Bloom Filter 插入全部n个元素后,该Bit依然为0的概率即为 P 0 n = ( 1 − 1 m ) k n P{0}^n=(1−\frac{1}m)^{kn} P0n=(1m1)kn,反之,该Bit为1的概率则为 P 1 n = 1 − P 0 n = 1 − ( 1 − 1 m ) k n P{1}^n=1−P{0}^n=1−(1−\frac{1}m)^{kn} P1n=1P0n=1(1m1)kn

由前文可知,判定一个元素存在于Bloom Filter,要求k个Hash Function的哈希值对应的Bit的值均为1。据此,我们可以计算出其误判率 P(true):

P ( t r u e ) = ( P 1 n ) k = [ 1 − ( 1 − 1 m ) k n ] k P(true)=(P{1}^n)^k=[1−(1−\frac{1}m)^{kn}]^k P(true)=(P1n)k=[1(1m1)kn<

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

训灼说

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值