布隆过滤器原理深度剖析

1970年,布隆提出布隆过滤器(BloomFilter),用于判断一个元素是否不在一个集合中,但是不能精确确定元素在集合中。

通常情况下,要确定一个元素是否存在于某个业务场景的集合,一般是将元素保存到集合中,然后通过比较确定,例如采用链表、树、散列表(又叫哈希表,Hash Table)等数据结构。但是,随着集合中元素的增加,需要的存储空间也会呈现线性增长,最终达到瓶颈,同时检索速度也越来越慢。因此,为了解决这种时间和空间性能问题,引入了布隆过滤器。

1. 数据结构与原理

BloomFilter是由一个固定大小的二进制向量或者位图(Bitmap),和一系列映射函数组成。

1.1 初始化

初始状态时,需要指定位图(数组)长度(N),并将所有位都都置为0
在这里插入图片描述
上图所示,假设N=18,则初始化Array=00 0000 0000 0000 0000

1.2 变量映射

布隆过滤器映射需要指定映射次数K,即变量需要映射到位图的K个点上,并将其位置值设置为1。
原理
上图所示,假设位图长度N=18,映射次数K=3,哈希计算函数分别为hash1hash2hash3

那么,对于元素A={x, y, z}加入布隆过滤器,则:

元素第一次映射位置第二次映射位置第三次映射位置
xhash1(x) % N = 1hash2(x) % N = 5hash3(x) % N = 13
yhash1(y) % N = 4hash2(y) % N = 11hash3(y) % N = 16
zhash1(z) % N = 3hash2(z) % N = 5hash3(z) % N = 11

最终,得到布隆过滤器串为:01 0111 0000 0101 0010

1.3 变量检索

此时,元素w加入,需要判断是否不存在。首先,经过K次映射计算其所在布隆过滤器位置。

元素第一次映射位置第二次映射位置第三次映射位置
whash1(x) % N = 4hash2(x) % N = 13hash3(x) % N = 15

其次,读取映射位置所在元素值,可以发现第三次映射位置所在值为0,那么判定元素w不存在于过滤器中。

1.4 总结

某个变量检索时,只要判断映射点值是否都为1,可以大概率判断是否存在于集合中。

  • 如果映射点有任何一个0,则被检索变量一定不存在;
  • 如果都是1,则被检索变量很可能存在。

思考:为什么说是可能存在,而不是一定存在呢?那是因为映射函数本身就是散列函数,散列函数是会有碰撞的。

2. 过滤器特性

2.1 误判率

布隆过滤器误判:多个键经过映射后在相同bit位设置为1,导致无法判断究竟是哪个输入产生的,因此误判的根源在于相同bi 位被多次映射且设置为1。

这种情况也造成了布隆过滤器的删除存在问题,因为布隆过滤器每个bit并不是独占的,很有可能多个元素共享了某一位。如果我们直接删除这一位的话,会影响其他的元素。

2.2 判断特点

  • 一个元素如果判断结果为存在的时候元素不一定存在,但是判断结果为不存在的时候则一定不存在。
  • 布隆过滤器可以添加元素,但是不能删除元素。因为删掉元素会导致误判率增加。

3. 案列代码

package org.apache.ithink;

/**
 * bloom filter
 * @author ithink/guojl.szu@hotmail.com
 * @date 2021-09-04
 */
public class BloomFilter {
  /**
   * 映射次数
   */
  private int k;
  /**
   * 每个Key占用字节数
   */
  private int bitsPerKey;
  /**
   * 位图大小
   */
  private int bitSize;
  /**
   * 底层存储位图
   */
  private byte[] array;

  public BloomFilter(int k, int bitsPerKey) {
    this.k = k;
    this.bitsPerKey = bitsPerKey;
  }

  /**
   * 生成键值的位图
   * @param keys 键
   * @return 键对应位图
   */
  public byte[] generate(String[] keys) {
    // 计算所有Key占用位数
    int actualBitSize = keys.length * bitsPerKey;
    // 由于每个字节占用8位
    // bitSize + 7 / 8: 避免bitSize < 8, 导致bitSize / 8为0
    // ((bitSize + 7) / 8) << 3: 保证位数是8的整数倍
    int middleBitSize = ((actualBitSize + 7) / 8) << 3; 
    // Key最少占用64位
    int bitSize = middleBitSize < 64 ? 64 : middleBitSize;
    // 创建位图, 因为byte是8位, 所以需要将所有位数除以8
    array = new byte[bitSize >> 3];

	// 保存位图占用大小
	this.bitSize = bitSize;
    
    // 循环处理
    for (int i = 0; i < keys.length; i++) {
      // 生成Hash值
      int hashCode = Bytes.hash(keys[i].getBytes());
      // 循环映射K次
      for (int t = 0; t < k; t++) {
        // 计算所在位图位置
        // hashCode % bitSize + bitSize: 保证取模之后不为负数
        // (hashCode % bitSize + bitSize) % bitSize: 有可能括号内超过bitSize, 
        //                                           所以需要再次取模计算
        int position = (hashCode % bitSize + bitSize) % bitSize;
        // 由于每个字节占用8位
        // position % 8: 计算位置所在当前字节第几位
        // (1 << (position % 8)): 将所在字节位值设置为1
        // position / 8: 计算position所在字节位置
        array[position / 8] |= (1 << (position % 8));
        // 将hashCode前17位和后15位进行位置切换
        int delta = (hashCode >> 17) | (hashCode << 15);
        // 保证每次计算的hashCode不同
        hashCode += delta;
      }
    }
    return array;
  }

  /**
   * 判断Key是否存在
   * @param key 键
   * @return true:存在, false:不存在
   */
  public boolean contains(String key) {
    // 生成哈希值
    int hashCode = Bytes.hash(key.getBytes());
    // 循环映射K次
    for (int t = 0; t < k; t++) {
      // 计算所在位图位置
      int position = (hashCode % bitSize + bitSize) % bitSize;
      // 如果只要一个位置值为0, 则表示不存在
      if ((array[position / 8] & (1 << (position % 8))) == 0) {
        return false;
      }
      // 将hashCode前17位和后15位进行位置切换
      int delta = (hashCode >> 17) | (hashCode << 15);
      hashCode += delta;
    }
    
    return true;
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值