C++布隆过滤器和哈西切分

一、布隆过滤器的提出

我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何快速查找呢?

  1. 用哈希表存储用户记录,缺点:浪费空间
  2. 用位图存储用户记录,缺点:不能处理哈希冲突
  3. 将哈希与位图结合,即布隆过滤器

二、布隆过滤器的概念

位图的局限性?
只能处理整数
如果是处理字符串、自定义类型呢?
需要布隆过滤器来处理

布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间


在这里插入图片描述
我们想要判断三个单词在不在结构中,首先将三个单词字符串转换成一个哈希值,然后在遍历布隆过滤器结构,看sort单词对应的20位置是0还是1,即可判断在不在。

一个单词字符串,即映射一个比特位,这是多么的节省空间啊!!!

存在的问题?

咱们的位图采用的是直接定址法,不会产生位置冲突。
但是布隆过滤器采用的是相对地址映射,字符串的排列组合情况是很多的,很有可能产生位置冲突(不同的字符串,映射到相同的位置)

所以字符串转成整数,是一定存在误判的:

1、在
2、不在
谁会误判呢?
会存在误判,不在 的结果是准确的(因为不在的情况下,那个映射的位置,比特位一定是0)

布隆是怎么解决这个问题的呢?

绞尽脑汁,想要把这个误判给干掉,但是解决不了,所以咱们允许误判,但是把这个误判的概率降低

如何降低误判呢?

采用一个单词映射多个位置的办法,这样误判率就会被降低了
在这里插入图片描述
但是也不敢映射过多的位置,这样的话,会使用更多的额外空间来存储,空间效率也不是很高

布隆过滤器应该开多大呢?

这个文章里面给出了https://zhuanlan.zhihu.com/p/43263751
利用公式,咱们可以计算,布隆过滤器长度和单词之前的数量关系(大概是4倍的关系)
在这里插入图片描述


三、布隆过滤器的实现

一般情况下布隆过滤器都是用来处理字符串的,所以这里可以将模板参数K的缺省类型设置为string。布隆过滤器中的成员一般也就是一个位图,我们可以在布隆过滤器这里设置一个非类型模板参数N,用于让调用者指定位图的长度,设置一个非类型模板参数X,用于控制位图长度与插入元素的倍数关系。

template<
size_t N,
size_t X=8,
class K=string,
class HashFunc1 = BKDRHash,
class HashFunc2 = APHash,
class HashFunc3 = DJBHash
>
class BloomFilter
{
public:
	// 具体一些操作函数
private:
	bitset<X*N> _bs;
};

实例化布隆过滤器的时候,我们选取了三个将字符串转为整型的哈西函数,这三个函数综合评分是最高,产生哈西冲突的概率也是最小的

struct BKDRHash
{
	size_t operator()(const string& s)
	{
		// BKDR
		size_t value = 0;
		for (auto ch : s)
		{
			value *= 31;
			value += ch;
		}
		return value;
	}
};

struct APHash
{
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (long i = 0; i < s.size(); i++)
		{
			if ((i & 1) == 0)
			{
				hash ^= ((hash << 7) ^ s[i] ^ (hash >> 3));
			}
			else
			{
				hash ^= (~((hash << 11) ^ s[i] ^ (hash >> 5)));
			}
		}
		return hash;
	}
};

struct DJBHash
{
	size_t operator()(const string& s)
	{
		size_t hash = 5381;
		for (auto ch : s)
		{
			hash += (hash << 5) + ch;
		}
		return hash;
	}
};

布隆过滤器的插入

插入元素时,需要通过三个哈希函数分别计算出该元素对应的三个比特位,然后将位图中的这三个比特位设置为1即可。

void Set(const K&key)
{
	size_t len = X * N;
	size_t index1 = HashFunc1()(key) % len;
	size_t index2 = HashFunc2()(key) % len;
	size_t index3 = HashFunc3()(key) % len;
		
	cout << index1 << endl;
	cout << index2 << endl;
	cout << index3 << endl << endl;
	
	// 将这三个位置标记为1
	_bs.set(index1);
	_bs.set(index2);
	_bs.set(index3);
}

布隆过滤器的判断在不在

判断时,需要通过三个哈希函数分别计算出该元素对应的三个比特位,然后判断位图中的这三个比特位是否被设置为1。这里就会存在前文所说到的误判:
1、在(存在误判)
2、不在(准确的)

bool Test(const K& key)
{
	size_t len = X * N;
	size_t index1 = HashFunc1()(key) % len;
	if (_bs.test(index1) == false)
	{
		return false;
	}
	size_t index2 = HashFunc2()(key) % len;
	if (_bs.test(index2) == false)
	{
		return false;
	}
	size_t index3 = HashFunc3()(key) % len;
	if (_bs.test(index3) == false)
	{
		return false;
	}

	return true;// 在 的情况是存在误判
}

测试1:

void TestBloomFilter1()
{
	BloomFilter<100> bf;
	bf.Set("sort");
	bf.Set("left");
	bf.Set("right");
	bf.Set("eat");
	bf.Set("tea");
}

在这里插入图片描述
我们可以看到,是不存在冲突的

测试2:

void TestBloomFilter2()
{
	BloomFilter<100> bf;
	bf.Set("张三");
	bf.Set("李四");
	bf.Set("牛魔王");
	bf.Set("红孩儿");
	bf.Set("eat");

	cout << bf.Test("张三") << endl;
	cout << bf.Test("李四") << endl;
	cout << bf.Test("牛魔王") << endl;
	cout << bf.Test("红孩儿") << endl;
	cout << bf.Test("孙悟空") << endl;
	cout << bf.Test("二郎神") << endl;
	cout << bf.Test("猪八戒") << endl;
	cout << bf.Test("ate") << endl;
}

在这里插入图片描述


测试3:

void TestBloomFilter3()
{
	BloomFilter<100> bf;

	srand(time(0));
	size_t N = 100;
	std::vector<std::string> v1;
	for (size_t i = 0; i < N; ++i)
	{
		std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html";
		url += std::to_string(1234 + i);
		v1.push_back(url);
	}

	for (auto& str : v1)
	{
		bf.Set(str);
	}

	for (auto& str : v1)
	{
		cout << bf.Test(str) << endl;
	}
	cout << endl << endl;

	std::vector<std::string> v2;
	for (size_t i = 0; i < N; ++i)
	{
		std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html";
		url += std::to_string(6789 + i);
		v2.push_back(url);
	}

	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";
		//std::string url = "https://www.baidu.com/s?wd=ln2&rsv_spt=1&rsv_iqid=0xc1c7784f000040b1&issp=1&f=8&rsv_bp=1&rsv_idx=2&ie=utf-8&tn=baiduhome_pg&rsv_dl=tb&rsv_enter=1&rsv_sug3=8&rsv_sug1=7&rsv_sug7=100&rsv_sug2=0&rsv_btype=i&prefixsug=ln2&rsp=5&inputT=4576&rsv_sug4=5211";
		//std::string url = "https://zhidao.baidu.com/question/1945717405689377028.html?fr=iks&word=ln2&ie=gbk&dyTabStr=MCw0LDMsMiw2LDEsNSw3LDgsOQ==";
		//std::string url = "https://www.cnblogs.com/-clq/archive/2012/01/31/2333247.html";
		url += std::to_string(rand());
		v3.push_back(url);
	}

	size_t n3 = 0;
	for (auto& str : v3)
	{
		if (bf.Test(str))
		{
			++n3;
		}
	}
	cout << "不相似字符串误判率:" << (double)n3 / (double)N << endl;
}

如何减小误判率?

控制X因子,X越大误判率越接近0,开的空间越大,误判率越低

布隆过滤器的删除

布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。
在这里插入图片描述

一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。

在这里插入图片描述

缺陷:

  1. 无法确认元素是否真正在布隆过滤器中
  2. 存在计数回绕
  3. 存储空间整体变多了,布隆过滤器的优势也没有了

布隆过滤器的优点

  1. 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
  2. 哈希函数相互之间没有关系,方便硬件并行运算
  3. 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
  4. 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
  5. 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
  6. 使用同一组散列函数的布隆过滤器可以进行交、并、差运算

布隆过滤器的缺点

  1. 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白名单,存储可能会误判的数据)
  2. 不能获取元素本身
  3. 一般情况下不能从布隆过滤器中删除元素
  4. 如果采用计数方式删除,可能会存在计数回绕问题

四、布隆过滤器的应用场景

需求:数据量大,节省空间,允许误判,这样场景,就可以使用布隆过滤器

在这里插入图片描述
昵称没被用过,那么一定不在布隆过滤器中,我们可以准确判断,但是如果某个昵称使用过,这个就存在误判,所以再去数据库中查找,看看到底用没用过。

常见的适用常见还有,利用布隆过滤器减少磁盘 IO 或者网络请求,因为一旦一个值必定不存在的话,我们可以不用进行后续昂贵的查询请求。

五、布隆过滤器的扩展[面试题]

给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出近似算法和精确算法。

近似算法

交集里面
1、存在不是交集的query 2、有相同query没进去
是第一种情况

近似算法,(允许有部分摸鱼的!)也就是允许存在一些误判,那么我们就可以使用布隆过滤器。
先读取其中一个文件当中的query,将其全部映射到一个布隆过滤器当中。
然后读取另一个文件当中的query,依次判断每个query是否在布隆过滤器当中,如果在则是交集,不在则不是交集。

精确算法

query是字符串,比如是sql语句,是网络请求url
假设每个query是10byte,100亿个query需要多少存储空间?——100G左右的空间
这里用咱们的set bitset 等都完成不了,需要使用哈西切分

六、哈西切分

一个文件不是100G吗,咱一次处理不了(内存只有1G),那么就把这个文件切割成为100份,能够平均切分吗?不能,因为不能提高效率,例如A0文件中,可能都存在B0~B99中的交集,我们依次比较,还是没能提高效率,所以咱们要哈西切分。

切分的时候,选择一个哈西函数进行切分,依次遍历文件中的每个query,通过哈西函数,把每个query转换成为一个整数 i (0<=i <= 200),切分时依次遍历A文件当中的每个query,通过哈希函数将每个query转换成一个整型 i,然后将这个query写入到小文件Ai当中。对于B文件也是同样的道理,但切分A文件和B文件时必须采用的是同一个哈希函数。由于切分A文件和B文件时采用的是同一个哈希函数,因此A文件与B文件中相同的query计算出的 i 值都是相同的,最终就会分别进入到Ai和Bi文件中,这也是哈希切分的意义。


在这里插入图片描述

如何读取交集呢?

Ai和Bi小文件找交集即可,A和B中相同的query一定进入编号相同的小文件

有点类似于归并排序的思想。
我们就只需要分别找出A0与B0的交集、A1与B1的交集、…、A199与B199的交集,最终将这些交集和起来就是A文件和B文件的交集。

如何在A1和B1小文件中查找交集呢?

这里我们就可以加载进入内存操作了
我们可以将其中一个小文件加载到内存,并放到一个set容器中,再遍历另一个小文件当中的query,依次判断每个query是否在set容器中,如果在则是交集,不在则不是交集。

如何确定切割的份数呢?

这个取决于咱们有多少内存,比如咱们只有1G内存,那么总共100G文件,那么切分成200份,一份500M左右,刚好差不多可以同时加载A1和B1进入内存中

可能会存在两个小文件的大小都大于1G

可以扩大我们的除数,比如扩大到1000,这样让我们切割的份数更多。
如果Ai和Bi都太大,超过内存,可以考虑换个哈希算法,重新再切分一次。
还可以将这两个小文件再进行一次切分,将其切成更小的文件,方法与之前切分A文件和B文件的方法类似(递归算法)。

给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址? 与上题条件相同,如何找到top K的IP?如何直接用Linux系统命令实现?

出现次数最多的IP

还是要用到哈西切分
在这里插入图片描述
相同的ip一定进入了同一个小文件,那么我们使用map统计一个小文件中的ip的次数,就是他准确的次数。用一个map<string, int> maxCount容器统计出每个小文件中各个IP地址出现的次数,然后将这个map清空,比对各个小文件中出现次数最多的IP地址,过程中记录出现次数最多的IP,使用一个pair <string , int> countIP结构l来记录即可,且不断的更新pair,最终就能够得到log file中出现次数最多的IP地址。

找到TOP K 的IP?

可以使用优先级队列
出现次数最多的k个ip
priority_queue<pair<string, int> ,com> minHeap 使用小堆

Linux命令找出top 10

sort log_file | uniq -c | sort -nr k 1,1 | head -10

sort 命令将以默认的方式将文本文件的第一列以 ASCII 码的次序排列,并将结果输出到标准输出。-n 依照数值的大小排序。-r 以相反的顺序来排序。[-k field1[,field2]] 按指定的列进行排序。
uniq -c或–count 在每列旁边显示该行重复出现的次数。

在这里插入图片描述

在这里插入图片描述

sort命令
uniq命令
拓展学习:一致性哈西

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值