Bloom Filter一般用于数据的去重计算,近似于HashSet的功能;但是不同于Bitmap(用于精确计算),其为一种估算的数据结构,存在误判(false positive)的情况。
1. 基本原理
Bloom Filter能高效地表征数据集合S={x1,x2,...,xn},判断某个数据是否属于这个集合。其基本思想如下:用长度为m的位数组A来存储集合信息,同时是有k个独立的hash函数hi(1≤i≤k)将数据映射到位数组空间。具体流程如下:
- 将长度为m的位数组全置为0;
- 对于数据x∈S,依次计算其k个hash函数值hi(x)=w,且1≤i≤k,1≤w≤mhi(x)=w,且1≤i≤k,1≤w≤m,将位数组中的第a位bit置为1,即A[w]=1.
当查询数据y是否属于集合S时,计算其k个hash函数值,如果hi(y))对应的位数组均为1,则数据y属于集合S;反之,则不属于。
2. 相关计算
在上述判断中,可能存在误判(false positive, FP),比如某数的kk个hash函数值可能属于集合S中某几个数k个hash函数值组成的集合。显然,误判率跟集合大小n、位数组大小m、hash函数的个数k有关;在其他条件不变的情况下,若nn越大(m越小,或k越多),则误判率越高。误判率估算公式如下:
在实际的场景中,常常是已知集合大小nn,预设误判率PfpPfp,需要计算位数组大小mm、hash函数的个数kk。通过一系列的数学推导,可得到如下公式:
详细的数学推导可参看相关文档。
3. 实战
Bloom Filter的Java实现有Guava、stream-lib,Scala实现有breeze、bloom-filter-scala。采用breeze库的Distinct Count实现如下:
import breeze.util.BloomFilter
val bf = BloomFilter.optimallySized[Int](5, 0.01)
val arr = Array(1, 3, 4, 5, 1, 2, 6, 3, 1)
var cnt = 0
arr.foreach { t =>
bf.contains(t) match {
case false => cnt += 1; bf.+=(t)
case _ =>
}
}
println(arr.distinct.length)
println(cnt)
从上面的Scala代码中,不难发现:在Distinct Count计算过程中,需要定义一个global变量,逐一用于对每个不属于集合元素进行计算。显然,在分布式计算中,这种方法不太适用;因为global变量没法做到实时的传递更新。因此,另一种估算算法HyperLogLog,拥有优秀的可加性、易于并行化,在大数据的场景下应用广泛——Spark、Kylin中的近似Distinct Count便是基于此。
4. 参考资料
[1] A. Broder and M. Mitzenmacher, Network applications of bloom filters: A survey.
[2] 张俊林, 《大数据日知录》.