集合数据结构一般都有这么一个方法:contains。其作用就是判断给定的元素是否存在集合中,这是一个常用的方法。其最简单的内部实现即遍历集合内的元素,一个个的判断是否与给定元素相等。为了更高效点我们甚至可以采用“更好的(好是相对的)”算法实现。比如如果该集合是已经排序的,那么我们用二分查找来实现contains肯定更好。但是,如果集合的数据量庞大到一定程度,大部分我们熟知的算法不再有什么用了。即使可以使用,但是机器内存也不允许。
而Bloom Filter就是这么一个空间利用率非常高的算法。我们先来看看这个算法的原理:
1 首先我们有一个长度为n的比特数组,开始的时候将这个比特数组里所有的元素都初始化为0
00000000000000000000
上面的比特数组n为20
2 然后选取k个哈希函数,这k个哈希函数产生的结果的值的范围在0到n-1之间(对于上面的比特数组,即0到19) 。对每个要添加进集合的对象进行哈希运算,然后将哈希计算结果作为数组的索引,将索引位置的比特位设置为1(不管该比特位原先为0还是为1)。
比如我们选取三个哈希函数,对于对象A哈希值为0,5,7。那么比特数组就为:
10000101000000000000
对象B的值为2,8,13,那么添加B后的比特数组为:
10100101100001000000
对象C为0,4,7(对象C的第一个哈希函数的值与对象A的相同了,没关系我们还是设置为1就可以了):
10101101100001000000
现在我们的Bloom Filter里已经有3个元素了。现在我们要判断某元素X是否在该集合中。就相当于我们要实现一个contains方法。那么这个方法如何实现呢?
对元素X采用相同的三个哈希函数哈希,然后以这三个哈希值为索引去比特数组里找。如果三个索引位置的比特位都为1我们就认为该元素在集合中,否则不是。
我们可以用伪代码简单的描述一下这个算法:
private bit[] bitSet = new bit[N];
public void add(Object element){
int[] hashValues = getHashValues(element);
for( int i : hashValues){
bitSet[i] = 1;
}
}
public boolean contains(Object element){
int[] hashValues = getHashValues(element);
for( int i : hashValues){
if(bitSet[i] != 1) return false;
}
return true;
}
}
算法还是挺直观的,对不。想想,一个很大的对象,经过一哈希,然后就变成了Bloom Filter里面的一个比特,这个空间利用效率是多么高啊。如果哈希函数的实现效率也很高的话那么不仅空间利用率高,时间复杂度也低啊。这真是一个神奇的算法对吧。
可能你想,以后我就把我们那个啥数组的contains方法替换成Bloom Filter的实现吧。
不过你仔细验证过这个算法没,它存在一些问题。这个算法有以下这么几个特征:
1 如果该元素真的在集合中,那么Bloom Filter的contains方法肯定会返回true,这就是Bloom Filter不会漏报的特性。
2 如果该元素不在集合中,但Bloom Filter的contains方法有可能返回true。因为不同的元素经过哈希之后哈希值可能发生碰撞。这是Bloom Filter有可能误报的特性。但是这个误报的几率并不高。
根据这两个特性Bloom Filter在大量数据时还是挺有用的。比如假设我们有一个缓存服务器集群,集群里的不同的服务器承担的缓存也不尽相同。如果一个用户请求过来了,我们如何能快速的判断出用户请求的这个url在集群里哪台服务器上呢?因为每台服务器上缓存的url对应的页面非常庞大,我们全部弄到内存里代价也很高。我们就可以在每台服务器上放一个Bloom Filter,里面添加的都是本服务器上有缓存的那些url。这样即使Bloom Filter误报了,那就是把一个url发到了一个并不持有该url对应的缓存的服务器上,结果就是缓存未命中,缓存服务器只需要将该url打到后端的上游服务器就好了。
根据Bloom Filter的特征我们可以看到不是所有的场景都可以用的,只有在一些能容许少量的误报的情况下使用才行。该算法用很低的误报率却换来了大量的存储空间,实在是是一个很巧妙的算法。
Bloom Filter算法:http://en.wikipedia.org/wiki/Bloom_filter