数学之美:布隆过滤器

算法题目

如果一个黑名单网站包含100亿个黑名单网页,每个网页最多占64B,设计一个系统,判断当前的URL是否在这个黑名单当中,要求额外空间不超过30GB,允许误差率为万分之一。

解题思路

  • 布隆过滤器
基础介绍

布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。
布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

实际工程的应用

实际上,布隆过滤器广泛应用于网页黑名单系统、垃圾邮件过滤系统、爬虫网址判重系统等,有人会想,我直接将网页URL存入数据库进行查找不就好了,或者建立一个哈希表进行查找不就OK了。

当数据量小的时候,这么思考是对的,但如果整个网页黑名单系统包含100亿个网页URL,在数据库查找是很费时的,并且如果每个URL空间为64B,那么需要内存为640GB,一般的服务器很难达到这个需求。

那么,在这种内存不够且检索速度慢的情况下,不妨考虑下布隆过滤器,但业务上要可以忍受判断失误率。
在这里插入图片描述

位图(bitmap)

布隆过滤器其中重要的实现就是位图的实现,也就是位数组,并且在这个数组中每一个位置只占有1个bit,而每个bit只有0和1两种状态。如上图bitarray所示!bitarray也叫bitmap,大小也就是布隆过滤器的大小。

假设一种有k个哈希函数,且每个哈希函数的输出范围都大于m,接着将输出值对k取余(%m),就会得到k个[0, m-1]的值,由于每个哈希函数之间相互独立,因此这k个数也相互独立,最后将这k个数对应到bitarray上并标记为1(涂黑)。

等判断时,将输入对象经过这k个哈希函数计算得到k个值,然后判断对应bitarray的k个位置是否都为1(是否标黑),如果有一个不为黑,那么这个输入对象则不在这个集合中,也就不是黑名单了!如果都是黑,那说明在集合中,但有可能会误,由于当输入对象过多,而集合也就是bitarray过小,则会出现大部分为黑的情况,那样就容易发生误判!因此使用布隆过滤器是需要容忍错误率的,即使很低很低!

布隆过滤器重要参数计算

通过上面的描述,我们可以知道,如果输入量过大,而bitarray空间的大小又很小,那么误判率就会上升。那么bitarray空间大小怎么确定呢?不要慌,已经有人通过数据推倒出公式了!!!哈哈,直接用~

假设输入对象个数为n,bitarray大小(也就是布隆过滤器大小)为m,所容忍的误判率p和哈希函数的个数k。计算公式如下:(小数向上取整)
m = − n × ln ⁡ p ( ln ⁡ 2 ) 2 k = ln ⁡ 2 × m n = 0.7 × m n p = ( 1 − e − n k m ) k \begin{array}{c}{\mathrm{m}=-\frac{n \times \ln p}{(\ln 2)^{2}}} \\ {\mathrm{k}=\ln 2 \times \frac{m}{n}=0.7 \times \frac{m}{n}} \\ {p=\left(1-e^{-\frac{n k}{m}}\right)^{k}}\end{array} m=(ln2)2n×lnpk=ln2×nm=0.7×nmp=(1emnk)k

推导过程如下
假设布隆过滤器中的hash function满足simple uniform hashing假设:每个元素都等概率地hash到m个slot中的任何一个,与其它元素被hash到哪个slot无关。若m为bit数,则对某一特定bit位在一个元素由某特定hash function插入时没有被置位为1的概率为:
1 − 1 m 1-\frac{1}{m} 1m1
则k个hash function中没有一个对其置位为1的概率为:
( 1 − 1 m ) k {(1-\frac{1}{m})}^k (1m1)k
如果插入了n个元素,但都未将其置位为1的概率为:
( 1 − 1 m ) k n {(1-\frac{1}{m})}^{kn} (1m1)kn
则此位被置位为1的概率为:
1 − ( 1 − 1 m ) k n 1-{(1-\frac{1}{m})}^{kn} 1(1m1)kn
现在考虑query阶段产生误判的概率,如果对于一个query,经过k个hash function之后,产生的k个比特位已被之前的n个元素置为了1,便产生了误判。之前已经计算了n个元素经过k个Hash函数后,某一位被置为1的概率是 1 − ( 1 − 1 m ) k n 1-{(1-\frac{1}{m})}^{kn} 1(1m1)kn,那么产生的k个比特位全部被置为1则产生误判,概率为
( 1 − ( 1 − 1 m ) k n ) k {(1-{(1-\frac{1}{m})}^{kn})}^k (1(1m1)kn)k
由于 ( 1 + x ) 1 x ∼ e (1+x)^{\frac{1}{x}} \sim e (1+x)x1e,当 x → 0 x→0 x0时,并且 − 1 m -\frac{1}{m} m1,当m很大时趋近于0,所以
( 1 − ( 1 − 1 m ) k n ) k = ( 1 − ( 1 − 1 m ) − m ⋅ k n m ) k ∼ ( 1 − e − n k m ) k \left(1-\left(1-\frac{1}{m}\right)^{k n}\right)^{k}=\left(1-\left(1-\frac{1}{m}\right)^{-m \cdot \frac{k n}{m}}\right)^{k} \sim\left(1-e^{-\frac{n k}{m}}\right)^{k} (1(1m1)kn)k=(1(1m1)mmkn)k(1emnk)k
从上式中可以看出,当m增大或n减小时,都会使得误判率减小,这也符合直觉。

现在计算对于给定的m和n,k为何值时可以使得误判率最低。设误判率为k的函数为:

f ( k ) = ( 1 − e − n k m ) k f(k)=\left(1-e^{-\frac{n k}{m}}\right)^{k} f(k)=(1emnk)k
b = e n m b=e^{\frac{n}{m}} b=emn,则简化为
f ( k ) = ( 1 − b − k ) k f(k)=\left(1-b^{-k}\right)^{k} f(k)=(1bk)k,两边取对数
ln ⁡ f ( k ) = k ⋅ ln ⁡ ( 1 − b − k ) \ln f(k)=k \cdot \ln \left(1-b^{-k}\right) lnf(k)=kln(1bk),两边对k求导
1 f ( k ) ⋅ f ′ ( k ) = ln ⁡ ( 1 − b − k ) + k ⋅ 1 1 − b − k ⋅ ( − b − k ) ⋅ ln ⁡ b ⋅ ( − 1 ) = ln ⁡ ( 1 − b − k ) + k ⋅ b − k ⋅ ln ⁡ b 1 − b − k \begin{aligned} \frac{1}{f(k)} \cdot f^{\prime}(k) &=\ln \left(1-b^{-k}\right)+k \cdot \frac{1}{1-b^{-k}} \cdot\left(-b^{-k}\right) \cdot \ln b \cdot(-1) \\ &=\ln \left(1-b^{-k}\right)+k \cdot \frac{b^{-k} \cdot \ln b}{1-b^{-k}} \end{aligned} f(k)1f(k)=ln(1bk)+k1bk1(bk)lnb(1)=ln(1bk)+k1bkbklnb
下面求最值
ln ⁡ ( 1 − b − k ) + k ⋅ b − k ⋅ ln ⁡ b 1 − b − k = 0 \ln \left(1-b^{-k}\right)+k \cdot \frac{b^{-k} \cdot \ln b}{1-b^{-k}}=0 ln(1bk)+k1bkbklnb=0
⇒ ( 1 − b − k ) ⋅ ln ⁡ ( 1 − b − k ) = − k ⋅ b − k ⋅ ln ⁡ b \Rightarrow \left(1-b^{-k}\right) \cdot \ln \left(1-b^{-k}\right)=-k \cdot b^{-k} \cdot \ln b (1bk)ln(1bk)=kbklnb
⇒ ( 1 − b − k ) ⋅ ln ⁡ ( 1 − b − k ) = b − k ⋅ ln ⁡ b − k \Rightarrow \left(1-b^{-k}\right) \cdot \ln \left(1-b^{-k}\right)=b^{-k} \cdot \ln b^{-k} (1bk)ln(1bk)=bklnbk
⇒ 1 − b − k = b − k \Rightarrow 1-b^{-k}=b^{-k} 1bk=bk
⇒ b − k = 1 2 \Rightarrow b^{-k}=\frac{1}{2} bk=21
⇒ e − k n m = 1 2 \Rightarrow e^{-\frac{k n}{m}}=\frac{1}{2} emkn=21
⇒ k n m = ln ⁡ 2 \Rightarrow \frac{k n}{m}=\ln 2 mkn=ln2
⇒ k = ln ⁡ 2 ⋅ m n = 0.7 ⋅ m n \Rightarrow k=\ln 2 \cdot \frac{m}{n}=0.7 \cdot \frac{m}{n} k=ln2nm=0.7nm
因此,即当 k = 0.7 ⋅ m n k=0.7 \cdot \frac{m}{n} k=0.7nm时误判率最低,此时误判率为:
P ( error ) = ( 1 − 1 2 ) k = 2 − k = 2 − ln ⁡ 2 ⋅ m n ≈ 0.618 5 m n P(\text {error})=\left(1-\frac{1}{2}\right)^{k}=2^{-k}=2^{-\ln 2 \cdot \frac{m}{n}} \approx 0.6185^{\frac{m}{n}} P(error)=(121)k=2k=2ln2nm0.6185nm
可以看出若要使得误判率 ≤ 1 / 2 ≤1/2 1/2,则:
k ≥ 1 ⇒ m n ≥ 1 ln ⁡ 2 k \geq 1 \Rightarrow \frac{m}{n} \geq \frac{1}{\ln 2} k1nmln21
这说明了若想保持某固定误判率不变,布隆过滤器的bit数m与被add的元素数n应该是线性同步增加的。

设计和应用布隆过滤器的方法

应用时首先要先由用户决定要add的元素数n和希望的误差率P。这也是一个设计完整的布隆过滤器需要用户输入的仅有的两个参数,之后的所有参数将由系统计算,并由此建立布隆过滤器。
系统首先要计算需要的内存大小m bits:
P = 2 − l n 2 ⋅ m n ⇒ ln ⁡ p = ln ⁡ 2 ⋅ ( − ln ⁡ 2 ) ⋅ m n ⇒ m = − n ⋅ ln ⁡ p ( ln ⁡ 2 ) 2 P=2^{-l n 2 \cdot \frac{m}{n}} \Rightarrow \ln p=\ln 2 \cdot(-\ln 2) \cdot \frac{\mathrm{m}}{\mathrm{n}} \Rightarrow \mathrm{m}=-\frac{n \cdot \ln p}{(\ln 2)^{2}} P=2ln2nmlnp=ln2(ln2)nmm=(ln2)2nlnp
再由m,n得到hash function的个数:
k = ln ⁡ 2 ⋅ m n = 0.7 ⋅ m n k=\ln 2 \cdot \frac{m}{n}=0.7 \cdot \frac{m}{n} k=ln2nm=0.7nm
至此系统所需的参数已经备齐,接下来add n个元素至布隆过滤器中,再进行query。

根据公式,当k最优时:
P ( error ) = 2 − k ⇒ log ⁡ 2 P = − k ⇒ k = log ⁡ 2 1 P ⇒ ln ⁡ 2 m n = log ⁡ 2 1 P P(\text {error})=2^{-k} \Rightarrow \log _{2} P=-k \Rightarrow \mathrm{k}=\log _{2} \frac{1}{P} \Rightarrow \ln 2 \frac{\mathrm{m}}{\mathrm{n}}=\log _{2} \frac{1}{P} P(error)=2klog2P=kk=log2P1ln2nm=log2P1
⟹ m n = 1 l n 2 ⋅ log ⁡ 2 1 P = 1.44 ⋅ log ⁡ 2 1 P \Longrightarrow \frac{\mathrm{m}}{\mathrm{n}}=\frac{1}{ln 2} \cdot \log _{2} \frac{1}{P}=1.44 \cdot \log _{2} \frac{1}{P} nm=ln21log2P1=1.44log2P1
因此可验证当P=1%时,存储每个元素需要9.6 bits:
m n = 1.44 ⋅ log ⁡ 2 1 0.01 = 9.6 b i t s \frac{\mathrm{m}}{n}=1.44 \cdot \log _{2} \frac{1}{0.01}=9.6 \mathrm{bits} nm=1.44log20.011=9.6bits
而每当想将误判率降低为原来的1/10,则存储每个元素需要增加4.8 bits:
m n = 1.44 ⋅ ( log ⁡ 2 10 a − log ⁡ 2 a ) = 1.44 ⋅ log ⁡ 2 10 = 4.8 b i t s \frac{\mathrm{m}}{\mathrm{n}}=1.44 \cdot\left(\log _{2} 10 a-\log _{2} a\right)=1.44 \cdot \log _{2} 10=4.8 \mathrm{bits} nm=1.44(log210alog2a)=1.44log210=4.8bits
这里需要特别注意的是,9.6 bits/element不仅包含了被置为1的k位,还把包含了没有被置为1的一些位数。此时的
k = 0.7 ⋅ m n = 0.7 ∗ 9.6 = 6.72 b i t s k=0.7 \cdot \frac{m}{n}=0.7 * 9.6=6.72 \mathrm{bits} k=0.7nm=0.79.6=6.72bits
才是每个元素对应的为1的bit位数。
k = 0.7 ⋅ m n k=0.7 \cdot \frac{m}{n} k=0.7nm从而使得P(error)最小时,我们注意到:
P ( error ) = ( 1 − e − n k m ) k P(\text {error})=\left(1-e^{-\frac{n k}{m}}\right)^{k} P(error)=(1emnk)k中的 e − n k m = 1 2 e^{-\frac{n k}{m}}=\frac{1}{2} emnk=21,即
( 1 − 1 m ) k n = 1 2 \left(1-\frac{1}{m}\right)^{k n}=\frac{1}{2} (1m1)kn=21
此概率为某bit位在插入n个元素后未被置位的概率。因此,想保持错误率低,布隆过滤器的空间使用率需为50%。

关于布隆过滤器使用的哈希函数

MurmurHash算法:高运算性能,低碰撞率的hash算法

  • To be continued
关于布隆过滤器的代码实现
  • To be continued
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值