【数据结构和算法】-布隆过滤器

背景

大家对哈希算法都很熟悉了,在做文件校验、唯一值生成等场景都用到了这个算法。不过,哈希算法会出现哈希冲突,在某些场景下,这是一个比较头疼的问题,就需要算法本身越来越复杂,而且也伴随着存储空间的增加。

某些场景我们可能只需要确定性的知道,某个元素肯定不存在,对存在的检测允许出现一定误判,这个时候怎么做呢?

有些小伙伴说,我用map存下来,每次检测不就好了?注意map是key、value形式,我们要检测value存在不存在,是要通过实际判定value的值,如果value本身是个基础类型,那好说。如果value是个图片呢?

有些小伙伴又说了,图片也不怕,我有MD5,是,没错,但是要知道MD5的开销是比较大的,这样数据量少还可以,如果是百万级别的数据量呢?

注意我们的场景,我们关注的是检测不存在的场景,对于存在的场景允许误判。比如缓存命中,不存在一定要去数据库查询。

这个时候,布隆过滤器登场了。

布隆过滤器 (Bloom Filter)

布隆过滤器是一种空间效率极高的 概率型数据结构,用于判断一个元素是否在一个集合中。它通过牺牲一定的准确性来换取存储空间和查询速度的优势。布隆过滤器的主要特点是:

  1. 空间效率高:相比其他数据结构,布隆过滤器占用的空间更少。
  2. 查询速度快:布隆过滤器的查询时间复杂度为 O(k),其中 k 是哈希函数的数量。
  3. 误判率:布隆过滤器可能会产生误判(false positive),即认为某个元素存在于集合中,但实际上不存在。但它不会产生漏判(false negative),即如果布隆过滤器说某个元素不在集合中,那么该元素一定不在集合中。

工作原理

  1. 初始化:布隆过滤器包含一个位数组(bit array)和多个哈希函数。
  2. 插入元素:将元素通过多个哈希函数映射到位数组中的多个位置,并将这些位置的值设为 1。
  3. 查询元素:将元素通过相同的哈希函数映射到位数组中的多个位置,检查这些位置的值是否都为 1。如果有一个位置的值为 0,则该元素肯定不在集合中;如果所有位置的值都为 1,则该元素可能在集合中。

误判率

布隆过滤器的误判率取决于以下几个因素:

  • 位数组的大小:位数组越大,误判率越低。
  • 哈希函数的数量:哈希函数越多,误判率越低。
  • 插入的元素数量:插入的元素越多,误判率越高。

应用场景

  1. 缓存系统:在缓存系统中,布隆过滤器可以用来快速判断某个数据是否存在于缓存中,减少不必要的缓存查找。
  2. 数据库:在数据库中,布隆过滤器可以用来快速判断某个记录是否存在,减少磁盘 I/O 操作。
  3. 网络爬虫:在网络爬虫中,布隆过滤器可以用来判断某个 URL 是否已经被抓取过,避免重复抓取。
  4. 垃圾邮件过滤:在垃圾邮件过滤系统中,布隆过滤器可以用来快速判断某个邮件地址是否在黑名单中。

示例代码

以下是一个简单的布隆过滤器实现示例:

java 复制代码
import java.util.BitSet;

public class BloomFilter {
    private final BitSet bitSet;
    private final int size;
    private final int hashFunctions;

    public BloomFilter(int size, int hashFunctions) {
        this.size = size;
        this.hashFunctions = hashFunctions;
        this.bitSet = new BitSet(size);
    }

    public void add(String item) {
        for (int i = 0; i < hashFunctions; i++) {
            int hash = hash(item, i);
            bitSet.set(hash % size, true);
        }
    }

    public boolean mightContain(String item) {
        for (int i = 0; i < hashFunctions; i++) {
            int hash = hash(item, i);
            if (!bitSet.get(hash % size)) {
                return false;
            }
        }
        return true;
    }

    private int hash(String item, int seed) {
        int hash = 0;
        for (int i = 0; i < item.length(); i++) {
            hash = (hash * seed) + item.charAt(i);
        }
        return Math.abs(hash);
    }

    public static void main(String[] args) {
        BloomFilter bloomFilter = new BloomFilter(1000000, 7);

        bloomFilter.add("Apple");
        bloomFilter.add("Banana");
        bloomFilter.add("Cherry");

        System.out.println(bloomFilter.mightContain("Apple")); // 输出: true
        System.out.println(bloomFilter.mightContain("Date"));  // 输出: false
    }
}

代码解释

  1. BitSet:用于存储位数组。
  2. 构造函数:初始化位数组的大小和哈希函数的数量。
  3. add 方法:将元素通过多个哈希函数映射到位数组中的多个位置,并将这些位置的值设为 1。
  4. mightContain 方法:将元素通过相同的哈希函数映射到位数组中的多个位置,检查这些位置的值是否都为 1。如果有一个位置的值为 0,则该元素肯定不在集合中;如果所有位置的值都为 1,则该元素可能在集合中。
  5. hash 方法:实现一个简单的哈希函数,根据种子值生成不同的哈希值。

注意事项

  1. 误判率:布隆过滤器的误判率可以通过调整位数组的大小和哈希函数的数量来控制。
  2. 不可删除:布隆过滤器不支持删除操作,因为删除一个元素可能会误删其他元素的标记。
  3. 选择合适的参数:根据实际需求选择合适的位数组大小和哈希函数数量,以平衡空间和误判率。

问题

1.为啥用多个哈希函数,怎么映射的?

详细解释
1. 位数组 (BitSet)

布隆过滤器的核心是一个位数组(通常使用 BitSet 表示)。位数组的每个位置可以是 0 或 1。初始状态下,所有位置的值都是 0。

2. 哈希函数

布隆过滤器使用多个哈希函数将元素映射到位数组中的多个位置。每个哈希函数会将输入的元素转换为一个整数,这个整数作为位数组的一个索引。

3. 映射过程

假设我们有一个布隆过滤器,位数组的大小为 m,使用 k 个哈希函数。当我们向布隆过滤器中添加一个元素时,具体步骤如下:

  1. 计算哈希值 :使用每个哈希函数对元素进行哈希运算,得到 k 个哈希值。
  2. 映射到位数组 :将这 k 个哈希值分别取模 m,得到 k 个索引。
  3. 设置位数组 :将位数组中对应这 k 个索引的位置的值设为 1。
具体示例

假设我们有一个布隆过滤器,位数组的大小为 10,使用 3 个哈希函数。我们要添加元素 "Apple"

  1. 计算哈希值

    • 使用第一个哈希函数 hash1 计算 "Apple" 的哈希值,假设结果为 15。
    • 使用第二个哈希函数 hash2 计算 "Apple" 的哈希值,假设结果为 23。
    • 使用第三个哈希函数 hash3 计算 "Apple" 的哈希值,假设结果为 31。
  2. 映射到位数组

    • 将 15 取模 10,得到索引 5。
    • 将 23 取模 10,得到索引 3。
    • 将 31 取模 10,得到索引 1。
  3. 设置位数组

    • 将位数组中索引 5、3 和 1 的位置的值设为 1。

初始状态的位数组(所有位置为 0):

[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

添加 "Apple" 后的位数组:

[0, 1, 0, 1, 0, 1, 0, 0, 0, 0]
查询过程

当我们查询一个元素是否在布隆过滤器中时,步骤如下:

  1. 计算哈希值 :使用相同的 k 个哈希函数对元素进行哈希运算,得到 k 个哈希值。
  2. 映射到位数组 :将这 k 个哈希值分别取模 m,得到 k 个索引。
  3. 检查位数组 :检查位数组中对应这 k 个索引的位置的值是否都为 1。如果有一个位置的值为 0,则该元素肯定不在集合中;如果所有位置的值都为 1,则该元素可能在集合中。

这样就更清晰了吧?布隆过滤器的思想就是,1个哈希容易冲突,那就上多个哈希函数,即使1个位上冲突了,其他位冲突的概率也非常小。举个不恰当的例子,就像我们产品上线前要经过多轮测试一样,一轮测试可能会泄露很多BUG,但是多轮测试一定会将BUG数量减少。

2.布隆过滤器可以用于缓存,但是如果误判为存在,本身不存在,这样不就出问题了吗?

这个问题非常有道理。确实,布隆过滤器的误判特性可能会导致一些问题,特别是在缓存系统中。让我们详细探讨一下这个问题,并讨论如何在实际应用中处理误判。

分析
1. 基本原理

在缓存系统中,布隆过滤器可以用来快速判断某个数据是否存在于缓存中。具体步骤如下:

  1. 查询布隆过滤器:首先通过布隆过滤器判断数据是否可能存在。
  2. 查询缓存 :如果布隆过滤器返回 true,则进一步查询缓存。
  3. 查询后端存储:如果缓存中没有找到数据,则查询后端存储(如数据库)。
2. 误判的影响
  • 误判为存在:如果布隆过滤器误判为存在,但实际上数据不在缓存中,会导致额外的缓存查询。虽然这会增加一些开销,但不会导致数据错误,因为最终会查询后端存储来获取数据。
  • 漏判 :布隆过滤器不会漏判,即如果布隆过滤器返回 false,则数据一定不在缓存中。
处理误判的方法
1. 优化布隆过滤器参数
  • 调整位数组大小:增加位数组的大小可以降低误判率。
  • 调整哈希函数数量:增加哈希函数的数量也可以降低误判率,但过多的哈希函数会增加计算开销。
2. 结合其他数据结构
  • 二级缓存:可以使用一个小型的精确缓存(如 LRU 缓存)与布隆过滤器结合使用。先通过布隆过滤器快速过滤掉大部分不存在的数据,再通过精确缓存确认数据是否存在。
  • 计数布隆过滤器:计数布隆过滤器允许删除操作,可以减少误判率。

我们在实际中常用的可能是二级缓存,看下例子:

实际应用示例

假设我们有一个缓存系统,使用布隆过滤器来判断数据是否存在于缓存中。

java 复制代码
import java.util.HashMap;
import java.util.Map;
import java.util.BitSet;

public class CacheSystem {
    private final BloomFilter bloomFilter;
    private final Map<String, String> cache;
    private final Map<String, String> backendStorage;

    public CacheSystem(int bloomFilterSize, int hashFunctions) {
        this.bloomFilter = new BloomFilter(bloomFilterSize, hashFunctions);
        this.cache = new HashMap<>();
        this.backendStorage = new HashMap<>();
    }

    public void addToCache(String key, String value) {
        cache.put(key, value);
        bloomFilter.add(key);
    }

    public String getFromCache(String key) {
        if (!bloomFilter.mightContain(key)) {
            return null; // 数据肯定不在缓存中
        }

        // 查询缓存
        String value = cache.get(key);
        if (value != null) {
            return value;
        }

        // 查询后端存储
        value = backendStorage.get(key);
        if (value != null) {
            addToCache(key, value); // 将数据加入缓存
        }

        return value;
    }

    public void addToBackendStorage(String key, String value) {
        backendStorage.put(key, value);
    }

    public static void main(String[] args) {
        CacheSystem cacheSystem = new CacheSystem(1000000, 7);

        cacheSystem.addToBackendStorage("Apple", "Fruit");
        cacheSystem.addToBackendStorage("Banana", "Fruit");
        cacheSystem.addToBackendStorage("Carrot", "Vegetable");

        cacheSystem.addToCache("Apple", "Fruit");
        cacheSystem.addToCache("Banana", "Fruit");

        System.out.println(cacheSystem.getFromCache("Apple")); // 输出: Fruit
        System.out.println(cacheSystem.getFromCache("Banana")); // 输出: Fruit
        System.out.println(cacheSystem.getFromCache("Carrot")); // 输出: Vegetable
        System.out.println(cacheSystem.getFromCache("Date")); // 输出: null
    }
}
代码解释
  1. BloomFilter 类:实现了布隆过滤器的基本功能。
  2. CacheSystem 类
    • bloomFilter:用于快速判断数据是否可能存在。
    • cache:用于存储缓存数据。
    • backendStorage:用于模拟后端存储。
    • addToCache 方法:将数据添加到缓存和布隆过滤器中。
    • getFromCache 方法:先通过布隆过滤器判断数据是否可能存在,如果可能,则查询缓存;如果缓存中没有,再查询后端存储,并将数据加入缓存。

问题是不是解决啦?关键不是代码,其实学习这个思想,把它内化成我们解决问题的思路是更重要的。

划重点

布隆过滤器的主要优势在于其高效的查询速度和较低的空间占用,适用于大规模数据的快速过滤。不要滥用哦。

相关推荐
ZSYP-S1 小时前
Day 15:Spring 框架基础
java·开发语言·数据结构·后端·spring
yuanbenshidiaos1 小时前
C++----------函数的调用机制
java·c++·算法
唐叔在学习1 小时前
【唐叔学算法】第21天:超越比较-计数排序、桶排序与基数排序的Java实践及性能剖析
数据结构·算法·排序算法
ALISHENGYA1 小时前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(switch语句)
数据结构·算法
chengooooooo1 小时前
代码随想录训练营第二十七天| 贪心理论基础 455.分发饼干 376. 摆动序列 53. 最大子序和
算法·leetcode·职场和发展
jackiendsc2 小时前
Java的垃圾回收机制介绍、工作原理、算法及分析调优
java·开发语言·算法
游是水里的游3 小时前
【算法day20】回溯:子集与全排列问题
算法
yoyobravery3 小时前
c语言大一期末复习
c语言·开发语言·算法
Jiude3 小时前
算法题题解记录——双变量问题的 “枚举右,维护左”
python·算法·面试
被AI抢饭碗的人3 小时前
算法题(13):异或变换
算法