通过一个小故事认识布隆过滤器(Bloom Filter)
某男生着几个朋友去KTV唱歌,他出示自己的身份证,KTV管理员根据身份证号“计算”出3个房间号,这伙人只能去这些房间,把灯都打开,开始K歌。(这里的“3”是假设,可能会调整)
再有客人来,KTV管理员重复上述操作。每次“计算”出来的3个房间号,几乎不可能重复,言外之意,可能重复,只是几率极低。而且,每次计算的结果都一样。如果不幸重复了,那也只能将就了。
有个女孩心急火燎地来寻找她男朋友,由于房间太多,得有数亿间(此处有夸张),逐个去查看几乎不可能。KTV管理员向她索要了她男朋友的身份证号,“计算”出3号房间号,并查看一个“神秘仪表盘”,如果房间有人,即亮灯着,通过“神秘仪表盘”一眼就能看出来。但KTV管理员对她说,如果3个房间号的灯都亮着,则她男朋友可能就在其中,也可能不在,需要她去核实下。如果3个房间的灯有一个或多个是熄灭的,她男朋友肯定不在这里,只能去别处再找找了。
布隆过滤器介绍
布隆过滤器(Bloom Filter)算法是由BURTON H. BLOOM于1970年提出的,论文见这里。主要用于解决这类问题:某个元素是否存在于一个很大很大很大的集合中。如果集合里的元素很少,方法就比较多了,所以这里强调了集合中已存在的元素个数非常多,可以简单理解为成百上千亿个,集合大小记为n。它的优点是占用的内存较少,查询时间较快,缺点是有一定的误判率,记为p,某些情况下不确定某个元素是否存在,进一步,要想删除该元素也就比较困难了。
简单地讲其实现算法:
- 初始化一个空的bit数组,数组的长度记为m,可以形象地理解为上述故事中的若干KTV房间;
- 初始化k个哈希函数,可以形象地理解为上述故事中的“计算”算法;
- 遍历成百上千亿个元素,对每个元素计算k个哈希值,并“添加到”bit数组中,可以形象地理解为上述故事中的一伙人进入KTV房间;
- 至此,准备工作已完毕;
- 当有人问“某元素”是否存在于那个bit数组中时,计算该元素的k个哈希值,判断这k个值是否存在于bit数组中:若都存在则该元素可能存在于bit数组中,言外之意,也可能不存在;若有1或多个值不存在,则该元素肯定不存在于bit数组中。可以形象地理解为上述故事中的女孩寻找男友;
- 一图胜千言:下图一分别对x、y、z计算3个Hash值,并添加到bit数组中。想判断w是否存在于bit数组中,先计算w对应的3个哈希值,这3个值有一个不存在于bit数组中,结论是:w不存在。
- 你品,你仔细品,是不是和该系列前述博客中提到的BitMap算法有相关性啊。这也是把布隆过滤和BitMap算法放在一起讲的原因。
直接上公式:
举个例子
不安全网页的黑名单已经被识别和收集了100亿个,即n等于100亿。每个网页的URL最多占用64字节。现在想要实现一种网页过滤系统,可以根据网页的URL判断该网站是否在黑名单上。要求该系统允许有万分之一以下的判断失误率,即p等于0.0001,并且使用的内存空间不要超过30G。
如果使用Set<String>存储,至少需要内存空间:100*10^8 × 64 ÷ 1024 ÷ 1024 ÷ 1024 ≈ 596G,显然,不满足要求。
套用上述公式,使用手机里的超级计数器(还不会使用LaTex数学公式:-),可算得:m=19.17*n,注意:m是没有单位的。由于代码中要创建一个大小为m的bit数组,该数组至少需要内存空间:19.17*100*10^8 ÷ 8 ÷ 1024 ÷ 1024 ÷ 1024 ≈ 22.32G,注意:除8是将bit先转换为byte。显然,满足不超过30G的要求。
k = ln(2) × 19.17*n ÷ n ≈ 13.29 ≈ 14,因为k表示哈希函数的个数,只能“天花板”取整数了。
隆过滤器的真实失误率,对入门者来说可以暂且不管,此处略。
其他类似使用场景
字处理软件中,需要检查一个英语单词是否拼写正确。据不完全统计,英语单词有100万个左右。传统思路,把所有单词录入到一个嵌入式数据库表里,建立索引。查询用户输入的拼写,查到了表明拼写正确,查不到表明拼写错误。
一个身份证号是否存在于全国失信人员数据库里。
在网络爬虫里,一个网址是否被爬取过。
某个手机号是否被标记为骚扰手机号。
代码实现
import java.util.BitSet;
/**
* 改编自:https://baike.baidu.com/item/%E5%B8%83%E9%9A%86%E8%BF%87%E6%BB%A4%E5%99%A8/5384697
*/
public class MyBloomFilter {
/**
* 约为10亿
*/
private static final int DEFAULT_SIZE = 256 << 22;
/**
* 为了降低错误率,使用加法hash算法,所以定义一个8个元素的质数数组
* 实际应用中,该数组的个数应该计算出来,此处简单设为8
*/
private static final int[] seeds = {3, 5, 7, 11, 13, 31, 37, 61};
/**
* 相当于构建 8 个不同的hash算法
* 实际应用中,该数组的个数,应该计算出来
*/
private static HashFunction[] functions = new HashFunction[seeds.length];
/**
* 初始化布隆过滤器
*/
private static BitSet bitset = new BitSet(DEFAULT_SIZE);
/**
* 添加数据
* 注意:此处和一般BitMap应用不同,虽然仅添加一个数,但却“点亮了8盏灯”
*/
public static void add(String value) {
for (HashFunction f : functions) {
bitset.set(f.hash(value));
}
}
/**
* 判断相应元素是否存在
* 若每个Hash值在BitSet中都存在,则该元素可能存在,也可能不存在
* 若每个Hash值,有一个或多个不存在于BitSet中,则该元素肯定不存在
*/
public static String contains(String value) {
for (HashFunction f : functions) {
boolean b = bitset.get(f.hash(value));
if (!b) {
return "肯定不存在:" + value;
}
}
return "可能存在,也可能不存在:" + value;
}
public static void main(String[] args) {
for (int i = 0; i < seeds.length; i++) {
functions[i] = new HashFunction(DEFAULT_SIZE, seeds[i]);
}
for (int i = 0; i < (1 * 10^8); i++) {
add(String.valueOf(i));
}
String id = "123456789";
add(id);
System.out.println(contains(id));
System.out.println(contains("234567890"));
}
}
class HashFunction {
/**
* 样本数,通常很大
*/
private int size;
/**
* 盐
*/
private int seed;
public HashFunction(int size, int seed) {
this.size = size;
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;
}
}
除了使用java.util.BitSet,当然也可以使用Google发布的EWAHCompressedBitmap,或者Guava组件中的BloomFilter类,或者Redis(注:需安装Redis插件rebloom)。Redis的实现,可以参考这篇博客及源码。
缺点&不足
- 有一定误判率
其他
看完这些内容,再读《数学之美》之“布隆过滤器”章节就很容易了。
我又看了一遍,我觉得我写的比他通俗易懂:-