如何高效判断元素w是否存在于集合A之中?首先想到的答案是,把集合A中的元素一个个放到哈希表中,然后在哈希表中查一下w即可。这样确实可以解决小数据量场景下元素存在性判定,但如果A中元素数量巨大,甚至数据量远远超过机器内存空间,该如何解决问题呢?
实现一个基于磁盘和内存的哈希索引当然可以解决这个问题。而另一种低成本的方式就是借助布隆过滤器(Bloom Filter)来实现。
布隆过滤器由一个长度为N的01数组array组成。首先将数组array每个元素初始设为0。
对集合A中的每个元素w,做K次哈希,第i次哈希值对N取模得到一个index(i),即index(i)=HASH_i(w)%N,将array数组中的array[index(i)]置为1。最终array变成一个某些元素为1的01数组。
下面举个例子,如图所示,A={x,y,z},N=18,K=3。
初始化array=000000000000000000。
对元素x,HASH_0(x)%N=0,HASH_1(x)%N=4,HASH_2(x)%N=16。因此array=100010000000000010。
对元素y,HASH_0(y)%N=7,HASH_1(y)%N=10,HASH_2(y)%N=14。因此array=000000010010001000。
对元素z,HASH_0(z)%N=2,HASH_1(y)%N=10,HASH_2(y)%N=17。因此array=001000000010000010。
此时,对于元素w,K次哈希值分别为:
HASH_0(w)%N=4
HASH_1(w)%N=13
HASH_2(w)%N=15
可以发现,布隆过滤器串中的第15位为0,因此可以确认w肯定不在集合A中。因为若w在A中,则第15位必定为1。
如果有另外一个元素t,K次哈希值分别为:
HASH_0(t)%N=1
HASH_1(t)%N=7
HASH_2(t)%N=17
我们发现布隆过滤器串中的第1、7、17位都为1,但是却没法肯定t一定在集合A中。
因此,布隆过滤器串对任意给定元素w,给出的存在性结果为两种:
·w可能存在于集合A中。
·w肯定不在集合A中。
在论文[1]中证明,当N取K*|A|/ln2时(其中|A|表示集合A元素个数),能保证最佳的误判率,所谓误判率也就是过滤器判定元素可能在集合中但实际不在集合中的占比。
举例来说,若集合有20个元素,K取3时,则设计一个N=3×20/ln2=87二进制串来保存布隆过滤器比较合适。
算法实现
布隆过滤器的代码实现很短,如下所示:
public class BloomFilter {
private int k;
private int bitsPerKey;
private int bitLen;
private byte[] result;
public BloomFilter(int k, int bitsPerKey) {
this.k = k;
this.bitsPerKey = bitsPerKey;
}
public byte[] generate(byte[][] keys) {
assert keys != null;
bitLen = keys.length * bitsPerKey;
bitLen = ((bitLen + 7) / 8) << 3; // align the bitLen.
bitLen = bitLen < 64 ? 64 : bitLen;
result = new byte[bitLen >> 3]; // each byte have 8 bit.
for (int i = 0; i < keys.length; i++) {
assert keys[i] != null;
int h = Bytes.hash(keys[i]);
for (int t = 0; t < k; t++) {
int idx = (h % bitLen + bitLen) % bitLen;
result[idx / 8] |= (1 << (idx % 8));
int delta = (h >> 17) | (h << 15);
h += delta;
}
}
return result;
}
public boolean contains(byte[] key) {
assert result != null;
int h = Bytes.hash(key);
for (int t = 0; t < k; t++) { // Hash k times
int idx = (h % bitLen + bitLen) % bitLen;
if ((result[idx / 8] & (1 << (idx % 8))) == 0) {
return false;
}
int delta = (h >> 17) | (h << 15);
h += delta;
}
return true;
}
}
有两个地方说明一下:
·在构造方法BloomFilter(int k,int bitsPerKey)中,k表示每个Key哈希的次数,bitsPerkey表示每个Key占用的二进制bit数,若有x个Key,则N=x*bitsPerKey。
·在实现中,对Key做k次哈希时,算出第一次哈希值h之后,可借助h位运算来实现二次哈希,甚至三次哈希。这样性能会比较好。