C++进阶 — 哈希的应用

目录

一、位图

 1. 位图概念

 2. 位图的实现

 3. 位图的应用

 4. 位图的优缺点

二、 布隆过滤器

 1. 布隆过滤器概念

 2. 布隆过滤器的实现

 3. 布隆过滤器优缺点

三、 海量数据面试题

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即不存在。

C++进阶 — 哈希的应用_第1张图片

 1. 位图概念

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

 2. 位图的实现

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

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

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

C++进阶 — 哈希的应用_第2张图片


template
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 _bs;
};

 3. 位图的应用

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

 4. 位图的优缺点

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

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

二、 布隆过滤器

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

 1. 布隆过滤器概念

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

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

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

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

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

 2. 布隆过滤器的实现

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

C++进阶 — 哈希的应用_第4张图片

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

C++进阶 — 哈希的应用_第5张图片C++进阶 — 哈希的应用_第6张图片

        查找这里也是找该值在位图中映射的三个是否都为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
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 _bf;
};

C++进阶 — 哈希的应用_第7张图片

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

完整代码:这里有当前布隆过滤器的误判率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分到其他的文件去,文件变小了,问题就处理了。

C++进阶 — 哈希的应用_第8张图片

 2. 位图应用

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

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

C++进阶 — 哈希的应用_第9张图片

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

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

C++进阶 — 哈希的应用_第10张图片
   2.3 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数?

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

C++进阶 — 哈希的应用_第11张图片

 3. 布隆过滤器

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

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

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

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


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

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

你可能感兴趣的:(C++进阶笔记,哈希算法,数据结构,算法)