引文
思考一个问题:从大量数据里面如何高效率地去重?
有过一点编程经验的人都知道,可以通过Set这种数据结构来做到。比如HashSet,采用了Hash算法,可以在O(1)的复杂度完成数据的添加和查询操作。确实,大多数情况,这也是我们会采取的方案。但是因为Set需要保存源数据信息,且有Hash冲突,当样本数据量特别庞大的情况下,比如有千万甚至上亿的数据量时,这种方式显得有些不切实际。
布隆过滤器
布隆过滤器(Bloom Filter)的思想跟Hashmap有部分类似,也是通过hash函数映射的方式保存样本信息,不一样的是它依赖的数据结构是一个位数组,数组每一位上要么是0,要么是1,初始状态全是0。
当要往过滤器添加一个元素的时候,我们需要n(n是正整数)个独立的hash函数给目标元素做哈希运算,然后我们将得到的n个结果分别映射到位数组上。
举个简单的例子,假设我们要添加元素的元素是数字,我们取n为3,选取的3个hash函数分别是
- h a s h 1 ( x ) = ( x 2 ) hash_{1}(x) = (x ^ 2) hash1(x)=(x2)%20
- h a s h 2 ( x ) = ( x 3 ) hash_{2}(x) = (x ^ 3) hash2(x)=(x3)%20
- h a s h 3 ( x ) = ( x 4 ) hash_{3}(x) = (x ^ 4) hash3(x)=(x4)%20
当添加7这个元素的时候,通过三个hash函数计算的结果分别是9,3和1,我们把位数组对应下标的元素记为1。
这样元素7就在位数组上以9,3和1三个位置信息的形式,存储了起来。后续所有的元素都经过三个hash函数,映射到位数组上。于是当我们要判断某个元素是否存在的时候,我们去数组上此元素应该在的位置处查看,对应的数字是否都为1。如果有数字为0,那么元素肯定不存在。如果数字都为1,那么元素大概率存在。
算法研究
布隆过滤器是一种概率数据结构(probabilistic data structure),可能会出现误判,但不会漏判。因为不同元素的hash结果可能会相同,而且被映射位置的数字可能已经被其他元素置为1,所以我们不能判断某个元素一定重复。
Hash函数
过滤器需要一系列独立的hash函数,函数的目标是将输出均匀地打散在目标域上。也就是说,元素通过hash函数映射到位数组上任何位置的概率应该是相同的。另外还有重要的一点,就是算法速度要快,过滤器的性能很大程度上取决于hash函数的性能。好的hash函数能在保持良好时间效率的情况下,降低过滤器的误判率。其中比较知名的是MurmurHash, FNV Hash, MD5
参数选择
一个好的布隆过滤器,需要有很高的空间时间效率,较低并且可控的误判率。从而我们很容易发现几个问题。
- 如何选取位数组长度m。m越大,占用的空间越大;m越小,误判的几率越高。
- 如何选取hash函数个数k。k越大,数组越快被占满从而误判率越高;k越小,产生hash冲突的概率越大,导致误判率也越高。
对于上述的问题,切入点是最小化误差率。怎样是误差?不就是当查询一个未重复元素的时候,对应映射的位置已经都为1了吗。对于长度为m的位数组,某个位置被设为1的概率是 1 m \frac{1}{m} m1,所以这个位置仍然为0的概率是
1 − 1 m 1-\frac{1}{m} 1