布隆过滤器 (Bloom Filter) 是 1970 年由布隆提出的。它可以检索一个元素是否存在于集合中。它的优点是空间效率高,查询时间极快,缺点是有一定的误判率,而且删除困难。
1. 背景
编程中,经常会有判断一个元素是否存在一个集合中:
- 网络爬虫程序:判断一个地址是否被访问过;
- 文字处理软件:检查单词的拼写 (也就是判断它是否存在词典里);
- 电子邮件黑名单。
其中,最直接的办法是,将集合所有元素存储起来,判断时与集合中的元素比较即可。
一般来说会使用哈希表来存储集合,速度快效率高,可以在 O(1) 的时间复杂度返回结果。但是哈希表本身由于负载因子的存在,不可能用满,也就是会有空间浪费的问题,对于网络爬虫来说,可能会处理几十亿的 URL 链接集合,哈希表占据的内存大小是非常可观的。
2. 原理
布隆过滤器,实际上是由一个很长的 bit 向量和一系列的随机映射函数构成的。
它的原理是,当集合新增元素时,通过 K 个哈希函数将该元素映射为多个哈希值,并对每个生成的哈希值对应的 bit 位置为 1。
举个例子,针对 张三
使用 3 个哈希函数,生成了 3 个哈希值 1、5、7,设置对应的 bit 位后如下图所示:
^-------------------------------^
| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---------------------------------
| 1 | 0 | 1 | 0 | 0 | 0 | 1 | 0 |
v-------------------------------v
同样,也对 李四
使用 3 个哈希函数,生成了 3 个哈希值 2、3、7:
^-------------------------------^
| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---------------------------------
| 1 | 0 | 1 | 0 | 1 | 1 | 1 | 0 |
v-------------------------------v
要判断 钱五
是不是在集合里,计算哈希值 5、6、7,显然不在。
特别注意的是,对张三、李四、钱五都生成了相同的哈希值 7,所以布隆过滤器是会误判的。
以检测一个可疑电子邮件地址是否存在黑名单为例:
布隆过滤器绝对不会漏掉任何一个存在黑名单的电子邮箱地址,但它可能将不在黑名单中的电子邮箱误判。补救方法是建立一个白名单,将可能误判的电子邮箱地址保存起来。
3. 优点
- 存储空间,插入时间,查询时间均为常数 O(k);
- K 个哈希函数之间并无关联,方便硬件并行实现;
- 不需要存储原始元素,有利于数据保密。
4. 缺点
- 存在一定的误判率:这个很容易理解,因为不能保证不同元素通过哈希函数的计算后,得到不同的哈希值;
- 删除元素困难:这个也不难理解,多个元素计算后,可能会共用同一个 1,如果删除元素将其置 0,会导致其他元素出错。
引用维基百科的解释:
一般情况下不能从布隆过滤器中删除元素。 我们很容易想到把位数组变成整数数组,每插入一个元素相应的计数器加 1,这样删除元素时将计数器减掉就可以了。 然而要保证安全地删除元素并非如此简单。 首先我们必须保证删除的元素的确在布隆过滤器里面。 这一点单凭这个过滤器是无法保证的。 另外计数器回绕也会造成问题。
False positives 概率推导:https://www.cnblogs.com/liyulong1982/p/6013002.html
关于如何选择哈希函数个数和布隆过滤器长度,有一个公式,可以根据业务情况,在误断率和内存空间权衡:
代码语言:txt复制m = - (n * ln p) / (ln 2) ^ 2, k = m * ln * 2 / n
其中,k 为哈希函数个数,m 为布隆过滤器长度,n 为插入的元素个数,p 为误断率。
5. 实现
网上有个 Golang 版,有时间的话写个 PHP 版,不过 PHP 处理二进制真心不爽……