海量数据处理之BloomFilter
一提到元素查找,我们会很自然的想到HashMap。通过将哈希函数作用于key上,我们得到了哈希值,基于哈希值我们可以去表里的相应位置获取对应的数据。除了存在哈希冲突问题之外,HashMap一个很大的问题就是空间效率低。引入Bloom Filter则可以很好的解决空间效率的问题。
掌握本文内容前,建议先熟练掌握前面一篇文章
原理
Bloom Filter是一种空间效率很高的随机数据结构,Bloom filter 可以看做是对bit-map 的扩展,布隆过滤器被设计为一个具有N的元素的位数组A(bit array),初始时所有的位都置为0。
当一个元素被加入集合时,通过K个Hash函数将这个元素映射成一个位阵列(Bit array)中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了。
如果这些点有任何一个 0,则被检索元素一定不在;
如果都是 1,则被检索元素很可能在。
添加元素
要添加一个元素,我们需要提供k个哈希函数。每个函数都能返回一个值,这个值必须能够作为位数组的索引(可以通过对数组长度进行取模得到)。然后,我们把位数组在这个索引处的值设为1。例如,第一个哈希函数作用于元素I上,返回x。类似的,第二个第三个哈希函数返回y与z,那么:
A[x]=A[y]=A[z] = 1
查找元素
查找的过程与上面的过程类似,元素将会被不同的哈希函数处理三次,每个哈希函数都返回一个作为位数组索引值的整数,然后我们检测位数组在x、y与z处的值是否为1。如果有一处不为1,那么就说明这个元素没有被添加到这个布隆过滤器中。如果都为1,就说明这个元素在布隆过滤器里面。当然,会有一定误判的概率。
算法优化
通过上面的解释我们可以知道,如果想设计出一个好的布隆过滤器,我们必须遵循以下准则:
- 好的哈希函数能够尽可能的返回宽范围的哈希值。
- 位数组的大小(用m表示)非常重要:如果太小,那么所有的位很快就都会被赋值为1,这样就增加了误判的几率。
- 哈希函数的个数(用k表示)对索引值的均匀分配也很重要。
计算m的公式如下:
m = - nlog p / (log2)^2
这里p为可接受的误判率。
计算k的公式如下:
k = m/n log(2)
这里k=哈希函数个数,m=位数组个数,n=待检测元素的个数(后面会用到这几个字母)。
哈希算法
哈希算法是影响布隆过滤器性能的地方。我们需要选择一个效率高但不耗时的哈希函数,在论文《更少的哈希函数,相同的性能指标:构造一个更好的布隆过滤器》中,讨论了如何选用2个哈希函数来模拟k个哈希函数。首先,我们需要计算两个哈希函数h1(x)与h2(x)。然后,我们可以用这两个哈希函数来模仿产生k个哈希函数的效果:
gi(x) = h1(x) + ih2(x)
这里i的取值范围是1到k的整数。
Google Guava类库使用这个技巧实现了一个布隆过滤器,哈希算法的主要逻辑如下:
long hash64 = ...;
int hash1 = (int) hash64;
int hash2 = (int) (hash64 >>> 32);
for (int i = 1; i <= numHashFunctions; i++) {
int combinedHash = hash1 + (i * hash2);
// Flip all the bits if it's negative (guaranteed positive number)
if (combinedHash < 0) {
combinedHash = ~combinedHash;
}
}
Guava中的Bloom Filter使用示例:
int expectedInsertions = ...; //待检测元素的个数
double fpp = 0.03; //误判率(desired false positive probability)
BloomFilter<CharSequence> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.forName("UTF-8")), expectedInsertions,fpp);
优点
它的优点是空间效率和查询时间都远远超过一般的算法,布隆过滤器存储空间和插入/查询时间都是常数O(k)。另外, 散列函数相互之间没有关系,方便由硬件并行实现。布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势。
缺点
布隆过滤器的缺点和优点一样明显,误算率是其中之一。
另外,一般情况下不能从布隆过滤器中删除元素。我们很容易想到把位数组变成整数数组,每插入一个元素相应的计数器加 1,这样删除元素时将计数器减掉就可以了。然而要保证安全地删除元素并非如此简单。首先我们必须保证删除的元素的确在布隆过滤器里面,而这一点单凭这个过滤器是无法保证的。
其实,用java实现bloomfilter也是很简单的,主要思想是在java的BitSet的基础上扩展一下hash函数即可。代码如下:
package bigdata.spark.distinct;
import java.util.BitSet;
public class BloomFilter {
/* BitSet初始分配2^25个bit */
private static final int DEFAULT_SIZE = 1 << 25;
/* 不同哈希函数的种子,一般应取质数 */
private static final int[] seeds = new int[]{5, 7, 11, 13, 31, 37, 61};
/* 存储海量数据使用bitset */
private BitSet bits = new BitSet(DEFAULT_SIZE);
/* 哈希函数对象用于判断元素是否存在于表中 */
private SimpleHash[] func = new SimpleHash[seeds.length];
//构造函数
public BloomFilter() {
for (int i = 0; i < seeds.length; i++) {
func[i] = new SimpleHash(DEFAULT_SIZE, seeds[i]);
}
}
// 将字符串标记到bits中
public void add(String value) {
for (SimpleHash f : func) {
bits.set(f.hash(value), true);
}
}
//判断字符串是否已经被bits标记
public boolean contains(String value) {
if (value == null) {
return false;
}
boolean ret = true;
for (SimpleHash f : func) {
ret = ret && bits.get(f.hash(value));
}
return ret;
}
/* 哈希函数类 */
public static class SimpleHash
{
//cap为hash函数的容量
private int cap;
//不同hash函数的种子
private int seed;
public SimpleHash(int cap, int seed) {
this.cap = cap;
this.seed = seed;
}
//hash函数,采用简单的加权和hash
public int hash(String value) {
int result = 0;
/* 对Value的每个字符进行hash 获取每个Hash值 */
int len = value.length();
for (int i = 0; i < len; i++) {
result = seed * result + value.charAt(i);
}
return (cap - 1) & result;
}
}
}
整理自网络:
https://blog.csdn.net/foreverling/article/details/79242968
https://blog.csdn.net/it_dx/article/details/71640504