今天学习一个有用的算法:布隆过滤器 ( Bloom Filter )。


布隆过滤器是 1970 年由 Burton Howard Bloom 提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于快速检索一个元素是否在一个集合中。


先来了解一中数据结构——位图,因为布隆过滤器本身就是基于位图的,是对位图的一种改进。

位图

这里说的位图 ( BitMap ) 不是图形学中的像素图片,而是内存中连续的二进制位 (Bit ),用于对大量整形数据做去重和查询。


我们来举一个例子,假设有 1 千万个整数,取值范围都在 1 - 1 亿之间,如何快速判断某一个整数是否在这 1 千万个整数之中呢。我们当然可以用散列表来解决,但是今天我们来用位图解决一下。


先申请一个长度为 1 亿的 boolean[] 数组,我们把这 1 千万个整数作为数组下标,将对应的数组值设置为 true 。比如,整数5对应的就是 arr[5] = true。


当我们查询某个数 K 是否在这 1 千万个整数中时,只需要取出 arr[K] 看其是否为 true 即可。


当然在大多数编程语言中,boolean 是用 byte 实现的,1 亿个字节的数组所占内存空间为 95.37MB。我们知道一个字节等于 8 个比特 ( Bit ),事实上,标识 boolean 变量我们只用一个 Bit 就可以,1 代表 true, 0 代表 false。


这就对应着一个叫做位图的数据结构,这并不需要我们自己实现,JDK 的标准类库中的 BitSet 已经帮我们做好了这一点,其原理是在内部维护者一个 long 类型的数组,所以 BitSet 的最小空间为 64Bit ( long 占 8 字节 64Bit 大小),通过封装来进行按位操作,并且支持自动扩容。


此时 1 亿个元素的数组所占的内存空间就缩减到了 12MB。但是前提是我们假设的数字范围是 1 - 1 亿,假如数字范围是 1 - 10 亿呢?那位图的大小就是 10 亿个 Bit 位,也就是 120MB 的大小,消耗的内存空间不降反增。为了解决这个问题,布隆过滤器就出场了。

布隆过滤器

针对上述 1 - 10 亿的问题,我们仍然使用 1 亿个大小的位图,然后通过哈希函数,对数字进行处理,让其映射到 1 - 1 亿的区间内。当然哈希函数的哈希冲突是不可避免的,不过我们可以设法减少这种冲突,布隆过滤器的做法是采用多个哈希函数。


我们使用 K 个哈希函数对同一个数字进行求值,将会得到 K 个不同的哈希值 X1,X2 … Xk,将这 K 个值作为位图中的下标,将位图中的 arr[X1],arr[X2] … arr[Xk] 都设置为 true ,也就是说用 K 个二进制位标识一个数字存在。


这样我们在查询某个数字是否存在的时候,我们对这个数字分别用这 K 个哈希函数分别求值,若 arr[X1],arr[X2] … arr[Xk] 其中任意一个不为 true,则说明该数字不存在于集合中。若都为 true,则极有可能在集合中,因为哈希冲突的存在我们对两个不同的数字求哈希值,这两个值有可能重复。


布隆过滤器的原理是,当一个元素被加入集合时,通过 K 个散列函数将这个元素映射成一个位数组中的 K 个点,把它们置为 true。检索时,我们只要看看这些点是不是都是 true 就(大约)知道集合中有没有它了:如果这些点有任何一个为 false ,则被检元素一定不在;如果都是 true,则被检元素很可能在。即布隆过滤器判定一个元素不在集合中则一定不存在,若判定在集合中则可能存在


需要注意的是,布隆过滤器不允许 remove 元素,因为那样的话会把相应的 K 个比特位位置为 false ,而其中很有可能有其他元素对应的位。

小结

以爬虫爬取的 URL 判重为例,假设有 10 亿个 URL 待判断,每个URL平均长度为 64 字节,我们可以用一个 10 倍的位图来存储,也就是大约 1.2GB,而换成散列表的话考虑到哈希冲突,装载因子不能过小,至少也需要 100GB 的空间,所以布隆过滤器在存储空间的消耗上降低了非常多。


效率方面,布隆过滤器的一次匹配过程涉及到多个哈希函数的计算属于 CPU 密集型,而散列表进行内存中的数据的多次匹配,属于内存密集型,CPU 的运算速度比内存快的多,所以理论上布隆过滤器更加高效。


运用布隆过滤器需要对误判有一定容忍度,如垃圾邮件识别、爬虫 URL 去重、防止缓存击穿等场景。同时建议维护一个小的白名单用于防止误判。


不过布隆过滤器作为一种概率模型,我们可以通过设置合理的哈希函数个数和位图的大小来尽可能降低这种误判几率,已经有经过证明的公式可以参考,此处略去公式和证明过程。


实际运用中,我们并不需要自己实现一个布隆过滤器,Google 的 Guava 类库中提供了 BloomFilter 供我们使用,可以直接设定好误判率后直接交给 Guava 进行处理。

[参考]

[1]《数学之美》 吴军著

[2] 《数据结构与算法之美》 王争