赞
踩
最近在学习隐私计算时了解到布谷鸟过滤器的概念(后面会专门写篇博客介绍),自然也就联系到了本文的主角—布隆过滤器。关于布隆过滤器的名号我很久之前就听说过了,但是一直没有深入了解,今天就系统性学习一下并通过这篇博客记录下来。废话不多说,开始吧!
布隆过滤器(Bloom Filter)是1970年由布隆(虽然我搜遍全网也没找到老先生的照片)提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。
布隆过滤器,在合理的使用场景中具有四两拨千斤的作用,由于使用场景是在大量数据的场景下,所以这东西类似于秒杀,虽然没有真的落地用过,但是也要说的头头是道。
常见于面试环节:比如大集合中重复数据的判断、缓存穿透问题等。
首先,布隆过滤器作为一个"过滤器"而不是数据库,它并不存储数据本身,而是告诉我们某个元素是否存在而已。
当然也许有人会说,我们使用传统的数据库MySQL也能告诉我们某个元素是否存在,SELECT查一下就好了啊,但数据库需要存储所有的数据元素,成本又高效率又低。
而我们的布隆过滤器只需要一个比特bit数组和三个哈希函数,这自然就会让我们的空间效率和查询时间都优于一般的算法。
对于布隆过滤器,有一个重中之重的概念:
布隆过滤器说存在的元素,真实不一定存在;而布隆过滤器说不存在的,真实一定不存在。
下面来看一下布隆过滤器的数据结构,其实很简单:
如上图所示,一共有三个hash算法,以及一个带有初始长度 m 的数组,数组的各位上存放的是bit,要么0要么1,默认全为0。在这种情况下,就算我们申请一个 m=100万的数组,其占用空间也就 1000000bit/8/1024 ≈ 122KB
。
当我们要将一个元素插入到布隆过滤器时,会执行如下操作:
下图形象展示了这个过程:
这样,如果再来一个元素"半岛铁盒",那么三个hash函数算到的还是下标为2、11、14三个同样的位置。
我们再模拟插入一个元素:
此时我们插入元素"bitcoin",经过hash计算得到的三个数组下标为1、11、13,我们会发现,对于"半岛铁盒"和"bitcoin",在hash2上计算结果都是11,也就是说有两个元素指向了下标11,也就是常说的哈希碰撞。
OK,此时我们的比特数组是这样的:
只看这个数组,我们能直接在里面找到元素"半岛铁盒"和"bitcoin"吗?
很显然不能,所以接下来就要讲布隆过滤器的查询。
我们应该怎么知道元素"bitcoin"有没有经过此布隆过滤器呢?
很简单,对于要查询的元素"bitcoin",只需要用三个hash函数计算它的hash:
hash1(“bitcoin”) = 1
hash2(“bitcoin”) = 11
hash3(“bitcoin”) = 13
接下来我们去下标1、11、13找,如果对应的比特值全为1,就说明元素"bitcoin"有可能来过。注意,这里是有可能,因为我们之前说过:布隆过滤器说存在的元素,真实不一定存在。
比如对于元素"可口可乐",假设我们通过hash函数算出来的三个hash值为:2、13、14。观察数组,我们会发现这三个下标处的比特位是1,那么我们就说,元素"可口可乐"是可能存在的。然而我们知道,在之前我们并没有插入过这个元素,这就说明布隆过滤器是存在一定的误报率的。
产生误报率的原因就显而易见了,我们的hash函数会发生碰撞:
而对于我们之前说过的后半句:布隆过滤器说不存在的,真实一定不存在。大家想必也能想清楚了,举个例子:对于元素"拿铁咖啡",假设我们经hash计算后得到的三个值为:1、2、6,哪怕其中的1、2处的比特为1,但是在index=6处的比特位是0,那么我们就可以笃定"拿铁咖啡"一定没有经过布隆过滤器。
这里给出一个网站,可以直接上手玩一下布隆过滤器。
布隆过滤器对元素的删除,肯定不可以,会出现问题,比如上面添加元素的 bit 位 11被两个变量的哈希值共同覆盖的情况下,一旦我们删除其中一个值。例如“半岛铁盒”而将其置位 0,那么下次判断另一个值例如“bitcoin”是否存在的话,会直接返回 false,而实际上我们并没有删除它,这就导致了误判的问题。
布隆过滤器的优点:
缺点:
下面给出了一些布隆过滤器的应用场景:
没有想到布隆过滤器的应用这么广泛吧,在场景容忍误判的情况下,布隆过滤器是大有可为的。
Guava是google开源的工具包,含有这些常用的模块:集合 [collections] 、缓存 [caching] 、原生类型支持 [primitives support] 、并发库 [concurrency libraries] 、通用注解 [common annotations] 、字符串处理 [string processing] 、I/O 等。在github上有39.9k的stars,可见其受欢迎程度。
下面我们来看个Guava自带的布隆过滤器的实战demo:
public class MyTest { private static int size = 100000; // 预期数据量 private static double fpp = 0.01; // 期望的误报率 private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(),size,fpp); public static void main(String[] args) { for (int i = 0; i < 100000; i++) { bloomFilter.put(i); } long start = System.nanoTime(); int count = 0; for (int i = size; i < size+100000; i++) { if(bloomFilter.mightContain(i)){ count++; System.out.println(i+"被误报了"); } } System.out.println("总耗时:"+(System.nanoTime()-start)/1_000_000+" ms"); System.out.println("总误报率次数"+count); System.out.println("总误报率:"+count*100/100000+"%"); } }
output:
...
...
...
199172被误报了
199240被误报了
199243被误报了
199273被误报了
199336被误报了
199700被误报了
199919被误报了
总耗时:23 ms
总误报率次数1018
总误报率:1%
多试几次,我们可以发现误报率是符合我们最开始设定的预期fpp的。
那么期望误报率fpp是不是越小越好呢?
显然不是的,越小的fpp需要的存储空间越大,需要的哈希函数也越多。
public class BloomFilter { // 位数组,用于存储布隆过滤器的状态 private BitSet bitSet; // 位数组的长度 private int bitSetSize; private int expectedNumberOfElements; // hash函数数量 private int hashFunctions; private Random random = new Random(); //public BloomFilter(){} public BloomFilter(int bitSetSize,int expectedNumberOfElements) { this.bitSetSize = bitSetSize; this.expectedNumberOfElements = expectedNumberOfElements; this.hashFunctions = (int) Math.round(Math.log(2.0) * bitSetSize / expectedNumberOfElements); this.bitSet = new BitSet(bitSetSize); } public void add(Object element){ for (int i = 0; i < hashFunctions; i++) { long hash = calcHash(element.toString(),i); int index = getIndex(hash); bitSet.set(index,true); } } public boolean mightContains(Object element){ for (int i = 0; i < hashFunctions; i++) { long hash = calcHash(element.toString(),i); int index = getIndex(hash); if(!bitSet.get(index)){ return false; } } return true; } private int getIndex(long hash) { return Math.abs((int)(hash%bitSetSize)); } private long calcHash(String element, int seed) { random.setSeed(seed); byte[] bytes = element.getBytes(); long hash = 0x7f52bed27117b5efL; for(byte b:bytes){ hash^=random.nextInt(); hash*=0xcbf29ce484222325L; hash^=b; } return hash; } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。