Bloom Filter一般用于数据的去重计算,近似于HashSet的功能;但是不同于Bitmap(用于精确计算),其为一种估算的数据结构,存在误判(false positive)的情况。
1. 基本原理
Bloom Filter能高效地表征数据集合(S = lbrace x_1 ,x_2 ,...,x_n brace),判断某个数据是否属于这个集合。其基本思想如下:用长度为(m)的位数组(A)来存储集合信息,同时是有(k)个独立的hash函数(h_i(1le i le k))将数据映射到位数组空间。具体流程如下:
- 将长度为(m)的位数组全置为0;
- 对于数据(x in S),依次计算其(k)个hash函数值(h_i(x)=w,且1le i le k, 1 le w le m),将位数组中的第(a)位bit置为1,即A[w]=1.
当查询数据(y)是否属于集合(S)时,计算其(k)个hash函数值,如果(h_i(y))对应的位数组均为1,则数据(y)属于集合(S);反之,则不属于。
2. 相关计算
在上述判断中,可能存在误判(false positive, FP),比如某数的(k)个hash函数值可能属于集合(S)中某几个数(k)个hash函数值组成的集合。显然,误判率跟集合大小(n)、位数组大小(m)、hash函数的个数(k)有关;在其他条件不变的情况下,若(n)越大((m)越小,或(k)越多),则误判率越高。误判率估算公式如下:
在实际的场景中,常常是已知集合大小(n),预设误判率(P_{fp}),需要计算位数组大小(m)、hash函数的个数(k)。通过一系列的数学推导,可得到如下公式:
详细的数学推导可参看相关文档。
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) // 6
println(cnt) // 6
从上面的Scala代码中,不难发现:在Distinct Count计算过程中,需要定义一个global变量,逐一用于对每个不属于集合元素进行计算。显然,在分布式计算中,这种方法不太适用;因为global变量没法做到实时的传递更新。因此,另一种估算算法HyperLogLog,拥有优秀的可加性、易于并行化,在大数据的场景下应用广泛——Spark、Kylin中的近似Distinct Count便是基于此。
4. 参考资料
[1] Broder, Andrei, and Michael Mitzenmacher. "Network Applications of Bloom Filters: A Survey." Internet Mathematics 1.4 (2011): 485-509.
[2] 张俊林, 《大数据日知录》.