【C++历练之路】哈希思想的应用——位图、布隆过滤器

W...Y的主页 😊

代码仓库分享💕 


前言:我们使用hash思想学习了哈希表,进行了模拟实现unordered_set与unordered_map。这些都是用hash思想实现出来的数据结构,今天我们来学习一下hash的应用——位图、布隆过滤器。

目录

1. 哈希的应用

1.1 位图

1.1.1 位图概念

1.1.2 位图的实现

1.1.3 位图的应用

1.1.4变形应用

1.2 布隆过滤器

1.2.1 布隆过滤器提出

1.2.2布隆过滤器概念

1.2.3 布隆过滤器的插入

​编辑 

1.2.4 布隆过滤器的查找

 1.2.5 布隆过滤器删除

1.2.6 布隆过滤器优点

1.2.7 布隆过滤器缺陷


1. 哈希的应用

1.1 位图

1.1.1 位图概念

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

 我们从脑海中第一冒出来的想法是什么呢?

1. 遍历,时间复杂度O(N)
2. 排序(O(NlogN)),利用二分查找: logN

3.set+find函数
4. 位图解决

前三种方法的唯一缺陷是40亿个数据太大,无法进行存储。(1G大概有10亿字节,10亿大概可以存储2.5亿个无符号整数,40亿大概需要16G内存。)

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

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

1.1.2 位图的实现

根据上述问题,我们需要开一个类数组的东西,需要开多大呢?无符号整数的范围是0~2^32-1,所以我们需要开2^32次方的比特位。大概需要512M的内存即可表式无符号整数是否存在。C++中提供位图bitset,我们自己进行模拟实现一下。

首先我们使用非类型模板参数,进行模板化。位图是存在数组中的,但是我们不能以比特位去开数组,所以我们可以使用bool、char、int进行定义。(以下代码是以int为单位开的数组,所以我们得除以32,向上取整+1)。

template<size_t N>
class bitset
{
public:
	bitset()
	{
		_bits.resize(N / 32 + 1, 0);
		//cout << N << endl;
	}

	// 把x映射的位标记成1
	void set(size_t x)
	{
		assert(x <= N);

		size_t i = x / 32;
		size_t j = x % 32;

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

	// 把x映射的位标记成1
	void reset(size_t x)
	{
		assert(x <= N);

		size_t i = x / 32;
		size_t j = x % 32;

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

	bool test(size_t x)
	{
		assert(x <= N);

		size_t i = x / 32;
		size_t j = x % 32;

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

 当我们进行一一映射时,首先将数除以32来找到对应的哪个int中,在将此数模32算出在此int的具体位置,那个位置就是我们需要操作的地方。

set函数是将x数从0变1的函数,所以我们找到此位置后将1左移j位,然后进行或操作即可。

reset函数是将数从1变0的函数,所以我们先左移后再取反,最后进行与操作即可。

test函数是查找数是否出现过,我们直接返回左移j位再进行与操作的结果。

1.1.3 位图的应用

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

1.1.4变形应用

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

使用map肯定是存不下这么多的数的,但是位图的功能是判断一个数是否出现。我们应该可以自己手撕一个位图进行,用两个字节存储信息:00代表没有出现过,01代表出现过一次,10代表出现两次以上。

我们也可以使用两个位图进行操作,和上面的逻辑一样,但是我们不用手撕,两个位图进行复用配合即可。

template<size_t N>
class two_bit_set
{
public:
	void set(size_t x)
	{
		// 00 -> 01
		if (_bs1.test(x) == false
			&& _bs2.test(x) == false)
		{
			_bs2.set(x);
		}
		else if (_bs1.test(x) == false
			&& _bs2.test(x) == true)
		{
			// 01 -> 10
			_bs1.set(x);
			_bs2.reset(x);
		}
	}


	bool test(size_t x)
	{
		if (_bs1.test(x) == false
			&& _bs2.test(x) == true)
		{
			return true;
		}

		return false;
	}
private:
	bitset<N> _bs1;
	bitset<N> _bs2;
};

1.2 布隆过滤器

1.2.1 布隆过滤器提出

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

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

1.2.2布隆过滤器概念

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

如何选择哈希函数个数和布隆过滤器长度icon-default.png?t=N7T8https://zhuanlan.zhihu.com/p/43263751/ 

1.2.3 布隆过滤器的插入

 

向布隆过滤器中插入:"baidu"

 

作为字符串,我们必须将字符串先转换成整数再转换成映射位置进行存储,但是数组是无穷无尽的而整数是有限的,所以为了避免hash冲突,我们必须映射多组来减少哈希冲突。 

首先我们得需要三个hash函数来将整数再转换成三个不同映射位置。字符串hash算法icon-default.png?t=N7T8http://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html

struct HashFuncBKDR
{
	// BKDR
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (auto ch : s)
		{
			hash *= 131;
			hash += ch;
		}

		return hash;
	}
};

struct HashFuncAP
{
	// AP
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (size_t i = 0; i < s.size(); i++)
		{
			if ((i & 1) == 0) // 偶数位字符
			{
				hash ^= ((hash << 7) ^ (s[i]) ^ (hash >> 3));
			}
			else              // 奇数位字符
			{
				hash ^= (~((hash << 11) ^ (s[i]) ^ (hash >> 5)));
			}
		}

		return hash;
	}
};

struct HashFuncDJB
{
	// DJB
	size_t operator()(const string& s)
	{
		size_t hash = 5381;
		for (auto ch : s)
		{
			hash = hash * 33 ^ ch;
		}

		return hash;
	}
};

 布隆过滤器:

template<size_t N,
	class K = string,
	class Hash1 = HashFuncBKDR,
	class Hash2 = HashFuncAP,
	class Hash3 = HashFuncDJB>
class BloomFilter
{
public:
	void Set(const K& key)
	{
		size_t hash1 = Hash1()(key) % M;
		size_t hash2 = Hash2()(key) % M;
		size_t hash3 = Hash3()(key) % M;

		_bs->set(hash1);
		_bs->set(hash2);
		_bs->set(hash3);
	}

	bool Test(const K& key)
	{
		size_t hash1 = Hash1()(key) % M;
		if (_bs->test(hash1) == false)
			return false;

		size_t hash2 = Hash2()(key) % M;
		if (_bs->test(hash2) == false)
			return false;

		size_t hash3 = Hash3()(key) % M;
		if (_bs->test(hash3) == false)
			return false;

		return true; // 存在误判(有可能3个位都是跟别人冲突的,所以误判)
	}

private:
	static const size_t M = 10 * N;
	std::bitset<M>* _bs = new std::bitset<M>;
};

1.2.4 布隆过滤器的查找

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

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

 1.2.5 布隆过滤器删除

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

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

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

1.2.6 布隆过滤器优点

1. 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无

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

1.2.7 布隆过滤器缺陷

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

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

我们可以结合计数来进行,如果一个位置有两个标记则记为2,删除时让计数-1即可,等到减到0时删除即可。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

W…Y

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

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

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

打赏作者

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

抵扣说明:

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

余额充值