算法题目
如果一个黑名单网站包含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×lnp(ln2)2k=ln2×mn=0.7×mnp=(1−e−nkm)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=(1−e−mnk)k
推导过程如下
假设布隆过滤器中的hash function满足simple uniform hashing假设:每个元素都等概率地hash到m个slot中的任何一个,与其它元素被hash到哪个slot无关。若m为bit数,则对某一特定bit位在一个元素由某特定hash function插入时没有被置位为1的概率为:
1−1m1-\frac{1}{m}1−m1
则k个hash function中没有一个对其置位为1的概率为:
(1−1m)k{(1-\frac{1}{m})}^k(1−m1)k
如果插入了n个元素,但都未将其置位为1的概率为:
(1−1m)kn{(1-\frac{1}{m})}^{kn}(1−m1)kn
则此位被置位为1的概率为:
1−(1−1m)kn1-{(1-\frac{1}{m})}^{kn}1−(1−m1)kn
现在考虑query阶段产生误判的概率,如果对于一个query,经过k个hash function之后,产生的k个比特位已被之前的n个元素置为了1,便产生了误判。之前已经计算了n个元素经过k个Hash函数后,某一位被置为1的概率是1−(1−1m)kn1-{(1-\frac{1}{m})}^{kn}1−(1−m1)kn,那么产生的k个比特位全部被置为1则产生误判,概率为
(1−(1−1m)kn)k{(1-{(1-\frac{1}{m})}^{kn})}^k(1−(1−m1)kn)k
由于(1+x)1x∼e(1+x)^{\frac{1}{x}} \sim e(1+x)x1∼e,当x→0x→0x→0时,并且−1m-\frac{1}{m}−m1,当m很大时趋近于0,所以
(1−(1−1m)kn)k=(1−(1−1m)−m⋅knm)k∼(1−e−nkm)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−(1−m1)kn)k=(1−(1−m1)−m⋅mkn)k∼(1−e−mnk)k
从上式中可以看出,当m增大或n减小时,都会使得误判率减小,这也符合直觉。
现在计算对于给定的m和n,k为何值时可以使得误判率最低。设误判率为k的函数为:
f(k)=(1−e−nkm)kf(k)=\left(1-e^{-\frac{n k}{m}}\right)^{k}f(k)=(1−e−mnk)k
设b=enmb=e^{\frac{n}{m}}b=emn,则简化为
f(k)=(1−b−k)kf(k)=\left(1-b^{-k}\right)^{k}f(k)=(1−b−k)k,两边取对数
lnf(k)=k⋅ln(1−b−k)\ln f(k)=k \cdot \ln \left(1-b^{-k}\right)lnf(k)=k⋅ln(1−b−k),两边对k求导
1f(k)⋅f′(k)=ln(1−b−k)+k⋅11−b−k⋅(−b−k)⋅lnb⋅(−1)=ln(1−b−k)+k⋅b−k⋅lnb1−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)1⋅f′(k)=ln(1−b−k)+k⋅1−b−k1⋅(−b−k)⋅lnb⋅(−1)=ln(1−b−k)+k⋅1−b−kb−k⋅lnb
下面求最值
ln(1−b−k)+k⋅b−k⋅lnb1−b−k=0\ln \left(1-b^{-k}\right)+k \cdot \frac{b^{-k} \cdot \ln b}{1-b^{-k}}=0ln(1−b−k)+k⋅1−b−kb−k⋅lnb=0
⇒(1−b−k)⋅ln(1−b−k)=−k⋅b−k⋅lnb\Rightarrow \left(1-b^{-k}\right) \cdot \ln \left(1-b^{-k}\right)=-k \cdot b^{-k} \cdot \ln b⇒(1−b−k)⋅ln(1−b−k)=−k⋅b−k⋅lnb
⇒(1−b−k)⋅ln(1−b−k)=b−k⋅lnb−k\Rightarrow \left(1-b^{-k}\right) \cdot \ln \left(1-b^{-k}\right)=b^{-k} \cdot \ln b^{-k}⇒(1−b−k)⋅ln(1−b−k)=b−k⋅lnb−k
⇒1−b−k=b−k\Rightarrow 1-b^{-k}=b^{-k}⇒1−b−k=b−k
⇒b−k=12\Rightarrow b^{-k}=\frac{1}{2}⇒b−k=21
⇒e−knm=12\Rightarrow e^{-\frac{k n}{m}}=\frac{1}{2}⇒e−mkn=21
⇒knm=ln2\Rightarrow \frac{k n}{m}=\ln 2⇒mkn=ln2
⇒k=ln2⋅mn=0.7⋅mn\Rightarrow k=\ln 2 \cdot \frac{m}{n}=0.7 \cdot \frac{m}{n}⇒k=ln2⋅nm=0.7⋅nm
因此,即当k=0.7⋅mnk=0.7 \cdot \frac{m}{n}k=0.7⋅nm时误判率最低,此时误判率为:
P(error)=(1−12)k=2−k=2−ln2⋅mn≈0.6185mnP(\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)=(1−21)k=2−k=2−ln2⋅nm≈0.6185nm
可以看出若要使得误判率≤1/2≤1/2≤1/2,则:
k≥1⇒mn≥1ln2k \geq 1 \Rightarrow \frac{m}{n} \geq \frac{1}{\ln 2}k≥1⇒nm≥ln21
这说明了若想保持某固定误判率不变,布隆过滤器的bit数m与被add的元素数n应该是线性同步增加的。
设计和应用布隆过滤器的方法
应用时首先要先由用户决定要add的元素数n和希望的误差率P。这也是一个设计完整的布隆过滤器需要用户输入的仅有的两个参数,之后的所有参数将由系统计算,并由此建立布隆过滤器。
系统首先要计算需要的内存大小m bits:
P=2−ln2⋅mn⇒lnp=ln2⋅(−ln2)⋅mn⇒m=−n⋅lnp(ln2)2P=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=2−ln2⋅nm⇒lnp=ln2⋅(−ln2)⋅nm⇒m=−(ln2)2n⋅lnp
再由m,n得到hash function的个数:
k=ln2⋅mn=0.7⋅mnk=\ln 2 \cdot \frac{m}{n}=0.7 \cdot \frac{m}{n}k=ln2⋅nm=0.7⋅nm
至此系统所需的参数已经备齐,接下来add n个元素至布隆过滤器中,再进行query。
根据公式,当k最优时:
P(error)=2−k⇒log2P=−k⇒k=log21P⇒ln2mn=log21PP(\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)=2−k⇒log2P=−k⇒k=log2P1⇒ln2nm=log2P1
⟹mn=1ln2⋅log21P=1.44⋅log21P\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=ln21⋅log2P1=1.44⋅log2P1
因此可验证当P=1%时,存储每个元素需要9.6 bits:
mn=1.44⋅log210.01=9.6bits\frac{\mathrm{m}}{n}=1.44 \cdot \log _{2} \frac{1}{0.01}=9.6 \mathrm{bits}nm=1.44⋅log20.011=9.6bits
而每当想将误判率降低为原来的1/10,则存储每个元素需要增加4.8 bits:
mn=1.44⋅(log210a−log2a)=1.44⋅log210=4.8bits\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⋅(log210a−log2a)=1.44⋅log210=4.8bits
这里需要特别注意的是,9.6 bits/element不仅包含了被置为1的k位,还把包含了没有被置为1的一些位数。此时的
k=0.7⋅mn=0.7∗9.6=6.72bitsk=0.7 \cdot \frac{m}{n}=0.7 * 9.6=6.72 \mathrm{bits}k=0.7⋅nm=0.7∗9.6=6.72bits
才是每个元素对应的为1的bit位数。
k=0.7⋅mnk=0.7 \cdot \frac{m}{n}k=0.7⋅nm从而使得P(error)最小时,我们注意到:
P(error)=(1−e−nkm)kP(\text {error})=\left(1-e^{-\frac{n k}{m}}\right)^{k}P(error)=(1−e−mnk)k中的e−nkm=12e^{-\frac{n k}{m}}=\frac{1}{2}e−mnk=21,即
(1−1m)kn=12\left(1-\frac{1}{m}\right)^{k n}=\frac{1}{2}(1−m1)kn=21
此概率为某bit位在插入n个元素后未被置位的概率。因此,想保持错误率低,布隆过滤器的空间使用率需为50%。
关于布隆过滤器使用的哈希函数
MurmurHash算法:高运算性能,低碰撞率的hash算法
- To be continued
关于布隆过滤器的代码实现
- To be continued

190

被折叠的 条评论
为什么被折叠?



