write in front
所属专栏: C++学习
️博客主页:睿睿的博客主页
️代码仓库:VS2022_C语言仓库
您的点赞、关注、收藏、评论,是对我最大的激励和支持!!!
关注我,关注我,关注我,你们将会看到更多的优质内容!!
哈希还有一个重要的用途就是布隆过滤器。之前的位图只能对于整数进行判断是否存在,整形有一个固定的范围,但是对于字符串这些没有范围的数据怎么判断在不在呢?字符串在通过哈希函数转化成的数据对应的位置可能和其他字符串通过哈希函数转化的数据相同,这样就会造成误判。
所以我们可以让多个哈希函数对一个字符串转化成多个数据进行标记,这样误判就会少了不少。而这就是布隆过滤器的初形。
在大多数游戏里面,取名都不能取相同的名字。所以就要判断这个昵称是否存在,底层可以用哈希表,但是玩家的数量太多了,都是以亿这个量级来就算,那么采用哈希表就会造成大量的空间浪费;如果用位图,但位图一般只能用于处理整型数据。
那么我们就可以采用哈希表+位图,即布隆过滤器来完成检测这个昵称是否已经被注册。上面的字符串的举例就是布隆过滤器,在实际中判断名称是否重复都是用下面的方式:
布隆过滤器是由布隆在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。
布隆过滤器的关键就是他使用了多个哈希函数(可以参考这篇文章:哈希函数)在这里挑选了三个误判率最低的哈希函数。
#include
#include
using namespace std;
//这里采用仿函数的形式封装哈希函数
struct BKDRHash
{
size_t operator()(const string& str)
{
size_t hash = 0;
for (auto ch : str)
{
hash = hash * 131 + ch;
}
//cout <<"BKDRHash:" << hash << endl;
return hash;
}
};
struct APHash
{
size_t operator()(const string& str)
{
size_t hash = 0;
for (size_t 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)));
}
}
//cout << "APHash:" << hash << endl;
return hash;
}
};
struct DJBHash
{
size_t operator()(const string& str)
{
size_t hash = 5381;
for (auto ch : str)
{
hash += (hash << 5) + ch;
}
//cout << "DJBHash:" << hash << endl;
return hash;
}
};
//非类型模板参数
template<size_t N, class K=string,class Hash1= BKDRHash, class Hash2= APHash,class Hash3= DJBHash>
class BloomFilter
{
public:
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);
}
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;
}
private:
bitset<N> _bs;
};
因为这里采用的是仿函数,所以我们将这个几个哈希函数定义为结构体,然后重载operator()
进行调用。
这里需要注意的是这里不支持删除操作,因为删除一个元素的时候,可能会影响其他元素。
另外,布隆过滤器的设计初衷就是为了快速查找元素是否存在于集合中,并在一些特定应用中提供高效的去重和查询功能。它不被设计用来维护可变的数据集,因此不支持删除操作。
这里参考了这篇文章:详解布隆过滤器的原理,使用场景和注意事项
这篇文章指出,最好的状态(空间消耗小和误判率最低)是:
当我们使用三个哈希函数的时候最佳状态如下:
大家可以通过下面代码测试一下
void TestBloomFilter2()
{
srand(time(0));
const size_t N = 10000;
BloomFilter<N*4> bf;
std::vector<std::string> v1;
std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html";
//std::string url = "猪八戒";
for (size_t i = 0; i < N; ++i)
{
v1.push_back(url + std::to_string(i));
}
for (auto& str : v1)
{
bf.set(str);
}
// v2跟v1是相似字符串集(前缀一样),但是不一样
std::vector<std::string> v2;
for (size_t i = 0; i < N; ++i)
{
std::string urlstr = url;
urlstr += std::to_string(9999999 + i);
v2.push_back(urlstr);
}
size_t n2 = 0;
for (auto& str : v2)
{
if (bf.test(str)) // 误判
{
++n2;
}
}
cout << "相似字符串误判率:" << (double)n2 / (double)N << endl;
// 不相似字符串集
std::vector<std::string> v3;
for (size_t i = 0; i < N; ++i)
{
//string url = "zhihu.com";
string url = "孙悟空";
url += std::to_string(i + rand());
v3.push_back(url);
}
size_t n3 = 0;
for (auto& str : v3)
{
if (bf.test(str))
{
++n3;
}
}
cout << "不相似字符串误判率:" << (double)n3 / (double)N << endl;
}
int main()
{
//testfilter();
TestBloomFilter2();
}
在这里一个query大约是30个字节,100亿个query也就是3000亿个字节。1G大约是10亿字节,所以这里有300G的数据。所以这个地方我们无法直接用位图直接解决。题目给了1G的内存,就说明还是要通过位图来解决,这里就使用了布隆过滤器。
这里我们将两个300G的数据,先通过哈希函数转化为整数,找到该query所在1000个小文件的位置,随后将query放到小文件里面
此时,我们可以读取Ai的数据放入set,在依次读取Bi的query,在通过哈希函数转化之后,在set里面寻找在不在,在就是交集,找到交集之后就将set里面的这个值删掉(避免相同的数据进入交集)。
存在的问题:
解决方法:
insert
抛异常bad_alloc
,就说明大量数据冲突,插入都溢出了。此时换一个哈希函数进行二次切分即可(重复上述操作) 这里和上面一样,将其分别放到不同的小文件里面,这里我们就放在300个小文件里面:
此时相同的ip肯定会放在同一个小文件里面,此时我们就使用map
统计次数就可以了。如果想要找到topk数据,直接用堆来操作就可以了。
更新不易,辛苦各位小伙伴们动动小手,三连走一走 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!
专栏订阅:
每日一题
C语言学习
算法
智力题
初阶数据结构
Linux学习
C++学习
更新不易,辛苦各位小伙伴们动动小手,三连走一走 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!