现在有一个非常庞大的数据,假设全是 int 类型。现在我给你一个数,你需要告诉我它是否存在其中(尽量高效)。
需求分析:只是要判断一个数据是否存在即可。但这里有一个比较重要的前提是非常庞大的数据。
常规实现
我想大多数想到的都是用 HashMap 来存放数据,因为它的写入查询的效率都比较高。
写入和判断元素是否存在都有对应的 API,所以实现起来也比较简单。
为此我写了一个单测,利用 HashSet 来存数据(底层也是 HashMap );同时为了后面的对比将堆内存写死:
1 | -Xms64m -Xmx64m -XX:+PrintHeapAtGC -XX:+HeapDumpOnOutOfMemoryError |
为了方便调试加入了 GC 日志的打印,以及内存溢出后 Dump 内存。
1 |
|
当我只写入 100 条数据时自然是没有问题的。
还是在这个基础上,写入 1000W 数据试试:

执行后马上就内存溢出。

可见在内存有限的情况下我们不能使用这种方式。
实际情况也是如此;既然要判断一个数据是否存在于集合中,考虑的算法的效率以及准确性肯定是要把数据全部 load 到内存中的。
Bloom Filter 原理
官方:它是一个保存了很长的二级制向量,同时结合
Hash函数实现的。

如图所示:
- 首先需要初始化一个二进制的数组,长度设为 L(图中为 8),同时初始值全为 0 。
- 当写入一个
A1=1000的数据时,需要进行 H 次hash函数的运算(这里为 2 次);与HashMap有点类似,通过算出的HashCode与 L 取模后定位到 0、2 处,将该处的值设为 1。 A2=2000也是同理计算后将 4、7 位置设为 1。- 当有一个
B1=1000需要判断是否存在时,也是做两次Hash运算,定位到 0、2 处,此时他们的值都为 1 ,所以认为B1=1000存在于集合中。 - 当有一个
B2=3000时,也是同理。第一次Hash 定位到index=4时,数组中的值为 1,所以再进行第二次Hash运算,结果定位到index=5的值为 0,所以认为B2=3000不存在于集合中。
布隆过滤有以下几个特点:
- 只要返回数据不存在,则肯定不存在。
- 返回数据存在,但只能是大概率存在。
- 同时不能清除其中的数据。
基于以上的 Hash 冲突的前提,所以 Bloom Filter 有一定的误报率,这个误报率和 Hash算法的次数 H,以及数组长度 L 都是有关的。
自己实现一个布隆过滤
1 | public class BloomFilters { |
- 首先初始化了一个 int 数组。
- 写入数据的时候进行三次
hash运算,同时把对应的位置置为 1。 - 查询时同样的三次
hash运算,取到对应的值,一旦值为 0 ,则认为数据不存在。
下面来测试一下,同样的参数:
1 | -Xms64m -Xmx64m -XX:+PrintHeapAtGC |
1 |
|
执行结果如下:

只花了 3 秒钟就写入了 1000W 的数据同时做出来准确的判断。
总结
布隆过滤常应用在数据库、爬虫、Redis防缓存击穿等。