卓越设计的数据结构系列——布隆过滤器(Bloom Filter)

前言

今天,我们将开启一个全新的系列——卓越设计的数据结构。在这个系列中,我们将重点分享一些可以令人赞不绝口的数据结构,不同于我们大学时期所学习的《数据结构》课程,本系列所分析的数据结构,都是一些在学术界和工业界中研究所诞生的成果,其本身是对传统数据结构和经典算法的组合和演变,从而能够在一类问题中表现出卓越的性能。
常常说,计算机这门学科,汇集了世界最聪明的头脑,无论在计算机最早期的硬件设计,还是到后期软件的设计和算法的研究,无不让人对其中的智慧赞不绝口。
通过这一系列的学习和研究,我们在工作或学习中,可以直接使用这类新型数据结构,另一方面,可以锻炼我们的研究思维能力,能够对传统数据结构与经典算法进行融合,获得全新的问题解决方案。那么,让我们开始感受这些无与伦比的大脑中的智慧吧。

问题分析

我们常常会有这样的需求:在已有数据集中,检测某一元素的存在性。具体的场景包括:

  • 云存储去重:服务器存储新文件前,检测是否已经存储了该文件
  • web缓存问题:web服务器常常为了提高用户的交互体验,将常用数据进行缓存,伴随着数据的动态更新,缓存数据也需要进行动态更新,那么,何时需要存储新的数据,就需要对缓存数据存在性进行检测了

该类问题可以简单概括为:当前已维护了一个单一副本的数据集,当新元素加入时,需要在已有数据集中检测当前元素是否存在。
此类问题的传统求解方法一般是通过记录元素的摘要,构造成文件摘要表,当新元素加入时,只需在元素摘要表中进行比对,即可快速验证。而此类求解方法遇到的困境主要是,随着数据集的膨胀,元素摘要表也将线性增长,查询时间也会线性的增长。从而,带来较大的查询开销,而在元素摘要表上作索引等优化,也会带来额外的维护开销。

设计目标

因此,我们需要一个更优的数据组织形式和查询算法,解决上述提到的问题,同时我们的求解方案需要满足以下几个特征:

  • 在不需要保存和暴露元素信息的情况下,实现元素存在性检测
  • 检测算法的时间复杂度尽量控制在O(1) 范围内
  • 尽量保证检测所需的存储和计算开销足够小

布隆过滤器

接下来,我们将隆重介绍本文的主角——布隆过滤器。首先,我们从最原始的布隆过滤方案谈起,此方案最早于1970年由Bloom在论文[1]中所提出。通过使用数组和一定数量的独立哈希函数构造整个方案,其基本模型包括三个阶段:模型初始化阶段、数据集登记阶段和布隆过滤阶段。

模型初始化造阶段

在此阶段,由用户定义所需数据集的大小m,以及能承受的假阳性(false false)概率f(后文将详细说明布隆过滤器的假阳性问题及解决方案)。
由概率计算公式1:n=ceil(m / (-k / log(1 - exp(log( p ) / k))))
获得初始化数组的大小n。
并根据概率计算公式2:k=round((m / n) * log(2))
获得独立哈希函数的个数k。
最后,初始化大小为n的数组,并将各位初始化为0,并准备k个独立的哈希函数进行后续初始化运算。
ps:还有一类需求,若同时配置好数据集大小m、假阳性概率f和独立哈希函数个数k,求解所需的初始化数组大小,可根据概率计算公式3:

lgp_k = Math.log(p) / k;	
r = (-k) / Math.log(1 - Math.exp(lgp_k))
n = Math.ceil(m / r)
数据集登记阶段

此阶段,对已有数据集进行登记,分别对数据集中每一个数据文件进行k次哈希运算,获得一个大小为k的哈希集合Hk,然后对此集合进行遍历,将每一个集合元素在数组上进行映射(可简单作&运算),将每一个映射位设置为1。

布隆过滤阶段

此阶段,对新加入数据集的元素进行过滤检测。及重复数据集登记阶段的过程,将元素进行k次独立的哈希计算获得哈希集合Hk,然后对此集合进行遍历,将每一个集合元素在数组上进行映射,检测所有映射位是否位1,若其中有一个不为1,那么该元素不存在。

一个简单的例子

在不考虑假阳性发生概率的情况下,进行布隆过滤器的简单演示:
首先准备三份文件f1、f2和f3,其中f2为f1的副本,f3不同于f1、f2,假设数组大小为15,独立哈希函数个数为3个,采用下述的优化方案,采用HMAC函数与对应3个独立key代替3个独立哈希函数。
当上传文件f1时,进行哈希数组的初始化,算法如下:

key1:jfwe30fm3o9z4
key2:p0z9mc8d63n7
key3:0k386fh36xl138
HMAC(key1,f1) = 1024421  & 15 = 5
HMAC(key2,f1) = 9102814  & 15 = 14
HMAC(key3,f1) = 7819302 & 15 = 6
布隆数组arr 0 0 0 0 0 1 1 0 0 0 0 0 0 1 

当上传文件f2时,进行去重检验,算法如下:

HMAC(key1,f2) = 1024421  & 15 = 5
HMAC(key2,f2) = 9102814  & 15 = 14
HMAC(key3,f2) = 7819302 & 15 = 6
判断:哈希数组arr[5] == arr[14] == arr[6] == 1?
结果:文件f2已经存在,不需要重复上传

当上传文件f3时,进行去重检验,算法如下:

HMAC(key1,f3) = 9810283  & 15 = 1
HMAC(key2,f3) = 3219203  & 15 = 3
HMAC(key3,f3) = 4864821 & 15 = 6
判断:哈希数组arr[5] == arr[14] == arr[6] == 1?
结果:文件f3不存在,需要进行上传,同时更新布隆数组
更新结果arr 0 1 0 1 0 1 1 0 0 0 0 0 0 1 
假阳性(false positive)分析

对传统《数据结构》课程中的散列表有过研究的同学都应该知道,散列表通过空间换时间的思路,能够在查找性能上获得非常好的表现,具体可以达到O(1)的时间复杂度。但是,由于设计机制本身所使用的哈希函数的特性,不可避免的带来了哈希碰撞的问题,即原本考量不同文件能够合理均匀的映射到不同的数组位置上去,但是由于哈希函数运算的不可推测性,常常造成不同文件映射到相同的位置上,从而造成一定的问题。对于散列表,当出现哈希碰撞时,我们可以通过哈希再散列、线性探测等方法,有效解决哈希碰撞问题。
在布隆过滤器中,同时也存在此类问题,即文件原本不存在,但是由于其他文件的映射处理,造成了其所对应的各个位置都被置为了1,从而让用户相信该文件是存在的,而实际却不存在,即为假阳性。假阳性问题在布隆过滤器中,还没有实质性的解决方案,其产生的本质也是哈希函数特性所造成的,因此,解决方案只能围绕着降低假阳性发生概率上展开:

  • 数组优化方案
    通过将数组按独立哈希函数的个数进行切分,构成一个k维数组,各个独立哈希函数则可以在对应的一维数组上作映射,从而可以分割哈希映射空间,从而有效的降低碰撞发生的概率
  • 哈希运算优化方案
    哈希运算方面,可以存在两方面的优化思路:一方面,可以通过HMAC的哈希映射方案,随机生成k个密钥,然后根据k个密钥进行哈希运算,从而可以减少对独立哈希函数个数的要求。另一方面,可以采用长摘要的哈希函数,将生成的摘要平均分割为k份,再将其进行独立映射
参考文献

[1] B. Bloom. Space/Time Trade-offs in Hash Coding with Allowable Errors. Communications of the ACM, 13:422-426, 1970.
[2] Almeida P S, Baquero C, Preguiça N, et al. Scalable Bloom Filters[J]. Information Processing Letters, 2007, 101(6):255-261.

推荐代码(Python)

上述参考文献中[2]所实现的代码,由Python进行实现
https://github.com/jaybaird/python-bloomfilter

总结

至此,我们已经完成了本系列的第一篇:布隆过滤器的学习。相信,各位同学如果认真学习完上述内容,将同我一样发出赞叹,多么优雅和高效的一种数据结构啊。当然,任何完美的事物,都会存在一些小瑕疵,假阳性问题将伴随布隆过滤器一通存在,如何在其中,找到代价平衡,也是一种智慧和选择。希望大家能有所收获,任何问题可以留言。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值