布隆过滤器是 1970 年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。主要用于判断一个元素是否在一个集合中。
通常我们会遇到很多要判断一个元素是否在某个集合中的业务场景,一般想到的是将集合中所有元素保存起来,然后通过比较确定。链表、树、散列表(又叫哈希表,Hash table)等等数据结构都是这种思路。但是随着集合中元素的增加,我们需要的存储空间也会呈现线性增长,最终达到瓶颈。同时检索速度也越来越慢,上述三种结构的检索时间复杂度分别为O ( n ) O(n)O(n),O ( l o g n ) O(logn)O(logn),O ( 1 ) O(1)O(1)。
这个时候,布隆过滤器(Bloom Filter)就应运而生。


布隆过滤器的误判是指多个输入经过哈希之后在相同的bit位置1了,这样就无法判断究竟是哪个输入产生的,因此误判的根源在于相同的 bit 位被多次映射且置 1。
这种情况也造成了布隆过滤器的删除问题,因为布隆过滤器的每一个 bit 并不是独占的,很有可能多个元素共享了某一位。如果我们直接删除这一位的话,会影响其他的元素。(比如上图中的第 3 位)
相比于其它的数据结构,布隆过滤器在空间和时间方面都有巨大的优势。布隆过滤器存储空间和插入/查询时间都是常数 O ( K ) O(K)O(K),另外,散列函数相互之间没有关系,方便由硬件并行实现。布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势。
布隆过滤器可以表示全集,其它任何数据结构都不能;
但是布隆过滤器的缺点和优点一样明显。误算率是其中之一。随着存入的元素数量增加,误算率随之增加。但是如果元素数量太少,则使用散列表足矣。
另外,一般情况下不能从布隆过滤器中删除元素。我们很容易想到把位数组变成整数数组,每插入一个元素相应的计数器加 1, 这样删除元素时将计数器减掉就可以了。然而要保证安全地删除元素并非如此简单。首先我们必须保证删除的元素的确在布隆过滤器里面。这一点单凭这个过滤器是无法保证的。另外计数器回绕也会造成问题。
在降低误算率方面,有不少工作,使得出现了很多布隆过滤器的变种。
在程序的世界中,布隆过滤器是程序员的一把利器,利用它可以快速地解决项目中一些比较棘手的问题。
如网页 URL 去重、垃圾邮件识别、大集合中重复元素的判断和缓存穿透等问题。
布隆过滤器的典型应用有:
- import java.util.BitSet;
-
- public class MyBloomFilter {
-
- /**
- * 位数组的大小
- */
- private static final int DEFAULT_SIZE = 2 << 24;
- /**
- * 通过这个数组可以创建 6 个不同的哈希函数
- */
- private static final int[] SEEDS = new int[]{3, 13, 46, 71, 91, 134};
-
- /**
- * 位数组。数组中的元素只能是 0 或者 1
- */
- private BitSet bits = new BitSet(DEFAULT_SIZE);
-
- /**
- * 存放包含 hash 函数的类的数组
- */
- private SimpleHash[] func = new SimpleHash[SEEDS.length];
-
- /**
- * 初始化多个包含 hash 函数的类的数组,每个类中的 hash 函数都不一样
- */
- public MyBloomFilter() {
- // 初始化多个不同的 Hash 函数
- for (int i = 0; i < SEEDS.length; i++) {
- func[i] = new SimpleHash(DEFAULT_SIZE, SEEDS[i]);
- }
- }
-
- /**
- * 添加元素到位数组
- */
- public void add(Object value) {
- for (SimpleHash f : func) {
- bits.set(f.hash(value), true);
- }
- }
-
- /**
- * 判断指定元素是否存在于位数组
- */
- public boolean contains(Object value) {
- boolean ret = true;
- for (SimpleHash f : func) {
- ret = ret && bits.get(f.hash(value));
- }
- return ret;
- }
-
- /**
- * 静态内部类。用于 hash 操作!
- */
- public static class SimpleHash {
-
- private int cap;
- private int seed;
-
- public SimpleHash(int cap, int seed) {
- this.cap = cap;
- this.seed = seed;
- }
- /**
- * 计算 hash 值
- */
- public int hash(Object value) {
- int h;
- return (value == null) ? 0 : Math.abs(seed * (cap - 1) & ((h = value.hashCode()) ^ (h >>> 16)));
- }
- }
- }
- class too{
- public static void main(String[] args) {
- String value1 = "https://javaguide.cn/";
- String value2 = "https://github.com/Snailclimb";
- MyBloomFilter filter = new MyBloomFilter();
- System.out.println(filter.contains(value1));
- System.out.println(filter.contains(value2));
- filter.add(value1);
- filter.add(value2);
- System.out.println(filter.contains(value1));
- System.out.println(filter.contains(value2));
- }
- }
测试结果
