算法题目
如果一个黑名单网站包含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=(1−e−mnk)k
推导过程如下
假设布隆过滤器中的hash function满足simple uniform hashing假设:每个元素都等概率地hash到m个slot中的任何一个,与其它元素被hash到哪个slot无关。若m为bit数,则对某一特定bit位在一个元素由某特定hash function插入时没有被置位为1的概率为:
1
−
1
m
1-\frac{1}{m}
1−m1
则k个hash function中没有一个对其置位为1的概率为:
(
1
−
1
m
)
k
{(1-\frac{1}{m})}^k
(1−m1)k
如果插入了n个元素,但都未将其置位为1的概率为:
(
1
−
1
m
)
k
n
{(1-\frac{1}{m})}^{kn}
(1−m1)kn
则此位被置位为1的概率为:
1
−
(
1
−
1
m
)
k
n
1-{(1-\frac{1}{m})}^{kn}
1−(1−m1)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−(1−m1)kn,那么产生的k个比特位全部被置为1则产生误判,概率为
(
1
−
(
1
−
1
m
)
k
n
)
k
{(1-{(1-\frac{1}{m})}^{kn})}^k
(1−(1−m1)kn)k
由于
(
1
+
x
)
1
x
∼
e
(1+x)^{\frac{1}{x}} \sim e
(1+x)x1∼e,当
x
→
0
x→0
x→0时,并且
−
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−(1−m1)kn)k=(1−(1−m1)−m⋅mkn)k∼(1−e−mnk)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)=(1−e−mnk)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)=(1−b−k)k,两边取对数
ln
f
(
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求导
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)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
⋅
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(1−b−k)+k⋅1−b−kb−k⋅lnb=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
⇒(1−b−k)⋅ln(1−b−k)=−k⋅b−k⋅lnb
⇒
(
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}
⇒(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
=
1
2
\Rightarrow b^{-k}=\frac{1}{2}
⇒b−k=21
⇒
e
−
k
n
m
=
1
2
\Rightarrow e^{-\frac{k n}{m}}=\frac{1}{2}
⇒e−mkn=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=ln2⋅nm=0.7⋅nm
因此,即当
k
=
0.7
⋅
m
n
k=0.7 \cdot \frac{m}{n}
k=0.7⋅nm时误判率最低,此时误判率为:
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)=(1−21)k=2−k=2−ln2⋅nm≈0.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}
k≥1⇒nm≥ln21
这说明了若想保持某固定误判率不变,布隆过滤器的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=2−ln2⋅nm⇒lnp=ln2⋅(−ln2)⋅nm⇒m=−(ln2)2n⋅lnp
再由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=ln2⋅nm=0.7⋅nm
至此系统所需的参数已经备齐,接下来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)=2−k⇒log2P=−k⇒k=log2P1⇒ln2nm=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=ln21⋅log2P1=1.44⋅log2P1
因此可验证当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.44⋅log20.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⋅(log210a−log2a)=1.44⋅log210=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.7⋅nm=0.7∗9.6=6.72bits
才是每个元素对应的为1的bit位数。
k
=
0.7
⋅
m
n
k=0.7 \cdot \frac{m}{n}
k=0.7⋅nm从而使得P(error)最小时,我们注意到:
P
(
error
)
=
(
1
−
e
−
n
k
m
)
k
P(\text {error})=\left(1-e^{-\frac{n k}{m}}\right)^{k}
P(error)=(1−e−mnk)k中的
e
−
n
k
m
=
1
2
e^{-\frac{n k}{m}}=\frac{1}{2}
e−mnk=21,即
(
1
−
1
m
)
k
n
=
1
2
\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