今天,我们将开启一个全新的系列——卓越设计的数据结构。在这个系列中,我们将重点分享一些可以令人赞不绝口的数据结构,不同于我们大学时期所学习的《数据结构》课程,本系列所分析的数据结构,都是一些在学术界和工业界中研究所诞生的成果,其本身是对传统数据结构和经典算法的组合和演变,从而能够在一类问题中表现出卓越的性能。
常常说,计算机这门学科,汇集了世界最聪明的头脑,无论在计算机最早期的硬件设计,还是到后期软件的设计和算法的研究,无不让人对其中的智慧赞不绝口。
通过这一系列的学习和研究,我们在工作或学习中,可以直接使用这类新型数据结构,另一方面,可以锻炼我们的研究思维能力,能够对传统数据结构与经典算法进行融合,获得全新的问题解决方案。那么,让我们开始感受这些无与伦比的大脑中的智慧吧。
我们常常会有这样的需求:在已有数据集中,检测某一元素的存在性。具体的场景包括:
该类问题可以简单概括为:当前已维护了一个单一副本的数据集,当新元素加入时,需要在已有数据集中检测当前元素是否存在。
此类问题的传统求解方法一般是通过记录元素的摘要,构造成文件摘要表,当新元素加入时,只需在元素摘要表中进行比对,即可快速验证。而此类求解方法遇到的困境主要是,随着数据集的膨胀,元素摘要表也将线性增长,查询时间也会线性的增长。从而,带来较大的查询开销,而在元素摘要表上作索引等优化,也会带来额外的维护开销。
因此,我们需要一个更优的数据组织形式和查询算法,解决上述提到的问题,同时我们的求解方案需要满足以下几个特征:
接下来,我们将隆重介绍本文的主角——布隆过滤器。首先,我们从最原始的布隆过滤方案谈起,此方案最早于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
对传统《数据结构》课程中的散列表有过研究的同学都应该知道,散列表通过空间换时间的思路,能够在查找性能上获得非常好的表现,具体可以达到O(1)的时间复杂度。但是,由于设计机制本身所使用的哈希函数的特性,不可避免的带来了哈希碰撞的问题,即原本考量不同文件能够合理均匀的映射到不同的数组位置上去,但是由于哈希函数运算的不可推测性,常常造成不同文件映射到相同的位置上,从而造成一定的问题。对于散列表,当出现哈希碰撞时,我们可以通过哈希再散列、线性探测等方法,有效解决哈希碰撞问题。
在布隆过滤器中,同时也存在此类问题,即文件原本不存在,但是由于其他文件的映射处理,造成了其所对应的各个位置都被置为了1,从而让用户相信该文件是存在的,而实际却不存在,即为假阳性。假阳性问题在布隆过滤器中,还没有实质性的解决方案,其产生的本质也是哈希函数特性所造成的,因此,解决方案只能围绕着降低假阳性发生概率上展开:
[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.
上述参考文献中[2]所实现的代码,由Python进行实现
https://github.com/jaybaird/python-bloomfilter
至此,我们已经完成了本系列的第一篇:布隆过滤器的学习。相信,各位同学如果认真学习完上述内容,将同我一样发出赞叹,多么优雅和高效的一种数据结构啊。当然,任何完美的事物,都会存在一些小瑕疵,假阳性问题将伴随布隆过滤器一通存在,如何在其中,找到代价平衡,也是一种智慧和选择。希望大家能有所收获,任何问题可以留言。