知识点引入
大家都玩过王者荣耀这一款游戏吧!在游戏中,我们可能会修改自己的游戏 id。这个 id 有一些要求,其中之一就是不能重复。那我们怎样才能快速判断一字符串是否已经存在呢?
在这之前我们学习过哈希表,可以将一个字符串通过字符串的哈希算法转化成整形,然后映射到哈希表中。哈希表能否用来解决这个问题呢?显然是不能的,因为无论你怎样选择字符串的哈希算法,在海量数据之下,两个字符串转化出来的整形是很可能会相同的。
那应该怎么解决这个问题呢!布隆这个人想到了一种降低判断字符串在还是不在的误判率的办法,那就是将同一字符串经过多个哈希算法进行整形转换。当判断一个字符串在不在的时候,当这个字符串经过这些哈希算法计算出来的数字在位图结构中均为 1 表示其可能存在;在位图结构中有一个不为 1 那么就可判定他不在。
因此,一开始的问题就能得到解决(这只是展示一种解决方案,实际肯定不是这么做的)
- 当你修改的 id 经过上述的方法判断出不在的时候,那么你修改的 id 就一定是没有被注册过的。判断不在的时间复杂度为 O(1)。
- 当你修改的 id 被判断出已经存在的话,因为上述方法还是存在误判率因此还需要在数据库中进行查找,做出准确的判定。
可以想象一个场景,当你不需要严格准确判定的话,我们根本可以不用去数据库中查找,直接告诉用户这个昵称已经存在了。这样整体的速度就相当快了。尽管这样做可能导致有一些昵称虽然没有被注册,却依然提示用户被注册过了!可是,用户怎么知道呢😄!!
布隆过滤器的概念
基本结构定义
- 底层数据结构:位图,这里的位图使用库里面的就行。关于位图在上一讲我们已经探讨过了,需要复习的 uu 可以点击这里回顾一下:➡️ C++实现位图。我们不需要百分百准确地判断元素在不在的问题,最好的数据结构就是布隆过滤器啦。布隆过滤器的将元素经过不同的哈希算法得到的值映射到位图中,通过位图来判断一个元素在不在。布隆过滤器主要是针对字符串的,但是普通的整形啥的要放到布隆过滤器里面也没啥问题呢!
- 哈希函数的选择:布隆过滤器的实现是对一个元素选用多个哈希算法,然后映射到位图中。通过这种方法来降低误判率,因此哈希算法的选择也至关重要,我们选择了三个比较优秀的哈希算法,来给布隆过滤器使用,分别是:BKDRHash,APHash,和 DJBHash。您可以网上搜索,了解他们的原理,不过不去了解也没关系,哈希算法在这一节并不重要。
- 模板参数:
- 首先我们需要一个非类型模板参数,给底层数据结构位图使用。
- 还需要一个模板参数表示布隆过滤器要判断什么类型的元素在还是不在,默认是 string 哈,布隆过滤器主要就是为 string 的哈希诞生的。
- 最后三个模板参数就是三个哈希算法。也就是三个仿函数嘛!如果你要增加哈希函数,增加模板参数就行啦!
如果你的布隆过滤器要判断的类型不是 string,一定要传入对应数量的哈希函数,默认的哈希函数只是针对 string 类型的嘛!
#include<string>
#include<bitset>
struct BKDRHash
{
size_t operator()(const string& str)
{
size_t hash = 0;
for(auto e : str) hash += hash * 131 + e;
return hash;
}
};
struct APHash
{
size_t operator()(const string& str)
{
size_t hash = 0;
for(int i = 0; i < str.size(); i++)
{
size_t ch = str[i];
if((i & 1) == 0)
{
hash ^= ((hash << 7) ^ ch ^ (hash >> 3));
}
else
{
hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));
}
}
return hash;
}
};
struct DJBHash
{
size_t operator()(const string& str)
{
size_t hash = 5381;
for(auto ch : str)
{
hash += (hash << 5) + ch;
}
return hash;
}
};
template<size_t N, class K = string,
class Hash1 = BKDRHash,
class Hash2 = APHash,
class Hash3 = DJBHash>
class BloomFilter
{
public:
private:
bitset<N> _bs; //底层数据结构:位图
};
void set(const K& key)
这个函数用来向布隆过滤器中插入数据。参数是插入的数据。实现的方法很简单哈。将传入的参数依次传入三个哈希函数,得到三个整形值之后在对位图的大小取模,然后分别将底层数据结构位图的对应位置置位一即可。
下图中假设位图的大小是 8,然后我们插入两个字符串:baidu,tencent
void Set(const K& key)
{
size_t hash1 = Hash1()(key) % N;
_bs.set(hash1);
size_t hash2 = Hash2()(key) % N;
_bs.set(hash2);
size_t hash3 = Hash3()(key) % N;
_bs.set(hash3);
}
Hash1()(key)
这个代码的意思是:创建一个匿名对象,通过这个匿名对象来调用 operator()
。
bool Test(const K& key)
这个函数用来判断一个字符串是否在布隆过滤器中。大体逻辑和向布隆过滤器中插入数据是一样的。只不过在依次调用哈希函数的过程中,我们就可以对结果做一次判断啦!如果通过哈希函数得到的整形值不在位图中(即位图的 test 接口返回 false )。
那么我们就可以直接得出结论:这个 key 不在布隆过滤器中,因为判断一个元素在布隆过滤器的条件是,这个 key 经过任意一个哈希函数得到的整形值在位图中的相应位置都是 1。
如果到函数末尾都没有返回 flase 那么,就可以返回 true 啦!虽然返回 true 的这个结果并不准确!
bool Test(const K& key)
{
size_t hash1 = Hash1()(key) % N;
if(_bs.test(hash1) == false) //每调用依次哈希函数就对位图做一次判断,看是否能直接返回结果
return false;
size_t hash2 = Hash2()(key) % N;
if(_bs.test(hash2) == false)
return false;
size_t hash3 = Hash3()(key) % N;
if(_bs.test(hash3) == false)
return false;
return true;
}
简单的测试代码
int main()
{
BloomFilter<95> bl;
bl.Set("猪八戒");
bl.Set("孙悟空");
bl.Set("唐僧");
bl.Set("沙悟净");
bl.Set("牛魔王");
bl.Set("铁扇公主");
cout << "猪八戒在不在哇:" << bl.Test("猪八戒") << endl;
cout << "孙悟空在不在哇:" << bl.Test("孙悟空") << endl;
cout << "唐僧在不在哇:" << bl.Test("唐僧") << endl;
cout << "芭蕉公主在不在哇:" << bl.Test("芭蕉公主") << endl;
return 0;
}
我们看到测试结果也是没有什么问题呢!