下文中用到位图的相关知识和代码见博客:位图的基本操作
布隆过滤器主要用于在一个字符串集合中查找某个字符串是否存在。要在集合中查找,首先要将该字符串集合储存起来。那么该如何存储呢?
如果用链表,树等结构进行存储,当集合中的元素越来越多时,所占的空间就越来越大。查找的效率也会越来越低。如果用哈希表来存储,首先要根据字符串哈希函数计算字符串对应的数值,将该数值通过除留余数法等哈希函数计算出下标值,然后将字符串整体存放在哈希表的该下标处中。此时,随着字符串数量的增多,占用的内存也会越来越大。同时发生哈希冲突的概率也会越来越大。在哈希表中解决哈希冲突的方法是通过闭散列(线性探测)或开散列(哈希桶)的方法来解决。但是随着元素个数的增多,查找效率会降低,因此就需要控制存放在哈希表中的元素个数了,此时内存的空间利用率就会降低。
所以,通过变形的哈希表来解决该问题:
首先考虑上述的内存占用问题。如果将一个字符串用一个比特位来表示,此时就会大大节省内存空间。这里就需要使用到位图的结构来进行存储,而且位图适用于不重复的数据集合,此时空间利用率特别高。
其次,要使各字符串对应的下标尽可能的不重复进而提高空间利用率。所以就要解决字符串的冲突问题。在计算字符串的存储下标时,经历了两个步骤:
(1)首先根据字符串哈希函数计算字符串对应的数值。
(2)将上述的数值通过除留余数法等哈希函数计算下标值。
随着字符串数量的增多,在上述两个过程中的任一步计算的数值相同的可能性就越大。因此,发生冲突的概率也会越来越大。因为:
上述由(1)计算的数值如果相同,则由(2)计算的下标值也一定相同。如果(1)计算的数值不相同,则由(2)计算的下标值也有可能相同。此时,都会发生冲突。
所以,在(1)中可以用多个不同的字符串哈希函数对同一字符串作用,此时,一个字符串对应多个数值,而多个数值相同的概率就会大大降低。因此在(2)中有个字符串对应的多个下标值相同的概率也会大大降低。虽然多个数值也有可能相同,因此可能无法判断某一字符串一定存在于集合中(因为该字符串计算的多个下标值可能有集合中另一字符串计算的下标值相同),但是一定可以判断该字符串不存在于集合中(如果该字符串计算的多个下标值在位图中的状态均不为1,则一定没有该字符串)。
因此,可以用位图加上多个字符串哈希函数的方法来实现布隆过滤器。字符串哈希函数越多,一个字符串对应的下标值也就越多,冲突的概率就会越小。
注意:在对布隆过滤器的基本操作中,没有删除某一字符串的操作。因为一个字符串对应多个下标处的状态,一个下标处的状态也可能被多个字符串使用。所以,一旦将一个字符串删除,就会将该字符串对应的多个下标处的1均置为0,此时就会影响其他的字符串。因此不能对布隆过滤器进行删除操作。
有关的字符串哈希函数的实现见:这篇博客。在实际传参时在该博客中任意选取两个字符串哈希函数即可。
基于上述的讨论,来实现布隆过滤器的基本操作:
1. 布隆过滤器的结构设计
上述有提到过,可以利用位图和多个字符串哈希函数来实现一个布隆过滤器。
#define MAXSIZE 1000//定义布隆过滤器中的位图最多有1000个比特位可以使用 typedef size_t (*BloomFunc)(const char* str);//定义字符串哈希算法 #define FUNCMAXSIZE 2//定义需要两个字符串哈希算法来表示一个字符串 typedef struct BloomFilter { //上述的两个成员实际表示的就是一个位图,这里,直接调用位图的结构将其封装在布隆过滤器中即可 BitMap bm;//在布隆过滤器中封装一个位图 BloomFunc bloomfunc[FUNCMAXSIZE];//定义字符串哈希算法 }BloomFilter;
2. 布隆过滤器的初始化
初始化时,只需对布隆过滤器中的位图以及字符串哈希函数进行初始化即可。对位图的初始化直接调用位图的初始化函数即可(见本文开头的博客)。
代码实现如下:
//布隆过滤器的初始化:参数func1,func2是两个字符串哈希函数 void BloomFilterInit(BloomFilter* bf,BloomFunc func1,BloomFunc func2) { if(bf == NULL || func1 == NULL || func2 == NULL) { //非法输入 return; } //对布隆过滤器中的位图进行初始化 BitMapInit(&bf->bm,MAXSIZE); bf->bloomfunc[0] = func1; bf->bloomfunc[1] = func2; return; }
3. 布隆过滤器的插入操作
在对布隆过滤器进行插入时,实际是对位图进行插入操作。但在插入之前首先要计算下标:
(1)根据对个字符串哈希函数对要插入的字符串计算出多个数值
(2)将上述的多个数值通过除留余数法计算出多个下标值
(3)再将位图中的由上述计算的多个下标位置处的状态置为1。此时相当于将上述的多个下标值分别插入到位图中,这里直接调用位图插入函数即可实现。
代码实现如下:
//布隆过滤器的插入 void BloomFilterInsert(BloomFilter* bf,const char* str) { if(bf == NULL || str == NULL) { //非法输入 return; } //首先根据字符串哈希算法计算字符串对应的数字 //然后将计算出的数字根据除留余数法转化为实际对应的比特位下标 uint64_t bloomnum[FUNCMAXSIZE];//该数组用于存放计算的多个下标值 int i = 0; for(;i < FUNCMAXSIZE;i++) { bloomnum[i] = bf->bloomfunc[i](str) % MAXSIZE; } //再将该下标插入到布隆过滤器中的位图中,直接调用位图的插入函数即可 for(i = 0;i < FUNCMAXSIZE;i++) { BitMapSet(&bf->bm,bloomnum[i]); } return; }
4. 在布隆过滤器中查找一个字符串是否存在
思路如下:
(1)首先根据多个字符串哈希函数计算出要查找字符串的多个数值
(2)根据除留余数法将多个数值转化为多个下标值
(3)在布隆过滤器的位图中查找上述的多个下标处的状态是否为1(这里可以使用位图的测试函数来实现)。只要有一个不为1,就说明该字符串一定不存在。只有全都为1,要查找的字符串才存在(注意:此时,也可能因为要查找的字符串与集合中的另一字符串计算出的多个下标相同。因此该情况下要查找的字符串也可能不存在,但是我们已经使用了多个字符串哈希函数来使出现这种情况的概率降低,所以这里就认为要查找的字符串存在)。
代码实现如下:
//在布隆过滤器中判断一个字符串是否存在 int BloomFilterTest(BloomFilter* bf,const char* str) { if(bf == NULL || str == NULL) { //非法输入 return 0; } //首先根据字符串哈希算法计算字符串对应的数字 //在根据该数字计算根据除留余数法计算比特位所在的下标 uint64_t bloomnum[FUNCMAXSIZE]; int i = 0; for(;i < FUNCMAXSIZE;i++) { bloomnum[i] = bf->bloomfunc[i](str)%MAXSIZE; } //然后在布隆过滤器中的位图结构中判断这两个比特位的状态 //如果均为1,则表示该字符串存在 //只要有一个为0,则表示该字符串不存在 for(i = 0;i < FUNCMAXSIZE;i++) { if(BitMapTest(&bf->bm,bloomnum[i]) == 0) { return 0; } } return 1; }
5. 销毁布隆过滤器
销毁时,将其中的位图销毁并将字符串哈希函数置空即可。
//布隆过滤器的销毁 void BloomFilterDestroy(BloomFilter* bf) { if(bf == NULL) { //非法输入 return; } //将布隆过滤器中的成员:位图进行销毁即可 BitMapDestroy(&bf->bm); bf->bloomfunc[0] = NULL; bf->bloomfunc[1] = NULL; return; }