【C++】哈希(位图,布隆过滤器)

今天的内容是哈希的应用:位图和布隆过滤器a31ac5e10a734ee6a6b4ec5d54248eae.gif


目录

一、位图

1.位图概念

2.位图的应用

二、哈希切分

三、布隆过滤器

1.布隆过滤器的概念

2.布隆过滤器的应用

四、总结


 

一、位图

1.位图概念

今天的内容从一道面试题开始引入:

给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在
这40亿个数中。
 
首先我们对40亿个无符号整数改变一下,它到底是多少G呢?
 
40亿个整数大概是  40亿*4个字节=160亿个字节
 
4G=2^32byte,大概为42亿九千万字节,所以1G大概就是10亿字节 ,所以40亿个整数大概就是16G,那这么大数据放到内存中肯定是放不下的,所以什么二分查找,什么map,set更何况还有额外的消耗,这更不可能完成了,于是我们可以利用哈希的思想来搞一个位图!
 
        判断数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0
代表不存在。
 

位图是一种直接定址法的哈希,因此效率很高,用O(1)就可以探测到对应位是0还是1,效率非常高,因此可以快速判断。

 

利用每一个比特位的0或1的情况,来判断数在不在,所以40亿不重复的数,开辟2^32-1个比特位,转化为G,也就512m,内存很小。

ea7d7fb0a5e846aab184583d7bddf21d.png

举例说明:

 

4dd4a283419944949cb5d6e616912a9d.png 每个数我们可以先num/8,算出他在第几个char里,然后再num%8算出在哪一位
比如:23/8=2,在第二个char;23%8=7,在第七位上面。

那如何把任意一位置1,且不改变其他位?

95c5eb9618d6410d991433853d0ece97.png

把它和左移(向高位移动)以后的1(即其他位是0,只有要改变那一位是1)和原来的数进行或运算,就可以得到结果。保证了其他位不变,只有该位被改变为了1.

那到底怎么移动呢?

67e2fd00263f4badbd96af2ea3e6707c.png(一个char中)

 

那可能有人就会想,这会不会跟大小端有关系,数据在内存中的存储形式???

错错错,大错特错,首先大小端只存在于大于1字节的数据类型中,其次不管从哪边移动,本质是向高位或者低位移动。

所以说,%8以后,是哪一位,1直接左移几位(即向高位移动)。

 

那么在把某一位置为1以后,要重新置为0的话,应该怎么搞呢?

同理得:直接将1移位以后,再取反,将结果和原数进行与运算。

 

那要测试这个数在不在位图中,怎么测试呢?也就是看某一位是不是1

直接返回  1移位以后和原数相与的结果,不为0则存在,为0则不存在。

 

我们来看代码实现:


template<size_t N>
	class bitset
	{
	public:
		bitset()
		{
			//_bit.resize((N/8) + 1, 0);
			_bit.resize((N >> 3) + 1, 0);//左移3位就相当于/8,效率更快一些,但要注意运算符的优先级
		}

		void set(size_t x)
		{
			size_t i = x >> 3;
			size_t j = x % 8;
			_bit[i] |= (1 << j);//在知道是哪一个char之后,直接把这一个char相与。
		}

		void reset(size_t x)
		{
			size_t i = x >> 3;
			size_t j = x % 8;
			_bit[i] &= (~(1 << j));
		}
		bool test(size_t x)
		{
			size_t i = x >> 3;
			size_t j = x % 8;
			return _bit[i] & (1 << j);
		}
	private:
		vector<char> _bit;
	};

2.位图的应用

1. 给定100亿个整数,设计算法找到只出现一次的整数?
 
统计次数的话,那么就需要两个位图来实现,两个比特位来统计00(0次),01(1次),10(2次及以上)。
7c7358063a1b4cf0ba1f70f1c4c1a733.png
直接上代码:
template<size_t n>
	class two_bitset
	{
	public:
		void set(size_t x)
		{
			if (!_bs1.test(x) && !_bs2.test(x))//00
			{
				_bs2.set(x); //0次变1次
			}
			else if (!_bs1.test(x) && _bs2.test(x))//01
			{
				_bs1.set(x);
				_bs2.reset(x);//1次变两次
			}
		}
		void printonce()
		{
			for (size_t i = 0; i < n; ++i)
			{
				if (!_bs1.test(i) && _bs2.test(i))
				{
					cout << i << endl;
				}
			}
			cout << endl;
		}
	private:
		bitset<n> _bs1;
		bitset<n> _bs2;
	};

一个数如果在两个位图中的同一位置都是0,那么说明就是0次 ,再进来的数就要将00第二位set为01,表示出现一次,后面同理可得。

2. 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
首先我们知道,整数的范围最大是42亿多,所以100亿个整数中,一定存在许多重复的整数。
所以将文件中的数据都放入位图中,只看 存在或者不存在两种状态,这就自动去掉了重复的数,某一位一直是1。只有512MB,可以存入内存中进行处理。
然后两个位图进行相与操作,同时为1说明是交集。
 
3. 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整
首先我们知道,整数的范围最大是42亿多,所以100亿个整数中,一定存在许多重复的整数。
所以将文件中的数据都放入位图中,只看 存在或者不存在两种状态,占用内存很少。
通过两个位图来表示次数,00(0次),01(1次),10(2次),11(3次及以上),然后控制条件(只找01,10)输出结果,和第一个问题其实是一样的。

二、哈希切分

还是一道面试题来引入哈希切分:

给一个超过100G大小的log fifile, log中存着IP地址, 设计算法找到出现次数最多的IP地址?
 
由前面所学,我们可能会想到位图,但是行不通,要统计次数就要开辟多个位图,
7c7358063a1b4cf0ba1f70f1c4c1a733.png
成倍的开辟位图来表示次数的话,会占用大量的内存空间,内存也存不下。
 
我们用的是map,但是在用map之前,要把大文件处理:
 
那我们就可以利用哈希的思想来把100G的文件分成100个小文件,每个1G,那么不就可以进内存了吗?
 
那怎么分???平均分?那当然不行,平均分对于分散的ip地址,都不在同一个小文件中,进入内存用map统计时,结果是不正确的。
 
直接哈希切分!!
ab9afb39fdfd4ef291128eddc6150928.png

 我们可以对100G大文件中的ip进行哈希切分,利用哈希表的思想,将哈希值相同的放入同一个小文件中,然后通过一个一个的小文件进入内存读取并统计个数,搞完一个clear掉,记录再进下一个。

理想很美好,现实却有点骨感?

单个文件超过1G:

因为存在哈希冲突,在数据进入小文件时,就会产生下面两种情况:

        1.一个小文件中,差不多都是不重复的数据,且个数还挺多,且map再加额外开销,导致内存很大,直接报错。

        2.一个小文件中,都是很多重复的数据,且个数还挺多,但是map却可以存下(重复的只增加次数),可以统计。

 

所以我们无需判断是哪种情况,直接无脑map,第一种情况发生就抛异常,捕获以后,换另一种哈希函数,再进行递归分割,拆成更小的文件后用map统计次数。

你懂了吗?99a3372c120248e988246197668c6d39.gif

与上题条件相同,如何找到top K的IP?如何直接用Linux系统命令实现?
利用堆来解决topK问题。

三、布隆过滤器

1.布隆过滤器的概念

开始讲布隆过滤器之前,我们要说一说位图的缺点是什么?

最大的缺点就是:1.开空间得看数据范围,一般要求范围集中,分散的话空间消耗就会上升

                             2.只能针对整型

如果给了一堆字符串,可不可以使用位图判断是否存在呢?

当然可以,可以使用哈希函数,将字符串转化为整型,再去映射到位图中。

当针对字符串来判断是否存在时,位图+哈希其实就是我们要讲的布隆过滤器。

布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的
率型数据结构,特点是 高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存
在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式 不仅可以提升查询效率,也
可以节省大量的内存空间
 
话不多说,上例子来理解这段话:
769846771ca0448f9e778f137b7d6d1b.png
 
当不同的字符串通过哈希函数转化为整型映射到位图中时,就会发生哈希碰撞!
比如find通过哈希函数可能和insert映射到同一位置,那么当find不存在时,但是他的位置的确已经被置为1,所以这就导致了:
                         判断存在是不准确的
                        判断不存在一定是准确的,因为位置是0,那一定不存在
7114a1caefd0494e8e2aa6f16db365b3.png

于是,我们就要想一些办法,让他的误判率低一些:

可以增加不同的哈希函数,转化为不同的哈希值,去映射到多个位置,降低误判率

bdd28914f4ee4493ab92a9c47ea36572.png

 这样的话,我们可以看到,只有当一个字符串映射的全部位置都置为1时,这个数才可能存在,说的是可能存在,因为也可能存在哈希碰撞。但降低了哈希碰撞的概率,降低了误判率。cb744e0dcd6447f1a58483d165f5ea5c.gif

那还有问题就是:一个字符串映射多个位图的位置,那位图应该开多大呢?

                             或者说如何选择哈希函数个数和布隆过滤器长度?

直接上大佬:大佬研究出来的一个公式:

9b929a81fde64d5e9ffcafe45e74f389.png

 现在来实现布隆过滤器:

f7006d4cdf994d91989a52bbb46874f0.png

template<
		size_t N,
		size_t M,
		class K = string,
		class Hash1= BKDRHash,
		class Hash2= APHash,
		class Hash3 = DJBHash>
	class Bloomfilter
	{
	public:
		void set(const K& key)
		{
			size_t i = Hash1()(key) % (N * M);
			size_t j = Hash2()(key) % (N * M);
			size_t k = Hash3()(key) % (N * M);

			_bs.set(i);
			_bs.set(j);
			_bs.set(k);
		}

		//void reset() 没有reset的原因是:因为存在哈希冲突,修改一个数的哈希值映射位置的值,会影响到其他的数,导致结果不准确。
		//硬要有reset,就需要计数,通过计数(--)来控制,那就需要成倍的位图来表示个数,严重浪费内存空间。

		//布隆过滤器,存在哈希冲突,所以确定不了一定存在的值
		//但是可以确定一定不存在的值
		bool test(const K& key)
		{
			size_t i = Hash1()(key) % (N * M);
			if (!_bs.test(i))
			{
				return false;
			}
			size_t j = Hash2()(key) % (N * M);
			if (!_bs.test(j))
			{
				return false;
			}
			size_t k = Hash3()(key) % (N * M);
			if (!_bs.test(k))
			{
				return false;
			}
			//到这里说明可能存在
			return true;
		}
	private:
		bitset <N* M> _bs;
	};

那布隆过滤器支持删除吗?当然不支持!

没有reset(不可以删除)的原因是:因为存在哈希冲突,修改一个数的哈希值映射位置的值,会影响到其他的数,导致结果不准确。

硬要有reset,就需要计数,通过计数(--)来控制,那就需要成倍的位图来表示个数,严重浪费内存空间。

2a3f5ccb62c44bcd9447367116781255.png

如上图所示这样实现 

2.布隆过滤器的应用

1.日常应用中,最常见的场景:

8ba05ab8855942ccb571aa8c9dc9559d.png

当数据量比较大时,会存放在磁盘中,磁盘访问速度相对来说很慢,所以在客户端和服务器中间加入布隆过滤器就会很大程度上加快访问速度,提高效率。

在过滤器阶段,数据不存在时,直接返回不存在;存在时,是可能存在(因为存在哈希冲突),所以会继续访问磁盘中的数据,数据在磁盘中存在即存在,不存在返回不存在。

2. 给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出
精确算法和近似算法。
 
query-般是查询指令,比如可能是一个网络请求, https://zhuanlan.zhihu.com/p/43263751/
或者是一个数据库sq|语句
假设平均每个query是50byte, 100亿个query合计多少内存? -- 500G
 
精确算法:交集中一定是准确的(哈希切分)
近似算法:那么一定是允许有误判的情况(有误差),那么就可以使用布隆过滤器。
 
当看到这个题目时,可能就会想到位图来解决,但是100亿个字符串都是不相同的,100亿个字符串已经超过了1G,不可行。
 
精确算法:
利用哈希函数,将100亿个query是500G,因为要到内存中比较两个文件,所以需要分为1000个小文件,每个小文件占用0.5G,那么两个小文件就可以都进内存中比较了。
如图所示:
5ce3cf30dd31485cbcbe258cde812f10.png

 当然也会出现哈希冲突超过0.5G的情况,若是重复数较多,但是我们是找交集,所以用位图来存或不在时,0.5G的小文件中数据个数占的内存一定小于0.5G,然后两个位图相与即可。

但如果是都不重复,就需要递归继续分割。用位图找交集

四、总结

不同的场景需要我们灵活的去找适合的方法去解决问题。

我们下期再见!3c06ef1b9607446d91d70f84d8b371c0.gif

 

评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

The s.k.y.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值