C++进阶 — 哈希的应用

目录

一、位图

 1. 位图概念

 2. 位图的实现

 3. 位图的应用

 4. 位图的优缺点

二、 布隆过滤器

 1. 布隆过滤器概念

 2. 布隆过滤器的实现

 3. 布隆过滤器优缺点

三、 海量数据面试题

扫描二维码关注公众号,回复: 14730201 查看本文章

1.哈希切割

  1.1 给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?

 2. 位图应用

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

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

​   2.3 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数?

 3. 布隆过滤器

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

   3.2 如何扩展BloomFilter使得它支持删除元素的操作


一、位图

        我们将通过一个面试题来了解位图:

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

这个题有两个重点:一是数不重复,二是判断在不在,那么此题该怎么解呢?

遍历?排序+二分查找?还是哈希表,红黑树?这里一个很严重的问题就是数据量太大,要存储数据的话内存存不下,那么该如何解决呢?

这时用位图就很方便,我们需要开一个范围大小的比特位,一个无符号整数大小的范围,每一个整数映射一个比特位,就相当于直接定址法。无符号整数的范围是42亿9千多万,将其转换为空间,也就是说需要一个512M大小的空间即可。

一个数存不存在,我们到映射的那个比特位去看,如果为1即存在,如果为0即不存在。

 1. 位图概念

所谓位图,就是用比特位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。
 

 2. 位图的实现

        通过位运算将映射的比特位更改成需要的状态,比如set,无论该比特位上的状态是0,还是1,都将该位改成1。那么怎么实现更改呢?range可以帮我们确定在哪个char的范围中,place可以帮我们确定具体位置,将1左移到映射的比特位下,再进行按位或运算。注:|(按位或) 两个比特位中有一个是1,结果为1。

        reset是将映射的比特位的状态改为0,找到映射位置,先将1左移到映射的比特位下,再按位取反,此时除了映射的比特位下是0,其余位全是1,将取反后的值进行位运算。注:&(按位与):两个比特位中都是1,结果为1,任意一个为0,结果为0。

        test是检查该值是否存在,存在返回真,不存在返回假。


template<size_t N>
class BitSet
{
public:
	BitSet()
	{	//因为除8后可能会丢掉后面的小数,所以+1,保证开足够的空间
		_bs.resize(N / 8 + 1, 0);
	}
	//将映射位置的比特位改为1
	void set(size_t x)
	{
		size_t range = x / 8;
		size_t place = x % 8;
		_bs[i] |= (1 << place);
		
	}
	//将映射位置的比特位改为0
	void reset(size_t x)
	{
		size_t range = x / 8;
		size_t place = x % 8;
		_bs[i] &= (~(1 << place));
	}
	//检查映射位置的比特位是否为真
	bool test(size_t x)
	{
		size_t range = x / 8;
		size_t place = x % 8;
		return _bs[i] & (1 << place);
	}
private:
	vector<char> _bs;
};

 3. 位图的应用

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

 4. 位图的优缺点

优点:节省空间,查找存不存在速度快

缺点:只能针对整形,且一般要求范围集中,如果范围特别分散,空间消耗就会很大。

二、 布隆过滤器

        位图可以很快速的帮我们找到某个数是否存不存在,但是它只能存放整形,如果我们要查找字符串怎么办呢?将哈希和位图结合就出现了布隆过滤器。

 1. 布隆过滤器概念

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

        如果只用一个哈希函数求映射值,由于字符串可以无限组合,所以肯定会有两个不同的字符串映射到同一个位置上,那么就会出现误判,什么是误判?一个字符串本来不存在过滤器中,由于它映射的那个位置别的字符串已经映射过了,所以在查找的时候会返回真,也就是这个字符串在过滤器中了,这就是误判。

为了降低误判的概率,我们可以把一个值通过不同的哈希函数映射在多个位置上,但是无论哈希函数再多也不会消除误判,而且函数函数增多以后,空间的消耗也会成倍数上升。

文章是关于各种哈希函数的讲解和效率对比,有兴趣可以看看:https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html

 文章是布隆过滤器的原理,使用场景和注意事项,有兴趣的可以看看:详解布隆过滤器的原理,使用场景和注意事项 - 知乎

 2. 布隆过滤器的实现

这里哈希函数我用三个,布隆过滤器开多少空间是通过一个公式算出来的,上面的文章里有详细说明。

        插入就是将该值求出的三个映射位置的比特位都改为1。

        查找这里也是找该值在位图中映射的三个是否都为1,如果都为1,则可能存在(因为会有误判),但是只要有一个映射位置不是1,那么该值一定不存在!

        这里为什么没有删除?是因为删除一个值,要把它映射位置的比特位改为0,这样可能会影响其他值,所以没有实现删除函数。布隆过滤器官方也没有给具体的实现,如果需要的话就自己写一个吧,也不难。

struct BKDRHash
{
	size_t operator()(const string& s)
	{
		// BKDR
		size_t value = 0;
		for (auto ch : s)
		{
			value *= 131;
			value += ch;
		}
		return value;
	}
};
struct DJBHash
{
	size_t operator()(const string& s)
	{
		unsigned int hash = 5381;

		for(auto& ch: s)
		{
			hash += (hash << 5) + ch;
		}
		return hash;
	}
}; struct APHash
{
	size_t operator()(const string& s)
	{
		unsigned int hash = 0;
		int i = 0;
		for (auto& ch :s)
		{
			if ((i & 1) == 0)
			{
				hash ^= ((hash << 7) ^ ch ^ (hash >> 3));
			}
			else
			{
				hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));
			}
			++i;
		}
		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 hash1 = HashFunc1()(key) % (N * 5);
		size_t hash2 = HashFunc2()(key) % (N * 5);
		size_t hash3 = HashFunc3()(key) % (N * 5);

		_bf.set(hash1);
		_bf.set(hash2);
		_bf.set(hash3);
	}
	bool test(const K& key)
	{
		size_t hash1 = HashFunc1()(key) % (N * 5);
		if (!_bf.test(hash1))
			return false;

		size_t hash2 = HashFunc2()(key) % (N * 5);
		if (!_bf.test(hash2))
			return false;

		size_t hash3 = HashFunc3()(key) % (N * 5);
		if (!_bf.test(hash3))
			return false;

		return true;	//存在但是会有误判
	}
private:
	std::bitset<N * 5> _bf;
};

布隆过滤器可以通过增加哈希函数的数量,或者改变布隆过滤器的长度,以此减小误判率。

完整代码:这里有当前布隆过滤器的误判率bitSet/bitSet/BloomFilter.h · 晚风不及你的笑/作业库 - 码云 - 开源中国 (gitee.com)

 3. 布隆过滤器优缺点

优点:

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

缺点:

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

三、 海量数据面试题

1.哈希切割

  1.1 给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?

100G大小的文件,这个我们直接用map,内存肯定放不下,那么怎么办呢?先用哈希切割将100G大文件切割成100个小文件,那么IP相同或者冲突的就都在一个文件里了。那么怎么分割呢?用一个仿函数把IP转化为整数,再进行取模得到映射位置,之后再将IP存入映射的文件中。将每个小文件依次读取到内存,用map去重后统计次数,读取完后再获取最多的那个IP就可以了。

这里为什么用哈希切割,不用平均分割呢?因为内存就那么大,大文件平均分割以后,相同的IP会出现在不同的文件里,此时统计次数就不准了,所以不能使用平均分割。

这里需要注意的是分割完成后,某个小文件的大小超过了1G怎么办?

此时还分两种情况处理:1.小文件里全是一个或多个IP重复,那么此时map是可以存放的,2.小文件里大多是不重复的IP,但冲突到这个小文件里了,此时map是存不下的。如果是第一种,我们可以直接用map存储,如果是第二种情况map存储不了,因为内存不够,会抛异常,我们可以将该小文件换一个哈希函数再次进行映射,将冲突的IP分到其他的文件去,文件变小了,问题就处理了。

 2. 位图应用

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

  •         通过分析此题会有三种状态:1.出现0次,2.出现一次,3.出现2次及以上的。
  •         虽然是100亿个整数,但是整数范围最大只有42亿9千多万,所以不用担心内存放不下的情况。而一个位图只有两种状态,那怎么办呢?我们可以开两个位图,如下图,上下组成一组,插入数据时可以分状态进行,如状态1:上下都为0,那么插入只改变bst2的比特位,如状态2,上0下1,那么将bst1里的比特位改为1,bst2里的比特位改为0,插入的时候是状态3则不需要改变,因为题目只要求找出现一次的,此时已经出现两次了,不符合要求了。
  •         查找可以通过检查映射位置的状态进行判断,也可以通过遍历位图检查状态来判断。

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

这个题虽然只有1G内存,但是整数范围最大只有42亿9千多万,也就是要开512M大小的位图。因为有两个文件,所以要开两个位图,先插入进行去重,再通过遍历的方式查看两个位图的状态,都两个都为1,就有交集,也可以将两个位图进行按位与运算,完成后比特位上还是1的就是交集。


   2.3 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数?

这个题是第一次的变形,题目要求找出不超过两次的整数,通过分析得出会出现4种状态:1.出现0次,2.出现1次,3.出现2次,4.出现3次及以上的。只需要插入和查找的时候多进行一次判断即可。

 3. 布隆过滤器

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

query一般是查询指令,比如是一个网络请求,或者是一个数据库sql语句。

精确算法:这里还是一样,用哈希切割的方式将A文件和B文件切割成若干个小文件,切割时要用同一个哈希函数,将映射关系相同的A小文件和B小文件分别插入不同的map中,去重后再找两个小文件的交集,然后换下一份小文件,直至结束。这里依旧会出现某个小文件特别大的情况,这时再换一个哈希函数进行映射,跟哈希切割那道题一样。

近似算法:因为文件很大,所以还需要先切割成小文件,再用布隆过滤器插入去重,然后找文件的交集,当然布隆过滤器会有误判。


   3.2 如何扩展BloomFilter使得它支持删除元素的操作

将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。但是此方法还是有缺陷:1. 无法确认元素是否真正在布隆过滤器中,2. 存在计数回绕。

猜你喜欢

转载自blog.csdn.net/weixin_68993573/article/details/129420041
今日推荐