文章目录
前言
布隆过滤器(Bloom Filter)是一种时间和空间上都比较高效的数据结构,它是1970年由布隆提出的。当然不是这个布隆:
它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。
布隆过滤器可以用于检索一个元素是否在一个集合中。
它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
基本思想
在学习数据结构时,应该都接触过哈希表,哈希表是通过哈希函数对数据进行映射,映射到对应存储地址,这种哈希表存储查找数据时间复杂度很低,为O(1)
事实上布隆过滤器,就是一个很长的二进制数组,存储0和1,默认初始全为0,并使用若干个哈希函数对数据进行映射,得到的若干地址,使这些地址对应值为1.
前面我们说过,布隆过滤器主要是用于检索一个元素是否在一个集合中,那是怎么判断的呢?
事实上,当判定数据A时,只需要进行映射,判断若干哈希函数映射出的若干哈希地址,对应值是否都为1,只有当全部为1时,才会判定该数据存在。
查询快
假设有K个哈希函数,那么查询该数据是否存在的时间复杂度为O(k)
节省存储空间
为什么不直接用哈希表,事实上,布隆过滤器归根到底是哈希映射的思想,如果直接用哈希表,存储空间是吃不消的,因为用户假如有100w,那么直接放到哈希表是吃不消的,而布隆过滤器则可以利用有限的空间,来实现判断该数据是否存在于集合之中。
误识别率
存在误判,可能要查到的元素并不存在,但是若干hash函数映射之后得到的k个位置上值都是1,按布隆过滤器的思想,就证明该数据存在,这就是误识的情况。
删除困难
某一位被两个数据同时映射到,删除的时候不能简单的直接置为0,因为置为0后,可能将影响两个数据。
简单来说,布隆过滤器不支持反向删除,当过的元素多了,这些元素留下的“印迹”无法摸出,当误判率逐步变高的时候,不得不重建布隆过滤器。
应用场景
例如在解决Redis缓存击穿上,布隆过滤器就大有用处,布隆过滤器可以看作是一层屏障,当不法请求带着id为-1的查询时,可先去布隆过滤器判断是否存在。
这时布隆过滤器体现的好处在于,一百万的数据,布隆过滤器仅需要很少的存储空间。
关于位图的补充
一个字节是8位,那么用一位来表示一个数,就可以很节省空间,10亿个数只需要125MB的内存。
例如对于53,与8相除得到 6.则 数据的位图 存储在数组元素 a[6] 中;再与8求余,得到5,则数据的位图 存储在 a[6] 的第5位二进制上。
面试题:判断一个数是否在40亿个整数中存在?
可以申请一个2^32大小的位图数组,而整数大小范围是0~2^32-1
,故可以放得下。
例如40亿个数中存在4,7,1,5,9,那么在BitMap中可以这么放
之后判断对应下标是否为1,是1则代表出现过。
两组40亿数,求交集和并集
申请两个2^32
大小的Bitmap:
- 如果是求交集,将这两个Bitmap按位与
- 如果是求并集,将这两个Bitmap按位或
给定a、b两个文件,各存放50亿个url,每个url各占64字节,内存限制是4G,让你找出a、b文件共同的url?
-
第一步:遍历文件a,对每个url求取
hash(url)%1000
,然后根据所取得的值将url分别存储到1000个小文件(记为a0,a1,…,a999,每个小文件约300M),为什么是1000?主要根据内存大小和要分治的文件大小来计算,我们就大致可以把298G大小分为1000份,每份大约300M(当然,到底能不能分布尽量均匀,得看hash函数的设计) -
第二步:遍历文件b,采取和a相同的方式将url分别存储到1000个小文件(记为b0,b1,…,b999)(为什么要这样做?
文件a的hash映射和文件b的hash映射函数要保持一致,这样的话相同的url就会保存在对应的小文件中,比如,如果a中有一个url记录data1被hash到了a99文件中,那么如果b中也有相同url,则一定被hash到了b99中)
所以现在问题转换成了:找出1000对小文件中每一对相同的url(不对应的小文件不可能有相同的url) -
第三步:对1000队小文件,设某一对数据分别为文件1和文件2,之后针对每个文件对:读取文件1,建立哈希表(为什么要建立hash表?因为方便后面的查找),然后再读取文件2,遍历文件b中每个url,对于每个遍历,我们都执行查找hash表的操作,若hash表中搜索到了,则说明两文件共有,存入一个集合。
布隆过滤器的优化—布谷鸟过滤器
布谷鸟过滤器源于布谷鸟哈希算法,布谷鸟哈希算法源于生活 —— 那个热爱「鸠占鹊巢」的布谷鸟:
布谷鸟从来不自己筑巢。它将自己的蛋产在别人的巢里,让别人来帮忙孵化。
待小布谷鸟破壳而出之后,因为布谷鸟的体型相对较大,它又将养母的其它孩子(还是蛋)从巢里挤走 —— 从高空摔下夭折了。
最简单的布谷鸟哈希结构是一维数组结构,会有两个 hash 算法将新来的元素映射到数组的两个位置:
- 如果两个位置中有一个位置为空,那么就可以将元素直接放进去。
- 但是如果这两个位置都满了,它就不得不「鸠占鹊巢」,随机踢走一个,然后自己霸占了这个位置。
但是如果这个位置也被别人占了呢?
好,那么它会再来一次「鸠占鹊巢」,将受害者的角色转嫁给别人。然后这个新的受害者还会重复这个过程直到所有的蛋都找到了自己的巢为止。
但是会遇到一个问题,那就是如果数组太拥挤了,连续踢来踢去几百次还没有停下来,这时候会严重影响插入效率。
这时候布谷鸟哈希会设置一个阈值,当连续占巢行为超出了某个阈值,就认为这个数组已经几乎满了。
这时候就需要对它进行扩容,重新放置所有元素。
还会有另一个问题,那就是可能会存在挤兑循环:
-
比如两个不同的元素,hash 之后的两个位置正好相同,这时候它们一人一个位置没有问题。
-
但是这时候来了第三个元素,它 hash 之后的位置也和它们一样,很明显,这时候会出现挤兑的循环。
-
不过让三个不同的元素经过两次 hash 后位置还一样,这样的概率并不是很高,除非你的 hash 算法太挫了。
布谷鸟哈希算法对待这种挤兑循环的态度就是认为数组太拥挤了,需要扩容(实际上并不是这样)
参考
https://blog.csdn.net/qq_42046105/article/details/94593197