[C++数据结构](23)哈希:位图,布隆过滤器,哈希切割

哈希特殊应用

位图(BitMap)

题目

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

思路

把这40亿个数放进 unordered_set 里?显然不现实,因为光是40亿个整型就需要约15GB空间,更别提哈希表的其他资源消耗,内存空间不够。

注意到我们需要找的是一个数存不存在,记录一个数是否存在只需要一个bit位,这样的话,一个bit位一个地址,采用直接定址法只需要开 2 32 2^{32} 232 (整型能表示的数据个数) 个bit,也就是 512MB 空间

代码实现

注意一些位运算的技巧

template<size_t N> // 非类型模板参数,表示要开的位数
class bit_set
{
public:
	bit_set()
	{
		_bits.resize(N / 8 + 1, 0); // N/8会省略小数,导致开的空间不够,所以+1多开一个字节
	}

	// x位置设为1
	void set(size_t x)
	{
		// 计算x位置在第几个字节
		size_t i = x / 8;
		// 计算在这一字节的第几个bit位
		size_t j = x % 8;

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

	// x位置设为0
	void reset(size_t x)
	{
		size_t i = x / 8;
		size_t j = x % 8;
		_bits[i] &= ~(1 << j);
	}

	// 判断x在不在
	bool test(size_t x)
	{
		size_t i = x / 8;
		size_t j = x % 8;
		return _bits[i] & (1 << j);
	}

private:
	vector<char> _bits;
};

其实库里面提供了位图容器

详情参考文档:bitset - C++ Reference (cplusplus.com)


位图应用

  1. 从100亿个整数中,找到只出现一次的整数

重写一个位图,让两个bit表示一个状态,一共三种状态:出现0次、出现1次、出现多次。

这样这个位图需要开的空间是原来位图的2倍,也就是1GB

代码实现:

使用两个bitset实现,就不需要重写映射关系了

template<size_t N>
class two_bitset
{
public:
	//出现0次、1次、多次分别表示为:00、01、10
	void set(size_t x)
	{
		bool in1 = _bs1.test(x);
		bool in2 = _bs2.test(x);
		if (!in1 && !in2) // 如果还未出现
		{
			// 设为01
			_bs2.set(x);
		}
		else if (!in1 && in2) // 如果已经出现一次
		{
			// 设为10
			_bs1.set(x);
			_bs2.reset(x);
		}
		// 如果已经出现2次及以上,不动
	}

	bool isOnce(size_t x)
	{
		return !_bs1.test(x) && _bs2.test(x);
	}

private:
	bitset<N> _bs1;
	bitset<N> _bs2;
};
  1. 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件的交集?

    分别建立两个位图,两个位图中都记为1的值就是交集

  2. 1个文件有100亿个整数,我们有1G内存,从中找出出现次数不超过2次的所有整数。

    在第一题的基础上加一个状态,让 10 表示出现2次,11 表示出现3次即以上

位图优点

节省空间,快

位图缺点

只能针对整型。

布隆过滤器

位图只能针对整型,因为整型可以使用直接定址法,且不存在哈希冲突。如果将字符串转换成整型则可能出现哈希冲突,而位图中没有真正存入这个字符串,无法进行比对,所以不能解决哈希冲突的问题。

布隆过滤器则是对位图的一种改造,可以让一个 key 映射多个地址(使用多个哈希函数),降低哈希冲突的概率。不过它依然有一定的误判概率。在查找的时候,它只能告诉你这个值一定不在可能在

具体映射多少个地址,要看具体情况,映射的越多,哈希冲突的概率越小,但是占用的空间就越大。

如何选择合适的哈希函数个数呢,这里直接给出公式:
m = − n ln ⁡ p ln ⁡ 2 2 k = m n ln ⁡ 2 m=-\frac{n\ln p}{\ln^22}\\ k=\frac mn\ln2 m=ln22nlnpk=nmln2
k k k 为哈希函数个数, m m m 为布隆过滤器长度, n n n 为插入元素的个数, p p p 为误报率。

实现

三个哈希函数的版本:

template<class K, size_t M,
	class HashFunc1,
	class HashFunc2,
	class HashFunc3>
class BloomFilter
{
public:
	void Set(const K& key)
	{
		size_t hash1 = HashFunc1()(key) % M;
		size_t hash2 = HashFunc2()(key) % M;
		size_t hash3 = HashFunc3()(key) % M;

		_bs.set(hash1);
		_bs.set(hash2);
		_bs.set(hash3);
	}

	bool Test(const K& key)
	{
		// 三个位只要有一个为0就是不存在
		size_t hash1 = HashFunc1()(key) % M;
		if (!_bs.test(hash1)) return false;
		size_t hash2 = HashFunc2()(key) % M;
		if (!_bs.test(hash2)) return false;
		size_t hash3 = HashFunc3()(key) % M;
		if (!_bs.test(hash3)) return false;
		// 都为1则表示可能存在
		return true;
	}

private:
	bitset<M> _bs;
};

布隆过滤器支持删除吗,如何删除?

  • 可以支持,但是不能直接删,因为删除一个值可能影响其他值,如果两个值的三个映射有交集,那么删除一个另一个就也找不到了。
    • 解决方案:可以使用多个位图来表示一个地址被映射的次数,删除一个值的时候只要在相应的映射位置减1即可,不过这样还存在一个误判,如果删除操作之后这个值的三个映射位置的计数都没有减为0,那么这个值会被误判为存在。
  • 布隆过滤器的一大优点就是节省空间,支持删除需要多开额外的空间,所以它一般不去实现删除操作。

布隆过滤器应用场景

  1. 注册的时候,快速判断一个昵称是否使用过。
    • 不在,是确定的,可以使用
    • 在,则再去数据库确认一遍
  2. 黑名单
    • 不在,同行
    • 在,再去系统确认

哈希切割

问题:给一个超过100G的log文件,log中存着IP地址,如何找出出现次数最多的IP地址。

这是个 kv 模型,不可使用位图和布隆过滤器。

解决方案:创建100个文件,然后依次读取ip,算出 i = HashFunc(ip) % 100,把这个 ip 放到第 i 个文件里面。这样可以保证同样的 ip 进入同一个的文件,最后使用 unordered_map 对每个文件进行统计(统计完一个文件,clear,再统计下一个文件)。

缺陷:

  • 某个小文件太大

    原因:

    • 某个ip相同的太多
    • 映射冲突到这个文件的ip太多

解决方案:

  • 捕获内存不足的异常,说明内存不够
    • 针对这个小文件,换个哈希函数再次进行哈希切割,分成更细小的文件。

应用:给两个文件,分别有100亿个ip,我们只有1G内存,如何找两个文件的交集

解决:创建A0 A1 ··· A999,B0 B1 ··· B999,共2000个小文件,分别计算两个文件中的 i = HashFunc(ip) % 1000,分别放到Ai,Bi中,最后对每一对小文件求交集。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

世真

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

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

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

打赏作者

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

抵扣说明:

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

余额充值