C++ 布隆过滤器&哈希切割

前言

现实生活中,存在很多key_value的模型,我们可以使用哈希或者红黑树存储这些数据。但是二者只是内存的存储方式,无法处理海量数据。
海量数据的处理我们可以使用位图处理。但是位图的局限性是,其只能映射整型,对于浮点型,字符串无法解决。但是我们可以同哈希表那样,将浮点型,字符串转换成整型,然后再映射。这样就可以解决问题
这就是布隆过滤器的原理,其本质还是位图

在这里插入图片描述

一. 布隆过滤器

布隆过滤器是 哈希+位图的结合

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

位图和哈希都是一个值映射一个位置,但是布隆过滤器需要映射字符串,字符串的组合非常之多,所以冲突的可能性很大,但是如果我们映射多个位置,那么误判的几率就降低很多了
如下图:
在这里插入图片描述

我们只需要使用多个哈希函数,将字符串转换成不同的整型映射即可。

但是,布隆过滤器可能存在误判的情况
因为我们查找一个字符串是否存在,是查看多个哈希函数转换成的整型地址,如果有一个是0,就代表不存在,因为如果存在,那这几个地址都是1。但是可能出现别的字符串已经映射到了这些地址,查找出来都是1。
所以布隆过滤器对于一个字符串存在是可能误判的,对于不存在是准确的。

  • 应用场景

新用户注册的昵称,就可以应用布隆过滤器。因为昵称是否误判,用户不知道,反之,电话号码就不能只用布隆过滤器,因为是否误判,用户可以知道。

布隆过滤器的实际使用正如其名,起到一个过滤器的作用。
如果一个数据使用布隆过滤器是不存在,是准确的,如果存在,再去数据库中查找,就保证了正确性。
因为数据库是在磁盘的,数据读取效率较低,先使用布隆过滤器刷选不存在的值,省去去数据库查找的开销。

二. 布隆过滤器的实现

上述我们讲到,布隆过滤器是具有多个哈希函数的位图,此位图与传统位图有所不同。
传统位图只能映射整型类型,整型可大可小,所以无论映射多少数据个数,位图的大小都必须开整型最大值。但是布隆过滤器映射的并不是整型,最常用的是字符型,所以并不需要像传统位图那样开空间。
但是这样就引出两个问题:开多大空间?要几个哈希函数?
很显然,过小的布隆过滤器很快所有的 bit 位均为 1,那么查询任何值都会返回“可能存在”,误判的几率很高。布隆过滤器的长度会直接影响误报率,布隆过滤器越长其误报率越小。

哈希函数的个数越多则布隆过滤器 bit 位置位 1 的速度越快,因为一个字符串会映射多个位置,且布隆过滤器的效率越低;但是如果太少的话,那我们的误报率会变高。

详细分析见详解布隆过滤器的原理,使用场景和注意事项

文章中有这样一张图
在这里插入图片描述
该图展示了,在映射数据个数和空间一定是,越多的哈希函数,误判率更低
在这里插入图片描述
哈希函数也不是越多越好,还需要综合来看:
虽然哈希函数更多,误判率更低,但是每映射一个数据,都需要用多个哈希函数转换,那想必时间也消耗更多。所以不是单纯的哈希函数越多越好

接下来的实现,我们使用3个哈希函数,根据公式,开的空间大致是数据个数的4倍。


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

//哈希函数2
struct APHash
{
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (size_t 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;
	}
};

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

//三个仿函数
template<size_t N,class K=string,
class HashFunc1=BKDRHash,
class HashFunc2=APHash,
class HashFunc3=DJBHash>
class BloomFilter
{
public:
	//映射
	void set(const K&key)
	{
		size_t len = N * _X;

		size_t hash1 = BKDRHash()(key) % len;
		_bs.set(hash1);

		size_t hash2 = APHash()(key) % len;
		_bs.set(hash2);

		size_t hash3 = DJBHash()(key) % len;
		_bs.set(hash3);

		//可以将这三个哈希地址打印一下
		cout << hash1 << " " << hash2 << " " << hash3 << endl;
	}

	//查找
	bool test(const string&key)
	{
		size_t len = N * _X;

		//有一个比特位是0,就是不存在
		size_t hash1 = DJBHash()(key) % len;
		if (_bs.test(hash1) == false)
			return false;

		size_t hash2 = APHash()(key) % len;
		if (_bs.test(hash2) == false)
			return false;

		size_t hash3 = DJBHash()(key) % len;
		if (_bs.test(hash3) == false)
			return false;

		//都为1,可能存在,会误判
		return true;
	}
private:
	static const size_t _X = 4;//开空间的倍数
	bitset<N*_X>_bs;
};
  • 测试
    在这里插入图片描述

PS:布隆过滤器一般不支持删除,因为映射一个比特位的字符串可能不只一个,删除很可能影响其他的数据。
要支持删除,就需要使用多个位图,标识一个比特位被映射的次数,如果删除,只是减少一次映射次数,直到减为0,才是真正的删除。

但是这样判断数据存在的误判率就更高了,所以布隆过滤器一般都不支持删除


三. 哈希切割

  • 给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?
    query是数据库的查询语句。可以理解为字符串
    100亿个query数据量很大,不能直接在内存中存储。所以我们可以将这个大文件切分成很多小文件,比如我们切分成1000个小文件,分为小文件Ai,和小文件Bi,而但是这样小文件的内容是不确定的。查找交集,需要比如A1和所有的小文件B查找交集,这样的效率实在太低了。
    所以我们可以在切分时做一些改动,我们还是切分成1000份,但是对于每一个query,我们可以进行一个哈希函数的转换,确定其要放置在哪个小文件。这样,A1只要和B1,A2只要和B2匹配交集即可。
    这就是哈希切割
    在这里插入图片描述

但是这样还会面临一个问题,因为本质也是哈希映射,就可能会有冲突,而冲突过多就可能导致小文件的大小膨胀

  1. 一个小文件有大量重复的数据
  2. 一个小文件有大量不同的数据

如何分辨这两种情况呢?如何解决这两种问题呢?

因为我们已经将文件分割小了,可以尝试使用unordered_set/set,依次存储该小文件的query。因为set可以对数据实现去重。而当内存满时,继续存储会抛bad_alloc异常
如果出现该异常,说明是情况2(有大量不同数据),因为去重没有达成效果。此时我们可以将小文件切分成更小的文件,继续哈希切割
而如果没有出现异常,说明是情况1(有大量重复数据),去重达到效果,那么该文件的哈希切割成功


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

该题也类似,先通过哈希函数将IP地址转换成哈希地址,然后切分成小文件,然后依次处理小文件,使用unordered_map/map统计IP地址出现的次数。
如果抛异常,说明冲突太多,还需要继续切分
如果没有抛异常,那么统计成功。
然后汇总所有小文件出现次数最多的IP,最后使用priority_queue获取到TopK

结束语

本篇内容到此就结束了,感谢你的阅读!

如果有补充或者纠正的地方,欢迎评论区补充,纠错。如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值