布隆过滤器的实现和应用
课设中对于BloomFilter结构中的散列函数(包括散列函数的个数和散列函数的设计)的思考借鉴于https://blog.youkuaiyun.com/jiaomeng/article/details/1495500?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522162406786316780264061749%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=162406786316780264061749&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-1-1495500.first_rank_v2_pc_rank_v29&utm_term=BloomFilter&spm=1018.2226.3001.4187
项目代码和报告的GitHub地址:https://github.com/tzd1090170081/-
(一) 需求和规格说明
(1)问题描述
布隆过滤器是由巴顿.布隆于一九七零年提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。假定需要存储一亿个电子邮件地址,首先建立一个十六亿二进制(比特),即两亿字节的向量,然后将这十六亿个二进制全部设置为零。对于每一个电子邮件地址X,可以用八个不同的散列函数(H1,H2,...,H8)产生八个从1到十六亿之间中的八个自然数g1,g2,...,g8。然后将这八个自然数对应的八个位置的二进制全部设置为一。同样的方法对这一亿个email地址都进行处理后,一个针对这些email地址的布隆过滤器就建成了。如图所示:
现在看看如何用布隆过滤器来实现一个电子邮件地址过滤器,首先将那些放在黑名单上的电子邮件地址放在布隆过滤器中。当检测一个可疑的电子邮件地址Y是否在黑名单中,仍用相同的八个随机数产生器(H1,H2,...,H8)对这个邮件地址产生八个自然数s1,s2,...,s8,这八个自然数对应的八个二进制位分别是t1,t2,...,t8。如果Y在黑名单中,显然,t1,t2,..,t8对应的八个二进制一定是一。这样在遇到任何在黑名单中的电子邮件地址,都能准确地发现。
布隆过滤器决不会漏掉任何一个在黑名单中的可疑地址。但是,它有一条不足之处。也就是它有极小的可能将一个不在黑名单中的电子邮件地址判定为在黑名单中,因为有可能某个好的邮件地址正巧对应个八个都被设置成一的二进制位。但这种可能性很小,此处将它称为误识概率。在上面的例子中,误识概率在万分之一以下。
因此布隆过滤器的好处在于快速,省空间。但是有一定的误识别率。常见的补救办法是在建立一个小的白名单,存储那些可能别误判的邮件地址。
(2)课程设计目的
学习BloomFilter结构,能应用该结构解决一些实际问题。
(3)基本要求
①定义BloomFilter结构的ADT,该ADT应支持在BloomFilter中加入一个新的数据,查询数据是否在此过滤器中,并完成该结构的设计和实现。
②应用BloomFilter结构拼写检查,许多人都对Word的拼写检查功能非常了解,当用户拼错一个单词的时候,Word会自动将这个单词用红线标注出来。Word的具体工作原理不得而知,但另一个拼写检查器UNIXspell-checkers这个软件中就用到了BloomFilter。UNIXspell-checkers将所有的字典单词存成BloomFilter数据结构,而后直接在BloomFilter上进行查询。本课程设计要求针对C语言设计和实现上述拼写检查器,即当写了一个正确的关键词,如int时,给该词标上颜色,如蓝色。
③针对上述C语言关键词拼写检查器进行分析,如错误分析,设计散列函数个数分析,运行时间复杂性、空间复杂性的分析。
④上述C语言关键词拼写检查器最好是在VC++或Java等可视化开发环境下实现。
⑤上述C语言关键词拼写检查器最好能支持所有的C++关键词。
(4)实现提示
BloomFilter结构中的散列函数(包括散列函数的个数和散列函数的设计)是本题目中需要深入思考的一个环节。
(二) 设计
(本程序使用QT这一C++图形用户界面应用程序开发框架设计GUI界面)
GUI界面部分:
主体上,使用QMenuBar、QTextEdit、QTreeWidget等控件设计文本编辑器的界面,并继承QSyntaxHighlighter类编写子类HighLighter类以实现高亮功能。
1.使用QMainWindow类设置窗口的各种参数及标题
2.使用QMenuBar类设计菜单栏。菜单栏内包括“功能”、“帮助”两个菜单。
3.使用QMenu类和QAction类设计菜单。
4.使用QDialog类与QLable类实现“帮助”菜单下“基本要求”选项的对话框
5.使用QTextEdit类设计文本框,使用QFont类设置文本框内字体格式。
6.使用QTreeWidget类和QTreeWidgetItem类,实现关键字的树形图形化显示。
7.使用自定义的QSyntaxHighlighter的子类HighLighter实现文本框内关键字的高亮显示。
功能的实现部分:
1.HighLighter类构造函数中将“keywords.txt”文件中存储的关键字导入关键字数组中。
2.在关键字判断过程中,重要的是,使用QRegExp类,借助正则表达式将文本框传递给HighLighter类的文本内容拆分为一个个单词,再借助二分查找在关键字数组进行检索,若查找成功则借助HighLighter类中继承自QSyntaxHighlighter类,并经过重写的方法highlightBlock(const QString &text),将文本框中的对应内容特定显示出来。
3.添加关键字的实现:借助二分查找与顺序查找在关键字数组检索需要添加的关键字。若查找成功,则弹出一个对话框,对于关键字已存在的情况进行提示,然后停止添加该关键字;若查找失败,将待添加的关键字按首字母的大小顺序插入到关键词数组中(此时GUI界面左侧的树形控件实时刷新),再将该关键字由7个哈希函数得到的值加入到位数组上,即刷新bloomfilter结构,当文本框内再次出现可被检测到的信号时(如:添加或删除字符等),新的关键字生效。
4.删除关键字的实现:同样借助二分查找与顺序查找在关键字数组检索需要删除的关键字。若查找失败,则弹出一个对话框,对于待删除的关键字不存在的情况进行提示,然后停止删除该关键字;若查找成功,将该关键字从关键词数组中删除(此时GUI界面左侧的树形控件实时刷新),再重新初始化位数组,并通过新的关键字数组进行散列,为位数组赋值,即刷新bloomfilter结构,当文本框内再次出现可被检测到的信号时(如:添加或删除字符等),被删除的关键字失效。
5.保存修改的实现:将关键字数组中存储的关键字导入 “keywords.txt”文件中。
Bloomfilter实现部分:
主体上,对于每个关键字,通过不同的哈希函数,将关键字对应的值存储到位数组中。GUI界面部分的高亮功能就是通过判断单词对应的哈希值是否在位数组中全部存在来实现的。
布隆过滤器设计的关键主要在两方面:第一,布隆过滤器的参数如散列函数个数,二进制数组比特位数等等;第二,散列函数的设计。
Bloom Filter在判断一个元素是否属于它表示的集合时会有一定的错误率。错误率的大小由位数组的大小、哈希函数的个数和关键字的个数决定。下面是相关的计算与推导过程:
为了简化模型,假设kn < m并且各个哈希函数是完全随机的。当集合S={X1, X2,…,Xn}的所有元素都被k个哈希函数映射到m位的位数组中时,这个位数组中某一位还是0的概率是:
其中1/m表示任意一个哈希函数选中这一位的概率(前提是哈希函数是完全随机的),(1-1/m)表示哈希一次没有选中这一位的概率。要把S完全映射到位数组中,需要做kn次哈希。某一位还是0意味着kn次哈希都没有选中它,因此这个概率就是(1-1/m)的kn次方。令p = e-kn/m是为了简化运算,这里用到了计算e时常用的近似:
令ρ为位数组中0的比例,则ρ的数学期望E(ρ)= p’。在ρ已知的情况下,要求的错误率为:
(1-ρ)为位数组中1的比例,(1-ρ)k就表示k次哈希都刚好选中1的区域,即false positive rate。上式中第二步近似在前面已经提到了,现在来看第一步近似。p’只是ρ的数学期望,在实际中ρ的值有可能偏离它的数学期望值。M. Mitzenmacher已经证明,位数组中0的比例非常集中地分布在它的数学期望值的附近。因此,第一步的近似得以成立。分别将p和p’代入上式中,得:
既然Bloom Filter要靠多个哈希函数将集合映射到位数组中,那么应该选择几个哈希函数才能使元素查询时的错误率降到最低呢?这里有两个互斥的理由:如果哈希函数的个数多,那么在对一个不属于集合的元素进行查询时得到0的概率就大;但另一方面,如果哈希函数的个数少,那么位数组中的0就多。为了得到最优的哈希函数个数,根据前面的错误率公式进行计算。
先用p和f进行计算。注意到f = exp(k ln(1 − e−kn/m)),我们令g = k ln(1 − e−kn/m),只要让g取到最小,f自然也取到最小。由于p = e-kn/m,我们可以将g写成:
根据对称性法则可以很容易看出当p = 1/2,也就是k = ln2· (m/n)时,g取得最小值。在这种情况下,最小错误率f等于(1/2)k ≈ (0.6185)m/n。另外,注意到p是位数组中某一位仍是0的概率,所以p = 1/2对应着位数组中0和1各一半。换句话说,要想保持错误率低,最好让位数组有一半还空着。
其中,p = 1/2时错误率最小这个结果并不依赖于近似值p和f。同样对于f’ = exp(k ln(1 − (1 − 1/m)kn)),g’ = k ln(1 − (1 − 1/m)kn),p’ = (1 − 1/m)kn,我们可以将g’写成:
同样根据对称性法则可以得到当p’ = 1/2时,g’取得最小值。
接下来求位数组的位数m:
假设X为全集中任取n个元素的集合,F(X)是表示X的位数组。那么对于集合X中任意一个元素x,在s = F(X)中查询x都能得到肯定的结果,即s能够接受x。显然,由于Bloom Filter引入了错误,s能够接受的不仅仅是X中的元素,它还能够є (u - n)个错误。因此,对于一个确定的位数组来说,它能够接受总共n + є (u - n)个元素。在n + є (u - n)个元素中,s真正表示的只有其中n个,所以一个确定的位数组可以表示:个集合。m位的位数组共有2m个不同的组合,进而可以推出,m位的位数组可以表示个集合。全集中n个元素的集合总共有个,因此要让m位的位数组能够表示所有n个元素的集合,必须有: 即:
上式中的近似前提是n和єu相比很小,这也是实际情况中常常发生的。根据上式,我们得出结论:在错误率不大于є的情况下,m至少要等于n log2(1/є)才能表示任意n个元素的集合。
前面得到当k = ln2· (m/n)时错误率f最小,这时f = (1/2)k = (1/2)mln2 / n。现在令f≤є,可以推出:
这个结果比前面算得的下界n log2(1/є)大了log2 e≈1.44倍。这说明在哈希函数的个数取到最优时,要让错误率不超过є,m至少需要取到最小值的1.44倍。
本程序中内置了63个关键字(C++的全部关键字),如表1所示,经过一定的建模分析后,预计希望保存的关键字为8000个。为将错误率控制在1%以下,取哈希函数个数为7,分别用直接定址法,除留余数法,平方后除留余数法,立方后除留余数法等构造哈希函数。位数组大小设置为80000bit,即一个大小为10000的unsigned char型数组。
asm | do | if | return | typedef |
auto | double | inline | short | typeid |
bool | dynamic_cast | int | signed | typename |
break | else | long | sizeof | union |
case | enum | mutable | static | unsigned |
catch | explicit | namespace | static_cast | using |
char | export | new | struct | virtual |
class | extern | operator | switch | void |
const | false | private | template | volatile |
const_cast | float | protected | this | wchar_t |
continue | for | public | throw | while |
default | friend | register | true | |
delete | goto | reinterpret_cast | try |
表1.程序内置的所有关键字
添加关键字的查找过程中采用二分查找,直到low与high相差小于5时,进行顺序查找,而预计希望保存的关键字为8000个,因此时间复杂度约为O(log2n),该过程中使用了3个int型变量和一个指向约为s个字节内存的char *型指针(s为关键字字符串平均长度),因此空间复杂度为O(1)。查找过程结束后,若关键字尚未存在关键字数组中,则将关键字添加进去,且将记录关键字数量的变量值加一。最好情况下,在数组最后添加,不需要移动其他字符串,时间复杂度为O(1),最坏情况下,在数组前面添加,其他所有字符串全部移动一次,时间复杂度为O(n*s),此过程不占用其他内存空间。然后,刷新布隆过滤器,即对新关键字执行7个哈希函数,将所得值加入位数组,此过程运算复杂度很低。
删除关键字的查找过程与添加关键字相同,时间复杂度约为O(log2n),空间复杂度为O(1). 查找过程结束后,若该关键字存在关键字数组中,则将关该键字删除,且将记录关键字数量的变量值减一。最好情况下,在数组最后删除,不需要其他操作,可视作时间复杂度,最坏情况下,在数组前面添加,其他所有字符串全部移动一次,时间复杂度为O(n*s),此过程不占用其他内存空间。然后,刷新布隆过滤器,此过程运算复杂度很高。
布隆过滤器作为数据结构时,添加关键词时刷新它的运算复杂度极低,而相应的删除关键词时刷新它的成本相当高。原因在于,添加关键字时只需要将新的关键字进行散列后,将所得值添加到位数组,时间复杂度为O(s),而删除关键字时需要将位数组重新初始化后,将所有关键字散列,并将所得值添加到位数组,时间复杂度为O(s*n)。
(三) 用户手册
(1)菜单栏由“功能”与“帮助”两个菜单组成
(2)“帮助”菜单中有一个选项“基本要求”,点击“基本要求”后,会弹出一个非模态对话框,将程序实现的基本要求展示出来。
(3)“功能”菜单中有三个选项“添加关键字”、“删除关键字”和“保存修改”。点击“添加关键字”后,会弹出一个模态输入对话框,在单行文本框输入单词后,就将该单词添加为新的关键字(如果该关键词已存在,则弹出一个模态对话框进行提示)。点击“删除关键字”后,会弹出一个模态输入对话框,在单行文本框输入关键词后,就将该关键字删除(如果该关键词不存在,则弹出一个模态对话框进行提示)。点击“保存修改”后,将目前的所有关键字保存在文本文件“keywords.txt”中。添加和删除关键字后,变动后的关键字要正常显示需要在GUI界面的文本框中发生事件后(比如增加、减少字符)。
(4)关键词的显示在GUI界面的左边的树形控件上实现。
(5)在GUI界面右边的文本输入框中,实现关键字高亮显示。
(四) 调试及测试
使用测试代码将关键词数组中的元素打印在终端中
eg:
asm auto bool break case catch char class const const_cast continue
default delete do double dynamic_cast else enum explicit export extern false float for friend goto if inline int key long mutable
namespace new operator private protected public register reinterpret_cast return short signed sizeof static static_cast struct switch template this throw true try typedef typeid typename
union unsigned using virtual void volatile wchar_t while
(五) 运行实例:
(六)进一步改进
(1)借鉴vscode扩展等的效果,实现关键字自动补全的功能。
(2)借鉴word等工具,实现单词识别效果,对无法识别的单词下划红色波浪线,以警示命名变量。
(3)实现在左侧树形控件单击右键,调出菜单。可以从右键菜单中实现添加关键字和删除关键字。
(4)添加快捷键,如添加关键字、删除关键字的模态输入对话框的弹出可以与快捷键绑定起来。