哈希——位图与布隆过滤器

C++入门17——哈希之哈希表详解中,我们了解了哈希算法,那么哈希算法该怎么运用到实际呢?接下来就介绍哈希算法常见的两种应用:位图与布隆过滤器。

1. 位图

1.1 位图的概念

了解位图的概念之前,先来看一道面试题:

①有40亿个未排序的无符号整数,给你一个无符号整数,如何快速判断该数是否存在这40亿个数中?

对于这道题,我们常规的解法通常为:

①直接遍历这40亿个数,判断该数是否存在;该方法可能不够快,那么又有方法②:先将40亿个数排序,再进行二分查找。

似乎方法②更好一些,可是我们还应该注意到:40亿个整数就是160亿字节≈15G,用以上两种方法需要15G的内存空间,先不说有没有造成内存空间浪费,15G的内存对我们的普通电脑来说能不能运行起来都是问题呀!

那么解决上述问题的最佳算法——位图就应运而生了:

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

1.2 具体思路

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

按照这种思路,40亿个整型数据就需要40亿比特位≈477MB的内存即可完成,大大降低了空间使用率。

如图:

1.3 位图的实现

具体实现如下: 

#include <iostream>
#include <vector>
#include <assert.h>

using namespace std;

namespace xxk
{
	template <size_t N>
	class bitset
	{
	public:
		bitset()
		{
			//先开辟空间(N个比特位对应N/8/4个整型)
			_bit.resize(N / 32 + 1, 0);//+1避免除不尽
		}

		//将数据映射到开辟好的空间中,映射到的位标记成1(可以理解为插入x)
		void set(size_t x)
		{
			size_t i = x / 32;//计算x映射在第几个整型里
			size_t j = x % 32;//计算x映射在第i个整型的第几个比特位里

			_bit[i] |= (1 << j);//第i个整型的第j个比特位变为1(先左移再按位或)
		}
		//将数据映射到开辟好的空间中,映射到的位标记成0(可以理解为删除x)
		void reset(size_t x)
		{
			size_t i = x / 32;//计算x映射在第几个整型里
			size_t j = x % 32;//计算x映射在第i个整型的第几个比特位里

			_bit[i] &= ~(1 << j);//第i个整型的第j个比特位变为0(先左移取反再按位与)
		}
		//
		bool test(size_t x)
		{
			assert(x <= N);

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

			return _bit[i] & (1 << j);//整型转bool值,0为false,非0为true
		}

	private:
		vector<int> _bit;
	};
}

测试:

验证6是否在arr中:

void func()
	{
		int arr[] = { 3,21,22,17,5,18,5,2,12,12,22,5,3,2,5 };
		bitset<22> bs;
		for (auto e : arr)
		{
			bs.set(e);
		}
		if (bs.test(6))
		{
			cout << "在" << endl;
		}
		else
		{
			cout << "不在" << endl;
		}
	}


 再看一道题:

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

在第①题中,我们只需判断一个数是否存在即可,所以存在就是1,不存在就是0,一个比特位就可以表示这两种状态;

可这道题显然不止存在两种状态,它存在三种状态:0次,1次,2次及以上,所以3种状态只能用两个比特位来表示了。我们可以在第①题中修改代码,开辟空间时就需要开辟2N个空间了;

不修改以上代码,也可以用以下方法解决:

可以复用以上代码,开辟两个位图,如果x只在其中之一个位图中存在,则符合条件。

具体实现如下:

template <size_t N>
	class two_bitset
	{
	public:
		void set()
		{
			//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);
			}
			//再有就是2次及以上的情况了
		}

		bool test(size_t x)
		{
			if (_bs1.test(x) == false && _bs2.test(x) == true)
			{
				return true;//只有当_bs2中存1时才表明x只出现一次
			}
			return false;
		}
	private:
		bitset<N> _bs1;
		bitset<N> _bs2;
	};

测试:

void func2()
	{
		int arr[] = { 3,21,22,17,5,18,5,2,12,12,22,5,3,2,5 };
		two_bitset<22> bs;
		for (auto e : arr)
		{
			bs.set(e);
		}
		cout << "只出现一次的数据有: ";
		for (auto e : arr)
		{
			if (bs.test(e))
			{
				cout << e << " ";
			}
		}
		cout << endl;
	}


再看一道题:

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

 复用以上代码,很容易解决这个问题:

void func3()
	{
		int arr1[] = { 3,21,22,17,5,18,5,2,12,12,22,5,3,2,5 };
		int arr2[] = { 3,21,22,17,6,1,11 };
		bitset<22> bs1;
		bitset<22> bs2;
		for (auto e : arr1)
		{
			bs1.set(e);
		}
		for (auto e : arr2)
		{
			bs2.set(e);
		}
		cout << "两个数组的交集为:";
		for (size_t i = 0; i < 23; i++)
		{
			if (bs1.test(i) && bs2.test(i))
			{
				cout << i << " ";
			}
		}
		cout << endl;
	}

小结:

位图的优点:快 + 节省空间;

位图的缺点:位图只能解决整型数据。

2.布隆过滤器

刚刚提到了位图的缺点,位图只能解决整型数据,可现在有这样一个问题:有一堆字符串string[] A,给你一个字符串a,设计算法求a是否在A里?

例如:string A[]={"苹果","梨","香蕉","橘子"};,判断"梨"是否在A里?

对于这个问题,其实很简单呀,我们已经学过哈希表了,在C++入门17——哈希之哈希表详解中,对于字符串类型的数据,我们特地封装了一个仿函数来专门处理字符串类型的数据,在这里也是同样的道理呀!先将字符串类型转为整型,再将整型映射到位图结构中,从而实现了查找a是否在A中。

没错,这样的思路是对的,采用哈希+位图的方法正是我们要讲的布隆过滤器。

可问题来了:承接以上的A,我问你"离"是否在A里?"梨""离"由字符串类型转为整型似乎并没有太大区别呀!这必然会导致一个问题:不同的字符串有可能会被映射到同一个位置上,那么此时采用上述算法有可能会返回"离"在A中的的结果!

也就是说:采用上述算法得出“不在”的结果一定为真,得出“在”的结果可能为真!

既然得出“不在”的结果一定为真,那我可以采用多个不同的哈希函数将A中的字符串映射到位图的多个不同位置,然后将a也经过这多个哈希函数映射到位图的多个不同位置,只要有1个位置匹配不上,那么a就一定不在A中。(也可以理解为,用一个哈希函数得到的布隆过滤器漏网大,用多个哈希函数得到的布隆过滤器漏网小,并且哈希函数越多漏网就越小)

如图:

经过此番了解,终于要介绍布隆过滤的概念了。 

 2.1布隆过滤器的概念

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

也就是说,得出“不存在”的结果不会误判,得出“存在”的结果会有误判,如何降低误判率也是我们需要了解的内容,具体请参考大佬的文章详解布隆过滤器的原理,使用场景和注意事项 - 知乎

据这篇文章的公式:

如果我们的k=3,可以大致算出m和n满足这样的关系:m≈4.3n,为了方便,我们不妨直接令m=5n。

这里我们就暂且设置3个哈希函数,关于字符串的哈希算法,可以参考大佬的文章字符串哈希函数,我们从中CV三个:

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

2.2 布隆过滤器的实现 

那么布隆过滤器的实现为:

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)
	{
		//key经过3个函数,只要有一个不在就返回false
		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;//即使返回true也有可能误判
	}

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

测试:

void func4()
{
	string A[] = { "苹果","梨","香蕉","橘子" };
	BloomFilter<20> bf;
	for (auto& s : A)
	{
		bf.Set(s);
	}
	for (auto& s : A)
	{
		if (bf.Test(s))
		{
			cout << "在" << " ";
		}
	}
	cout << endl;
	cout << bf.Test("离") << endl;
}

2.3 验证误判率

上一段代码验证m=5nm=10n时和相似字符串不相似字符串的误判率:

void func5()
{
	srand(time(0));
	const size_t N = 100000;
	BloomFilter<N> bf;

	std::vector<std::string> v1;
	std::string url = "https://blog.youkuaiyun.com/weixin_73629886?spm=1000.2115.3001.5343";
	//std::string url = "徐霞客";

	for (size_t i = 0; i < N; ++i)
	{
		v1.push_back(url + std::to_string(i));
	}

	for (auto& str : v1)
	{
		bf.Set(str);
	}

	// v2跟v1是相似字符串集(前缀一样),但是后缀不一样
	std::vector<std::string> v2;
	for (size_t i = 0; i < N; ++i)
	{
		std::string urlstr = url;
		urlstr += std::to_string(9999999 + i);
		v2.push_back(urlstr);
	}

	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)
	{
		//string url = "zhihu.com";
		string url = "孙悟空";
		url += std::to_string(i + rand());
		v3.push_back(url);
	}

	size_t n3 = 0;
	for (auto& str : v3)
	{
		if (bf.Test(str))
		{
			++n3;
		}
	}
	cout << "不相似字符串误判率:" << (double)n3 / (double)N << endl;
}

结果如图:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值