布隆过滤器原理及简单实现

原理

布隆过滤器,英文BloomFilter,是一个时间复杂度和空间复杂度很低,并用来检测元素是否存在的一种数据结构。它本质上是一个位图,把元素通过多次的hash计算出来的值当作索引,如果索引对应的位图的二进制位为0,说明该元素不存在,如果都为1,该元素可能存在。“可能存在"我们称为布隆过滤器的误判率,这是由于hash冲突导致的。从这可以看出来,布隆过滤器适用与对大数据集进行去重、检验是否存在等场景,但由于它的误判率,并不适用"零错误的场景”。下面我们来看看它的原理。

在这里插入图片描述

上图就是布隆过滤器的原理,中间就是bitmap,每一位都是二进制位。然后上面的5和7就是已经存在的元素,他们分别经过3次hash运算,将bitmap对应的位改为了1。下面的10是一个不存在的元素,但是由于hash冲突,导致它计算出来的3个位都为1,这就会让布隆过滤器发生误判。由此可以得知,误判率上升有以下几种因素:1.bitmap太小,导致频繁的hash冲突;2.本身存在的元素过多,导致bitmap大多数位都为1;3.hash函数散列较差。并且可以发现,布隆过滤器是不支持删除元素的,如果将5删除,那么下标为3的位就会变为0,这样也会把7进行"误删"。

简单实现

上面了解过布隆过滤器的原理后,那我们就自己来实现一个简单版的。我才用了long数组做为bitmap,在java中,long为64位,也就是说,当数组长度为10时,bitmap可以表示640位。

public class BloomFilter<T> {
	
    //bitmap
    private long bitmap[];
    //bitmap的真实长度,数组长度 * 64
    private int realLength;

    public BloomFilter(int length){
        this.bitmap = new long[length];
        //左移6位相当于 *64
        this.realLength = length << 6;
    }

    //向布隆过滤器添加key
    public void add(T key){
        //计算当前key在整个bitmap中(640位)的索引
        int index1 = Math.abs(hash1(key) % this.realLength);
        int index2 = Math.abs(hash2(key) % this.realLength);
        int index3 = Math.abs(hash3(key) % this.realLength);
        location(index1);
        location(index2);
        location(index3);
    }
    
    private void location(int index){
        //这里计算出来的i,相当于在bitmap数组中的哪个桶位
        //因为我们最终是要去桶位的long上进行修改,所以要先计算出桶位
        int i = index >>> 6;
        //这里就是计算出bitmap[i]的具体二进制位
        int j = index & (64 - 1);
        //将具体的那一位改为1
        bitmap[i] |= 1L << j;
    }

    //判断布隆过滤器是否存在当前key
    public boolean contains(T key){
        int index1 = Math.abs(hash1(key) % this.realLength);
        int index2 = Math.abs(hash2(key) % this.realLength);
        int index3 = Math.abs(hash3(key) % this.realLength);
        return hasKey(index1) && hasKey(index2) && hasKey(index3);
    }

    public boolean hasKey(int index){
        //i和j的算法同上
        int i = index >>> 6;
        int j = index & (64 - 1);
        long temp = bitmap[i];
        //temp | 1L << j 会让其中的j位变为1
        //如果下面的等式为true,说明原本j位就是1,否则说明当前key不存在
        return temp == (temp | 1L << j);
    }

	//3个hash函数,可以自行实现散列能力更好的
    //这里借鉴了HashMap的hash函数
    private int hash1(T key){
        int hashCode = key.hashCode();
        return hashCode ^ (hashCode >>> 4);
    }

    private int hash2(T key){
        int hashCode = key.hashCode();
        return hashCode ^ (hashCode >>> 3);
    }

    private int hash3(T key){
        int hashCode = key.hashCode();
        return hashCode ^ (hashCode >>> 16);
    }
}
class Test{
    public static void main(String[] args) {
        BloomFilter<String> filter = new BloomFilter(10);
        filter.add("abc");
        filter.add("def");
        filter.add("xyz");
        System.out.println(filter.contains("abc")); //true
        System.out.println(filter.contains("def")); //true
        System.out.println(filter.contains("xyz")); //true
        System.out.println(filter.contains("afwafwa")); //false
    }
}

上面这个实现用了一些巧妙的位运算,大家可以自行计算一下,比较简单。使用long的64位来做为bitmap,可以节省很多的空间,事实上Google的guava库中实现的布隆过滤器也是这样做的,下面我们来分析一下它的源码。

guava的布隆过滤器使用就不说明了,大家自己去查阅相关资料。具体路径在com.google.common.hash的#BloomFilter,不过guava使用了下面这个类来封装具体的bitmap实现。

private final LockFreeBitArray bits;

我们简单分析以下核心方法

static final class  LockFreeBitArray {
  private static final int LONG_ADDRESSABLE_BITS = 6;
  //使用原子类来保证并发安全,这个data相当于上面自实现的bitmap
  final AtomicLongArray data;

  //bitIndex就是整个bitmap的索引
  boolean set(long bitIndex) {
    //先判断是否有值,有就直接返回
    if (get(bitIndex)) {
      return false;
    }
    //计算data数组的下标
    int longIndex = (int) (bitIndex >>> LONG_ADDRESSABLE_BITS);
    //计算具体要修改的data[longIndex]的哪一位
    long mask = 1L << bitIndex; // only cares about low 6 bits of bitIndex
    long oldValue;
    long newValue;
    //自旋cas修改
    do {
      //获取data[longIndex]
      oldValue = data.get(longIndex);
      //与运算修改具体的那一位
      newValue = oldValue | mask;
      //相等,说明那一位已经是1了
      if (oldValue == newValue) {
        return false;
      }
    } while (!data.compareAndSet(longIndex, oldValue, newValue));
    return true;
  }
  
  //判断bitIndex是否为1
  boolean get(long bitIndex) {
    return (data.get((int) (bitIndex >>> LONG_ADDRESSABLE_BITS)) & (1L << bitIndex)) != 0;
  }
}

在本文中只是简单的分析了一下guava实现的布隆过滤器的数据结构,它的hash算法也是比较出色的,这一块感兴趣的就自己去搜下吧。

计数布隆过滤器

在分析了基础的布隆过滤器之后,我们知道原版是不支持删除的,那如果我们对每一位都进行计数,删除时也对每一位进行-1不就可以了吗?直接上代码。

public class CountingBloomFilter<T> {

    private byte[] bitmap;

    public CountingBloomFilter(int length){
        this.bitmap = new byte[length];
    }

    public void add(T key){
        int length = bitmap.length;
        int index1 = Math.abs(hash1(key) % length);
        int index2 = Math.abs(hash2(key) % length);
        int index3 = Math.abs(hash3(key) % length);
        location(index1);
        location(index2);
        location(index3);
    }

    private void location(int index){
        this.bitmap[index]++;
    }

    public boolean contains(T key){
        int length = bitmap.length;
        int index1 = Math.abs(hash1(key) % length);
        int index2 = Math.abs(hash2(key) % length);
        int index3 = Math.abs(hash3(key) % length);
        return hasKey(index1) && hasKey(index2) && hasKey(index3);
    }

    public boolean hasKey(int index){
        return this.bitmap[index] > 0;
    }

    public void remove(T key){
        int length = bitmap.length;
        int index1 = Math.abs(hash1(key) % length);
        int index2 = Math.abs(hash2(key) % length);
        int index3 = Math.abs(hash3(key) % length);
        this.bitmap[index1]--;
        this.bitmap[index2]--;
        this.bitmap[index3]--;
    }

    private int hash1(T key){
        int hashCode = key.hashCode();
        return hashCode ^ (hashCode >>> 4);
    }

    private int hash2(T key){
        int hashCode = key.hashCode();
        return hashCode ^ (hashCode >>> 8);
    }

    private int hash3(T key){
        int hashCode = key.hashCode();
        return hashCode ^ (hashCode >>> 16);
    }
}

class Test{
    public static void main(String[] args) {
        CountingBloomFilter<String> filer = new CountingBloomFilter<>(5);
        filer.add("abc");
        filer.add("bcd");
        filer.add("cde");
        System.out.println(filer.contains("abc"));//true
        System.out.println(filer.contains("bcd"));//true
        System.out.println(filer.contains("cde"));//true
        filer.remove("abc");
        System.out.println(filer.contains("abc"));//false
        System.out.println(filer.contains("bcd"));//true
        System.out.println(filer.contains("cde"));//true
        filer.remove("bcd");
        System.out.println(filer.contains("abc"));//false
        System.out.println(filer.contains("bcd"));//false
        System.out.println(filer.contains("cde"));//true
        filer.remove("cde");
        System.out.println(filer.contains("abc"));//false
        System.out.println(filer.contains("bcd"));//false
        System.out.println(filer.contains("cde"));//false
    }
}

这里采用了字节数组来做为bitmap,不过相比于原版的布隆过滤器,计数的方式缺点很明显,空间上使用1个字节表示原本的1位,然后就是误判率会增高。当然上面的代码也只是一个很简单的思路,并不能和生产环境的去比较。但在这我也想过,用byte实现计数的话,1个字节可以表示127的计数。如果用long实现,将long的低16位做为bitmap,高48位做为计数,每一个低位能分配3位,也就是低位能计数7个,那么相比于byte实现,long表示的bitmap位数是byte实现的16倍,但是计数缺比较低。这些也只是我个人的一个思考,真正的还是根据具体的业务场景选型。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值