C++之哈希表的应用(位图,布隆过滤器,海量数据处理,哈希切割)

1.哈希的应用

1.1位图

1.1.1 位图概念

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

思路一:先进行排序,再将排序过后的数进行二分查找,排序的时间复杂度是(N*logN),二分查找的时间复杂度是(logN)

思路二:放进set或者unordered_set,再进行查找

上面两种思路似乎看上去都是可行的,但是我们知道40亿个无符号整数总共占16GB,对内存的消耗是巨大的。

思路三:利用位图解决内存消耗过大问题

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

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

1.1.2 位图的实现

template<size_t N>
class BitSet
{
public:
	BitSet()
	{
		_bits.resize(N / 32 + 1, 0);
		//这里一定要注意多开一位,因为除以32是向下取整了,那么我们需要再多给一个位置
	}
	void Set(size_t x)
	{
		assert(x < N);
		size_t i = x / 32;// 算出x映射的位在第i个整数
		size_t j = x % 32;// 算出x映射的位在这个整数的第j个位
		_bits[i] |= (1 << j);
		//注意这里一定要加或(或操作数中有一个1则结果为1),用1进行左移然后或等赋值给这一位数
	}
	void Reset(size_t x)
	{
		assert(x < N);
		size_t i = x / 32;// 算出x映射的位在第i个整数
		size_t j = x % 32;// 算出x映射的位在这个整数的第j个位

		_bits[i] &= (~(1 << j));
		//这里先进行移位然后取反是为了把我们需要置位的位置弄成0,然后其他位置都为1,方便后续的与操作
		//注意这里的&(这里采用与操作是为了不影响操作数中原本的1,并还要把对应位置的1置为0)
	}
	bool Test(size_t x)
	{
		assert(x < N);
		size_t i = x / 32;// 算出x映射的位在第i个整数
		size_t j = x % 32;// 算出x映射的位在这个整数的第j个位

		//两个为1的数相与后结果为1,说明这个数存在
		return _bits[i] & (1 << j);
		// 如果第j位是1,结果是非0,非0就是真
		// 如果第j为是0,结果是0,0就是假
	}
private:
	vector<int> _bits;
};

1.1.3 位图的应用

位图的常见应用场景:

快速查找某个数据是否在一个集合中;排序;求两个集合的交集,并集;操作系统中磁盘块标记等
1. 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?

方案一:依次读取第一个文件中的所有数据标记映射到一个位图中,再依次读取另一个文件中的所有整数,判断在不在这个位图中,在就是交集,不在就不是

方案二:依次读取第一个文件中所有的数据标记映射到位图1,再依次读取第二个文件中所有的数据标记映射到位图2.再对两个位图进行与操作(依次与位图中整数即可),与完过后还是1的位映射的整数就是交集.

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

这个问题相较于上一个题判断在不在的问题就复杂一些,因为我们采用一个二进制比特位只能判断在或者不在的情景,这里需要我们找出只出现一次的整数,那么我们就可以采用两个比特位来映射一个整数的方式:

标记一个整数的几种状态:

出现0次:标记为00

出现1次,标记为01

出现2次及以上:标记为10

void Set(size_t x)
{
	BitSet<-1> _bit1;//都开到42亿整数个位
	BitSet<-1> _bit2;

	//00->01
	if (!_bit1.Test(x) && !_bit2.Test(x))
	{
		_bit2.Set(x);
	}
	//01->10
	else if(!_bit1.Test(x) && _bit2.Test(x))
	{
		_bit1.Set(x);
		_bit2.Reset(x);
	}
	else if (_bit1.Test(x) && !_bit2.Test(x))
	{
		//这里可以不做任何处理
	}
	else
	{
		assert(false);
	}
}


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

这个问题和上面的就很类似,不过需要统计的是出现两次的而已,思路都是一样的

标记一个整数的几种状态:

出现0次:标记为00

出现1次,标记为01

出现2次:标记为10

出现3次及以上:标记位11

void Set(size_t x)
{
	BitSet<-1> _bit1;//都开到42亿整数个位
	BitSet<-1> _bit2;

	//00->01
	if (!_bit1.Test(x) && !_bit2.Test(x))
	{
		_bit2.Set(x);
	}
	//01->10
	else if(!_bit1.Test(x) && _bit2.Test(x))
	{
		_bit1.Set(x);
		_bit2.Reset(x);
	}
	//10->11
	else if (_bit1.Test(x) && !_bit2.Test(x))
	{
		_bit2.Set(x);
	}
	//11的情况
	else
	{
		//不做处理
	}
}

总结:根据上面的位图应用不难看出位图的本质就是直接定址法哈希,每个整数映射一个比特位。

分析位图的优缺点:

优点:大量节省空间的消耗,搜索查找速度很快

缺点:只能处理整型,适用场景较为单一

那么下面就引申出位图的变形延申版——布隆过滤器

1.2 布隆过滤器

1.2.1 布隆过滤器提出

我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何快速查找呢?
方式1. 用哈希表存储用户记录,缺点:浪费空间
方式2. 用位图存储用户记录,缺点:不能处理哈希冲突
方式3. 将哈希与位图结合,即布隆过滤器

1.2.2布隆过滤器概念

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

 如下图:

1.2.3 布隆过滤器的插入

  

void Set(const K& key)
	{
		//将同一个字符串采用不同的哈希函数转换成三个不同的整形数
		size_t i1 = Hash1()(key) % N;
		size_t i2 = Hash2()(key) % N;
		size_t i3 = Hash3()(key) % N;

		//cout << i1 << " " << i2 << " " << i3 << endl;

		_bitset.Set(i1);
		_bitset.Set(i2);
		_bitset.Set(i3);//分别将算出来的整数对应到位图中并置1
	}

1.2.4 布隆过滤器的查找

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

bool Test(const K& key)
	{
		//依次判断每一位,只要存在一位不在就返回错,在是准确的
		size_t i1 = Hash1()(key) % N;
		if (_bitset.Test(i1) == false)
		{
			return false;
		}
		size_t i2 = Hash2()(key) % N;
		if (_bitset.Test(i2) == false)
		{
			return false;
		}
		size_t i3 = Hash3()(key) % N;
		if (_bitset.Test(i3) == false)
		{
			return false;
		}
		// 这里3个位都在,有可能是其他key占了,在是不准确的,存在误判
		return true;
	}

注意:布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可能存在,因为有些哈希函数存在一定的误判。
比如:在布隆过滤器中查找"alibaba"时,假设3个哈希函数计算的哈希值为:1、3、7,刚好和其他元素的比特位重叠,此时布隆过滤器告诉该元素存在,但是该元素是不存在的,所以判断元素在库中是不准确的,但是如果布隆过滤器告诉该元素不存在,那么一定不存在,因为相同字符串经过哈希函数过后算出来的整数都是一样的,所以判断元素不在库中是非常准确的.

1.2.5 布隆过滤器删除

布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。
比如:删除上图中"tencent"元素,如果直接将该元素所对应的二进制比特位置0,“baidu”元素也被删除了,因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。
一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。
缺陷:
1. 无法确认元素是否真正在布隆过滤器中
2. 存在计数回绕

显然删除操作用在布隆过滤器是十分不可靠的,尤其是在数据量很大的工程中.

1.2.6 布隆过滤器的模拟实现和测试代码

struct HashBKDR//哈希函数1
{
	size_t operator()(const std::string& s)
	{
		size_t value = 0;
		for (auto ch : s)
		{
			value += ch;
			value *= 131;
		}
		return value;
	}
};
struct HashAP//哈希函数2
{
	size_t operator()(const std::string& s)
	{
		register size_t hash = 0;
		size_t ch;
		for (long i = 0; i < s.size(); i++)
		{
			ch = s[i];
			if ((i & 1) == 0)
			{
				hash ^= ((hash << 7) ^ ch ^ (hash >> 3));
			}
			else
			{
				hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));
			}
		}
		return hash;
	}
};
struct HashDJB//哈希函数3
{
	size_t operator()(const std::string& s)
	{
		register size_t hash = 5381;
		for (auto ch : s)
		{
			hash += (hash << 5) + ch;
		}
		return hash;
	}
};
template<size_t N, class K = std::string,
	class Hash1 = HashBKDR,
	class Hash2 = HashAP,
	class Hash3 = HashDJB>
class BloomFilter
{
public:
	void Set(const K& key)
	{
		//将同一个字符串采用不同的哈希函数转换成三个不同的整形数
		size_t i1 = Hash1()(key) % N;
		size_t i2 = Hash2()(key) % N;
		size_t i3 = Hash3()(key) % N;

		//cout << i1 << " " << i2 << " " << i3 << endl;

		_bitset.Set(i1);
		_bitset.Set(i2);
		_bitset.Set(i3);//分别将算出来的整数对应到位图中并置1
	}
	bool Test(const K& key)
	{
		//依次判断每一位,只要存在一位不在就返回错,在是准确的
		size_t i1 = Hash1()(key) % N;
		if (_bitset.Test(i1) == false)
		{
			return false;
		}
		size_t i2 = Hash2()(key) % N;
		if (_bitset.Test(i2) == false)
		{
			return false;
		}
		size_t i3 = Hash3()(key) % N;
		if (_bitset.Test(i3) == false)
		{
			return false;
		}
		// 这里3个位都在,有可能是其他key占了,在是不准确的,存在误判
		return true;
	}
private:
	BitSet<N> _bitset;
};


void TestBloomFilter()
{
	/*BloomFilter<100> bf;
	bf.Set("张三");
	bf.Set("李四");
	bf.Set("牛魔王");
	bf.Set("红孩儿");

	cout << bf.Test("张三") << endl;
	cout << bf.Test("李四") << endl;
	cout << bf.Test("牛魔王") << endl;
	cout << bf.Test("红孩儿") << endl;
	cout << bf.Test("孙悟空") << endl;*/

	BloomFilter<8000> bf;//随着N的增加误判率会明显降低
	size_t N = 1000;

	//先插入一组字符串
	std::vector<std::string> v1;
	for (size_t i = 0; i < N; ++i)
	{
		std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html";
		url += std::to_string(1234 + i);
		v1.push_back(url);
	}
	for (auto& str : v1)
	{
		bf.Set(str);
	}

	//测试相似字符串是否在位图中存在的概率
	std::vector<std::string> v2;
	for (size_t i = 0; i < N; ++i)
	{
		std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html";
		url += std::to_string(6789 + i);
		v2.push_back(url);//将字符串放到一个数组中
	}
	size_t n2 = 0;
	for (auto& str : v2)
	{
		if (bf.Test(str))
		{
			++n2;//测试误判率
		}
	}
	cout << "相似字符串误判率:" << (double)n2 / (double)N << endl;

	//测试不相似字符串是否在位图中出现的概率
	std::vector<std::string> v3;
	for (size_t i = 0; i < N; ++i)
	{
		std::string url = "https://zhuanlan.zhihu.com/p/43263751";
		url += std::to_string(6789 + i);
		v3.push_back(url);
	}
	size_t n3 = 0;
	for (auto& str : v3)
	{
		if (bf.Test(str))
		{
			++n3;
		}
	}
	cout << "不相似字符串误判率:" << (double)n3 / (double)N << endl;
}

1.2.7 布隆过滤器优点

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

5. 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
6. 使用同一组散列函数的布隆过滤器可以进行交、并、差运算

1.2.8 布隆过滤器缺陷

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

1.2.9实际操作中针对布隆过滤器的改良:

实际中一般不会只用布隆过滤器就判断是否存在的问题,这样会非常影响客户的体验,布隆过滤器用来提高工作效率是个非常不错的选择

1.2.10 布隆过滤器的应用场景

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

2.海量数据处理

哈希切割:当一个文件非常大需要我们处理时,我们可以将一个大问题利用哈希算法进行切割,从而使庞大的问题变成了计算机容易处理的子问题。下面引出一个问题来看看哈希切割的细节

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

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

~|Bernard|

你的鼓励是我写下去最大的动力

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

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

打赏作者

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

抵扣说明:

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

余额充值