1. Bloom Filter算法简介
Bloom-Filter,即布隆过滤器,1970年由Bloom中提出。它可以用于检索一个元素是否在一个集合中。
Bloom Filter(BF)是一种空间效率很高的随机数据结构,它利用位数组很简洁地表示一个集合,并能判断一个元素是否属于这个集合。它是一个判断元素是否存在集合的快速的概率算法。Bloom Filter有可能会出现错误判断,但不会漏掉判断。也就是Bloom Filter判断元素不再集合,那肯定不在。如果判断元素存在集合中,有一定的概率判断错误。因此,Bloom Filter”不适合那些“零错误的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter比其他常见的算法(如hash,折半查找)极大节省了空间。
它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
2. Bloom Filter基本思想
Bloom-Filter算法的核心思想就是利用多个不同的Hash函数来解决“冲突”。
计算某元素x是否在一个集合中,首先能想到的方法就是将所有的已知元素保存起来构成一个集合R,然后用元素x跟这些R中的元素一一比较来判断是否存在于集合R中;我们可以采用链表等数据结构来实现。但是,随着集合R中元素的增加,其占用的内存将越来越大。试想,如果有几千万个不同网页需要下载,所需的内存将足以占用掉整个进程的内存地址空间。即使用MD5,UUID这些方法将URL转成固定的短小的字符串,内存占用也是相当巨大的。
于是,我们会想到用Hash table的数据结构,运用一个足够好的Hash函数将一个URL映射到二进制位数组(位图数组)中的某一位。如果该位已经被置为1,那么表示该URL已经存在。
Hash存在一个冲突(碰撞)的问题,用同一个Hash得到的两个URL的值有可能相同。为了减少冲突,我们可以多引入几个Hash,如果通过其中的一个Hash值我们得出某元素不在集合中,那么该元素肯定不在集合中。只有在所有的Hash函数告诉我们该元素在集合中时,才能确定该元素存在于集合中。这便是Bloom-Filter的基本思想。
1)位数组:
假设Bloom Filter使用一个m比特的数组来保存信息,初始状态时,Bloom Filter是一个包含m位的位数组,每一位都置为0,即BF整个数组的元素都设置为0。
2)添加元素,k个独立hash函数
为了表达S={x1, x2,…,xn}这样一个n个元素的集合,Bloom Filter使用k个相互独立的哈希函数(Hash Function),它们分别将集合中的每个元素映射到{1,…,m}的范围中。
当我们往Bloom Filter中增加任意一个元素x时候,我们使用k个哈希函数得到k个哈希值,然后将数组中对应的比特位设置为1。即第i个哈希函数映射的位置hashi(x)就会被置为1(1≤i≤k)。
注意,如果一个位置多次被置为1,那么只有第一次会起作用,后面几次将没有任何效果。在下图中,k=3,且有两个哈希函数选中同一个位置(从左边数第五位,即第二个“1“处)。
3)判断元素是否存在集合
在判断y是否属于这个集合时,我们只需要对y使用k个哈希函数得到k个哈希值,如果所有hashi(y)的位置都是1(1≤i≤k),即k个位置都被设置为1了,那么我们就认为y是集合中的元素,否则就认为y不是集合中的元素。下图中y1就不是集合中的元素(因为y1有一处指向了“0”位)。y2或者属于这个集合,或者刚好是一个false positive。
一个Bloom Filter有以下参数:
m | bit数组的宽度(bit数) |
n | 加入其中的key的数量 |
k | 使用的hash函数的个数 |
f | False Positive的比率 |
Bloom Filter的f满足下列公式:
在给定m和n时,能够使f最小化的k值为:
此时给出的f为:
根据以上公式,对于任意给定的f,我们有:
n = m ln(0.6185) / ln(f)
同时,我们需要k个hash来达成这个目标:
k = - ln(f) / ln(2)
由于k必须取整数,我们在Bloom Filter的程序实现中,还应该使用上面的公式来求得实际的f:
f = (1 – e-kn/m)k
以上3个公式是程序实现Bloom Filter的关键公式。
4. BloomFilter的应用
1.黑名单
比如邮件黑名单过滤器,判断邮件地址是否在黑名单中
2.排序(仅限于BitSet)
仔细想想,其实BitSet在set(int value)的时候,“顺便”把value也给排序了。
3.网络爬虫
判断某个URL是否已经被爬取过
4.K-V系统快速判断某个key是否存在
典型的例子有Hbase,Hbase的每个Region中都包含一个BloomFilter,用于在查询时快速判断某个key在该region中是否存 在,如果不存在,直接返回,节省掉后续的查询。
4. JavaAPI
public class BloomFilter {
private static final int BIT_SIZE = 2 << 28 ;//二进制向量的位数,相当于能存储1000万条url左右,误报率为千万分之一
private static final int[] seeds = new int[]{3, 5, 7, 11, 13, 31, 37, 61};//用于生成信息指纹的8个随机数,最好选取质数
private BitSet bits = new BitSet(BIT_SIZE);
private Hash[] func = new Hash[seeds.length];//用于存储8个随机哈希值对象
public BloomFilter2(){
for(int i = 0; i < seeds.length; i++){
func[i] = new Hash(BIT_SIZE, seeds[i]);
}
}
/**
* 像过滤器中添加字符串
*/
public void addValue(String value)
{
//将字符串value哈希为8个或多个整数,然后在这些整数的bit上变为1
if(value != null){
for(Hash f : func)
bits.set(f.hash(value), true);
}
}
/**
* 判断字符串是否包含在布隆过滤器中
*/
public boolean contains(String value)
{
if(value == null)
return false;
boolean ret = true;
//将要比较的字符串重新以上述方法计算hash值,再与布隆过滤器比对
for(Hash f : func)
ret = ret && bits.get(f.hash(value));
return ret;
}
/**
* 随机哈希值对象
*/
public static class Hash{
private int size;//二进制向量数组大小
private int seed;//随机数种子
public Hash(int cap, int seed){
this.size = cap;
this.seed = seed;
}
/**
* 计算哈希值(也可以选用别的恰当的哈希函数)
*/
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 (size - 1) & result;
}
}
public static void main(String[] args) {
String[] values = {
"www.baidu.com", "www.iqiyi.com", "weibo.com", "mail.163.com", "www.baidu.com""
};
BloomFilter2 bf = new BloomFilter2();
for (int i = 0; i < values.length; i++) {
System.out.println("\nURL: " + values[i]);
System.out.println("加入前");
System.out.println("是否已经存在:" + bf.contains(values[i]));
bf.addValue(values[i]);
System.out.println("加入后");
System.out.println("是否已经存在:" + bf.contains(values[i]));
}
// Arrays.stream(values).forEach(str -> {
// for(int i = 0; i < str.length(); i++){
// System.out.println(str+": hashcode"+i+":"+(int)(str.charAt(i)));
// }
// });
}
}