海量数据查找之Bloom Filter算法

简介

  布隆过滤器(Bloom filter)是一个高空间利用率的概率性数据结构,由Burton Bloom于1970年提出。被用于测试一个元素是否在集合中(由于集合无重复元素的性质,可用来判重)。
  可在数据量大到传统无错误散列(hash)方法需要使用的内存量是不可满足时使用,传统无错散列方法可以消除所有无用的磁盘访问,同时需要使用的内存量也非常大,而布隆过滤器在有限的内存使用量下依旧可以排除大部分无用的磁盘访问。

特性

  • 存在假阳性(将不在集合中的元素误判为在集合中),不存在假阴性(将在集合中的元素误判为不在集合中)。过滤器中的元素个数越多,假阳性的可能性越大,总的来说,当不考虑集合中元素个数的情况下,每个元素由10个以下的bit来表示就可以保证1%以内的假阳性概率。
  • 元素可以被加入过滤器,但不可从过滤器中删除(因为删除的时候有可能会影响到其他元素,之后会细说)。

空间和时间优势

  • 布隆过滤器不需要存储数据项,但是同时它需要在其他地方单独存储真正的数据项。对于一个拥有最优k值且误判率在1%的布隆过滤器,每个元素只需要9.6bits(与元素的大小无关)。这个优点一部分继承自数组的紧凑性,另一方面由它本身的概率性决定。若给每个元素增加4.8bits左右,误判率将会减少十倍。
  • 布隆过滤器在添加和查找元素时,所需要的时间时一个常数,O(k),完全与集合中元素个数无关。没有其他固定空间的集合数据结构有这样的效率,但是对于稀疏散列表来说,平均访问时长在实际使用中比一些布隆过滤器要短。在硬件实现方式中,布隆过滤器的优势在于他的k个查询之间不相关,因此可以并行处理。

算法描述

  一个空的布隆过滤器是一串被置为0的bit数组(假设由m位)。同时,应该声明k个不同的散列函数生成一个统一随机分布,每一个散列函数都将元素映射到m个bit中的一个(k是一个小于m的常数,与加入过滤器中的元素个数成比例)。k与相应的m的选择由误判率决定。
  向过滤器中添加元素时,通过k个散列函数得到该元素对应的k个位置,并将这些位置置为1。查询某个元素/测试是否与已有元素重复时,依旧通过k个散列函数得到对应的k个位置,判断这些位置是否为1(若全为1则在集合内/重复)

如下图,其中,{x,y,z}为集合,w为进行比对的元素,m=18,k=3,不同颜色的箭头表示散列映射关系。可以看出,w并不在{x,y,z}这个集合中。

  1. 当k比较大时,设计k个不同且无相关的散列函数是不现实的。对于一些输出的位数较多的优秀的散列函数(优秀指不同bit区间之间联系很小),我们可以将其切割成多个bit区域来代替多个散列函数。或者我们可以传递k个不同的值(例如:0,1,2,…,k-1)到一个散列函数中对其进行初始化;或者将这k个值整合到待计算元素中,再进行计算。对于较大的m,k,无相关性的散列函数可以使误判率的增加量减少。
  2. 从过滤器中删除元素是不可能的,因为有可能删除的当前元素与其他元素共享了某一个bit。当置该bit为0时,就会产生假阴性,这是绝对不允许的。
  3. 想要保持误判率低,过滤器的空间使用率(bit数组中置为1的概率)应为50%左右

数组长度-m值的选取

对于给定的p(误判率)和将要加入集合的元素个数n,m由如下公式定义:

hash次数-k值的选取
对于给定的m和n,使得假阳性率(误判率)最小的k通过如下公式定义:

应用场景

  1. 网络爬虫中,判断一个url是否访问过
  2. 电子邮件过滤,判断是否黑名单邮件
  3. 海量数据统计,非hadoop环境下,计算亿级deviceid数量

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import java.util.BitSet;

public class BloomFilter {
/* BitSet初始分配2^24个bit */
private static final int DEFAULT_SIZE = 1 << 25;

/* 不同哈希函数的种子,一般应取质数 */
private static final int[] seeds = new int[]{5, 7, 11, 13, 31, 37, 61};

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 {
private int cap;
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;
int len = value.length();
for (int i = 0; i < len; i++) {
result = seed * result + value.charAt(i);
}
return (cap - 1) & result;
}
}
}
JouyPub wechat
欢迎订阅「K叔区块链」 - 专注于区块链技术学习