是什么
它由布隆于 1970 年提出,由一个很长的二进制向量和一系列随机映射函数组成。 通过它,可以帮助我们快速定位一个元素在一个集合中是否存在。但是需要指出的是,这是一种概率型数据结构(probabilistic data structure),当然,概率是单向的,当它判定元素不存在时,一定不存在,当它判断存在时,有概率存在。
工作原理
我们需要准备好一个m位的二进制数组,还有k个hash函数,将内容可以映射到0~m-1这些位置上。 当新增加1个元素时,k个hash函数计算出k个值,然后将这些位置上的标识改成已占用(图示灰色)。 当需要查询一个元素是否存在,同样用k个hash函数计算出k个值,如果这k个位置上的标识都是已占用,那么这个元素被布隆过滤器判定为存在(实际仅仅是有概率存在);只要有1个位置的标识是未占用,那么这个元素一定不存在。
我们跟着一个例子走一遍,为了简单起见,我们设置k=2,m=8,即只有2个hash函数+1个8位的二进制数据,初始情况,8个位置都是未占用。
我们新增1个元素,Jerry
hash1计算的结果为4,hash2计算的结果为6,那么我们把4和6位置设置为已占用
我们再次新增1个元素,Tom
hash1计算的结果为1,hash2计算的结果为4,那么我们把1和4位置设置为已占用
我们需要查询1个元素是否存在,Lucy
hash1计算的结果为3,hash2计算的结果为1,因为位置3为未占用,所以可以判断一定不存在
我们需要查询1个元素是否存在,Lily
hash1计算的结果为6,hash2计算的结果为1,因为位置1和位置6都已经已占用,布隆过滤器会判断元素存在,但是实际上,我们知道这个元素是不存在的,这里位置1和6已占用是因为其他元素的hash冲突
为什么
本质上,布隆过滤器是一种数据结构。这种单向概率的数据结构,其实是为了改进在特定条件下的HashMap。它速度更快,占用空间更小,但是由于是概率型的数据结构,适用场景必然比HashMap更少。
为什么速度更快?
一个重要原因是,几乎不处理hash冲突,当判断存在时,实际是否存在也是概率性的事件。 我们可以对比一下java的HashMap,因为m总是有限的,所以元素增加时,出现hash冲突的概率在变大,那么java是怎么处理的呢,1.8之前,直接用的链表,如果链表很大,查询效率从O(1)退化到O(n);1.8之后,如果链表元素大于8,会优化成红黑树;不过无论是哪种,其实处理Hash冲突的时间远远大于Hash函数的时间。
为什么使用多个哈希函数
其实可以直接对比HashMap,因为m有限,hash冲突是必然发生的,而且冲突的概率随着元素/m的增大而增大。发生冲突的时候,布隆过滤器为了快,不会像HashMap一样去寻找是否真的存在,它会直接返回这个元素已经存在了(当然实际可能并不存在)。但是如果有2个hash函数的话,可以缓解这样的情况,因为2个哈希函数算出来的值都存在的概率就会比1个要小。可以想象,随着k值的增加,总会到达一个临界点,再增加hash函数的话,会导致每个元素进入布隆过滤器时占用的槽位更多,反而加剧hash冲突。
为什么占用空间更小
使用一个二进制数组,1个标志位仅占用1位,并不存储元素本身,对比HashMap存储Entry的方式,小了很多
删除
布隆过滤器一个重要缺点,不支持删除元素,当然现在已经有支持删除的布隆过滤器,但是从实现原理看,这种妥协反而会部分丢失掉上面2个重要的优点,反而让它在本来适用的场景下失去意义。这个原理,我们以后再写一篇,目前我的理解,这个带删除功能的布隆过滤器几乎没有实用价值。
适用场景
那有哪些适用的场景呢?下面列举一些典型的场景: 短链系统,判断一个短链是否使用过 邮件系统,判断一个邮箱是否在黑名单中 缓存系统,判断一个是否需要后续查询 不难发现,这些场景都是及其需要布隆过滤器的2个重要优点:速度快、空间小;但是对2个重要缺点:概率存在和不支持删除,都可以容忍的场景。 其实找任何东西的适用场景都是这4个字:扬长避短。
思考
在使用布隆过滤器的地方,你思考一下为什么不使用HashMap,应该更能理解它的优缺点。