【c++】: 哈希的应用


一.位图

位图法就是bitmap的缩写,所谓bitmap,是用每一位来存放某种状态,适用于大规模数据,但数据状态又不是很多的情况。通常是用来判断某个数据存不存在的。

位图特点:
1.快,节省空间
2.相对局限,只能处理整形

1.1 位图的应用

1. 快速查找某个数据是否在一个集合中
2. 排序 + 去重
3. 求两个集合的交集、并集等
4. 操作系统中磁盘块标记

例如:给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在
这40亿个数中。

1G = 1024MB    1MB = 1024KB    1KB = 1024 Byte    1 Byte = 8 bit
那么我们先看看40亿个不重复的无符号整数会占多少内存

40亿整数 = 160亿字节
1G = 1024MB = 102410241024 Byte = 2^30 Byte ,就约等于10亿多字节
那么40亿个整数大概会占15~16G的内存

我们看下面的方法,在对于数据量太大的情况下都效果一般

  1. 排序(O(NlogN)),利用二分查找: logN (但由于数据量太大,只能存在磁盘中,不方便支持二分查找,效率低)
  2. 探索树和哈希表(由于探索树与哈希表中不止会存数据,还会存储其他的事物(如探索树会存储左右节点的指针),在使用时就可能会导致内存不足

对于这类题,使用位图去解决就比较好

数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0代表不存在.

在这里插入图片描述
我们用位图的话就只需要(2^32 -1)个bit位,
(2^32 - 1)bit 约等于 512 MB,就相比上面的几种方法,内存消耗就小了许多

1.2 位图的实现

位图最主要功能的实现

	template<size_t N>
	class bitset
	{
	public:
		bitset()
		{
			//多开一个字节的空间,防止出现10/8 = 0的情况
			_bits.resize(N/8 + 1, 0);
		}

		void set(size_t x)
		{
			// x/8 在第几个char中
			// x%8 在该char的第几个位置上 
			size_t i = x / 8;
			size_t j = x % 8;

			_bits[i] |= (1 << j);

		}
		
		void reset(size_t x)
		{
			size_t i = x / 8;
			size_t j = x % 8;

			_bits[i] &= ~(1 << j);
		}

		bool test(size_t x)
		{
			size_t i = x / 8;
			size_t j = x % 8;

			return _bits[i] & (1 << j);
		}
	private:
		vector<char> _bits;
	};

二. 布隆过滤器

2.1 布隆过滤器的提出

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


1. 用哈希表存储用户记录,缺点:浪费空间
2. 用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理
了。
3. 将哈希与位图结合,即布隆过滤器

2.2 布隆过滤器概念

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

2.3 布隆过滤器的插入

插入原理
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
模拟实现

//用多个Hash仿函数计算出多个散列值
	struct HashBKDR
	{
		size_t operator()(const string& key)
		{
			
			rsize_t val = 0;
			for (auto ch : key)
			{
				val *= 131;
				val += ch;
			}
			return val;
		
		}
	};

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

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

	template<size_t N,
		class K = string,class Hash1 = HashBKDR,
		class Hash2 = HashAP,class Hash3 = HashDJB>
	class BloomFilter
	{
	public:
		void Set(const K& key)
		{
			size_t hash1 = Hash1()(key) % (_ratio * N);
			_bits.set(hash1);
			
			size_t hash2 = Hash2()(key) % (_ratio * N);
			_bits.set(hash2); 

			size_t hash3 = Hash3()(key) % (_ratio * N);
			_bits.set(hash3); 

		}
		bool Test(const K& key)
		{
			size_t hash1 = Hash1()(key) % (_ratio * N);
			size_t hash2 = Hash2()(key) % (_ratio * N);
			size_t hash3 = Hash3()(key) % (_ratio * N);
			
			if(_bits.test(hash1) && _bits.test(hash2)&& _bits.test(hash3))
			{
				return true;//可能存在误判
			}
			return false;//准确的
			
		}
	private:
		const static size_t _ratio = 5;
		//插入一个值需要开的位数,哈希函数个数改变,这个也得改
		bitset<_ratio*N>  _bits;//实际存储个数
	};

下面的是一篇写布隆过滤器的原理,使用场景和注意事项的文章链接,可以知道这里的_ratio是怎么来的。
https://zhuanlan.zhihu.com/p/43263751/

2.4 布隆过滤器的查找

布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特
位一定为1。所以可以按照以下方式进行查找: 分别计算每个哈希值对应的比特位置存储的是否为零,只要有一个为零,代表该元素一定不在哈希表中,否则可能在哈希表中。

注意:布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可
能存在,因为有些哈希函数存在一定的误判。

比如:在布隆过滤器中查找"alibaba"时,假设3个哈希函数计算的哈希值为:1、3、7,刚好和其
他元素的比特位重叠,此时布隆过滤器告诉该元素存在,但实该元素是不存在的。

其查询元素的过程如下:

1. 通过k个无偏hash函数计算得到k个hash值
2. 依次取模数组长度,得到数组索引
3. 判断索引处的值是否全部为1,如果全部为1则存在(这种存在可能是误判),如果存在一个0则必定不存在

2.5 布隆过滤器删除

布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。

比如:删除上图中"tencent"元素,如果直接将该元素所对应的二进制比特位置0,“baidu”元素也
被删除了,因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。

三 海量数据的题目

3.1 位图的题目

1.给定100亿个整数,设计算法找到只出现一次的整数?

方法一:
由于需要找只出现一次的整数,就不能单纯的向上面一样只记录出现了没有,我们这里要记录能出现一次的,那么我们就可以将它分成【出现0次,出现1次,出现2次及以上】的三种情况,那么我们在设计的时候就只需要2个比特位来映射一个值就行了
在这里插入图片描述

方法二:
对于这种,除了用2在一个位图中用两个比特位来映射以外,我们还能用2个位图用相同的一个比特位来进行组合

在这里插入图片描述

template<size_t N>
	class twobitset
	{
	public:
		void set(size_t x)
		{
			bool insert1 = _bs1.test(x);
			bool insert2 = _bs2.test(x);

			//00
			if (insert1 == false && insert2 == false)
			{
				//->01
				_bs2.set(x);
			}
			//01 
			else if (insert1 == false && insert2 == true)
			{
				//->10
				_bs1.set(x);
				_bs2.reset(x);
			}
		}

		void print_once_num()
		{
			for (size_t i = 0; i < N; i++)
			{
				if (_bs1.test(i) == false && _bs2.test(i) == true)
				{
					cout << i << endl;
				}
			}
		}
	private:
		bitset<N> _bs1;
		bitset<N> _bs2;

	};
	void Test_twobitset()
	{
		int arr[] = { 1,2,3,1,2,3,5,6,8 };
		twobitset<100> tws;
		for (auto e : arr)
		{
			tws.set(e);
		}
		tws.print_once_num();
	}

2. 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?

,先将2个文件都分成n个小文件,再用2个位图对相应的小文件文件进行映射,每2个相应的小文件在位图中的位置都为一的都是2个文件的交集(哈希切分)
在这里插入图片描述

3. 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
这道题与题目一类似,只不过这道题的分类变为了【出现0次,出现1次,出现2次,出现3次及以上】这4种情况,就需要用 00(0次),01(1次),10(2次),11(3次及以上)来代表这4种情况。

	template<size_t N>
	class twobitset
	{
	public:
		void set(size_t x)
		{
			bool insert1 = _bs1.test(x);
			bool insert2 = _bs2.test(x);

			//00
			if (insert1 == false && insert2 == false)
			{
				//->01
				_bs2.set(x);
			}
			//01 
			else if (insert1 == false && insert2 == true)
			{
				//->10
				_bs1.set(x);
				_bs2.reset(x);
			}
		}

		void print_once_num()
		{
			for (size_t i = 0; i < N; i++)
			{
				if ((_bs1.test(i) == false && _bs2.test(i) == true)||
					_bs1.test(i) == true && _bs2.test(i) == false)
				{
					cout << i << endl;
				}
			}
		}
	private:
		bitset<N> _bs1;
		bitset<N> _bs2;

	};

3.2 布隆过滤器的题目

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

哈希切分
1.假设每个query 占30byte,那么100亿个query约要占300G的内存
2.我们需要将2个300G内存的数据文件,只用1G的内存找他们的交集,我们把这两个大文件A,B分别按照hash切分,分成1000分的小文件,这样就可以放到内存中,然后我们拿A中的小文件分别和B中的比对,这样就可以找到两个文件的交集
在这里插入图片描述

估计算法步骤:

(1) 通过字符串哈希算法,将字符串转换成数字
(2) 通过散列函数将这些数字映射到内存中
(3)判断映射的位置是否都存在,存在表明有交集.

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

缺陷:

  1. 无法确认元素是否真正在布隆过滤器中
  2. 存在计数回绕

3.3 哈希切割

给一个超过100G大小的log file, log中存着IP地址,

设计算法找到出现次数最多的IP地址?
我们先考虑一下,100G大小的文件,一般是无法存到普通的计算机中的,我们的硬盘根本没这么大; 我们可以使用前面讲的位图,一个整形32位,最多可以存42亿多的数据,100G的大文件,最多需要3.2G就可以放进去,但是却难以统计最多的IP地址。
为了解决上面的问题,我们可以把大文件放到小文件中,再来统计就会很容易 。

算法思想:哈希切分

(1)哈希函数hash(IP)%1000个文件,这样大文件就可以分成1000个小文件。
(2)字符串哈希函数将字符串转换为整数
(3)通过同一个散列函数映射到相应的文件,此时同一个ip一定会映射到同一个文件
(4)用<ip,次数>统计,用map或者unordered_map实现

在这里插入图片描述

与上题条件相同,如何找到top K的IP?

(1)本题采用哈希切分,如上题,统计每个ip出现的次数
(2)用文件的前K个数建小堆
(3)用K+1个数和堆顶相比,大的话替换,调整堆

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值