哈希表的应用

本文介绍了位图在海量数据中判断是否存在特定整数的应用,以及如何通过位图和布隆过滤器解决面试题中的问题,如快速查找40亿个整数中是否存在指定数、在内存限制下寻找交集和查找特定频率的IP地址。还探讨了布隆过滤器的概念和误判问题,以及如何扩展支持删除元素的操作。
摘要由CSDN通过智能技术生成

个人主页:Lei宝啊 

愿所有美好如期而遇


位图

概念

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

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

  1. 遍历,时间复杂度O(N)
  2. 排序(O(NlogN)),利用二分查找: logN
  3. 位图解决 数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一 个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0 代表不存在。 

实现

template<size_t N>
class bitset
{
public:

	bitset()
	{
		/*
			N是整数个数,我们要将他当做一个bit位,而vecotr元素都是int
			一个int32个比特位,如果我们有50个数,那么结果是1,却不够,
			所以我们采取进一的操作
		*/
		v.resize(N / 32 + 1, 0);
	}

	void set(size_t num)
	{
		assert(num <= N);

		size_t pos_vec = num / 32;
		size_t pos_bit = num % 32;

		v[pos_vec] |= 1 << pos_bit;
	}

	//num <= N
	void reset(size_t num)
	{
		assert(num <= N);

		size_t pos_vec = num / 32;
		size_t pos_bit = num % 32;

		v[pos_vec] &= ~(1 << pos_bit);
	}

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

		size_t pos_vec = num / 32;
		size_t pos_bit = num % 32;

		return v[pos_vec] & (1 << pos_bit);
	}

private:
	vector<int> v;

};

应用

  1. 给定100亿个整数,设计算法找到只出现一次的整数?
  2. 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
  3. 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
应用一解决方法:

100亿整数,其中可能会有多个重复的整数,而我们需要找到只出现一次的整数,一个位图是不够的,所以我们使用两个位图:

template<size_t N>
class algorithm_bitmap_to_TheOnlyNum
{
public:
	void set(size_t num)
	{
		//00->01
		if (bt1.test(num) == false && bt2.test(num) == false)
			bt2.set(num);
		//01->11
		else if (bt1.test(num) == false && bt2.test(num) == true)
			bt1.set(num);
		else
			return;
	}

	bool test(size_t num)
	{	
		if (bt1.test(num) == false && bt2.test(num) == true)
			return true;
		
		return false;	
	}

private:
	bitset<N> bt1;
	bitset<N> bt2;

};

当num在bt1中的比特位为0且在bt2中的比特位为0时表示该数字不存在,当在bt1中为0且在bt2中为1时表示出现1次,其余情况为2次及以上。

应用二解决方法:

首先100亿个整数,也就是说如果正常存储就是400亿字节,也就是需要大约40GB内存,两个文件就是80GB,即使我们分别将他们存储在位图中,一个位图也需要大约1GB内存,两个位图就是2GB,而我们只有1GB的内存,所以我们这样做:

我们的模板参数是常量N,这个N决定着有多少个bit位,整型值的范围在-2^31 ~ 2^31-1,但是比特位没有负数位,所以我们使用无符号整数来映射负数,所以比特位的范围就是0 ~ 2^32-1,我们可以先开一半的空间,这样两个位图加起来就只占一半内存,我们先映射0 ~ 2^31的值,找出交集后,再去映射另一半的值,映射另一半值的时候,值先减去2^31。

这样我们就实现了1GB内存,找到文件的两个交集。

template<size_t N>
class bitset
{
public:

	bitset()
	{
		/*
			N是整数个数,我们要将他当做一个bit位,而vecotr元素都是int
			一个int32个比特位,如果我们有50个数,那么结果是1,却不够,
			所以我们采取进一的操作
		*/
		v.resize(N / 32 + 10, 0);
	}

	//void set(size_t num)
	//{
	//	if (num > N)
	//		return;

	//	size_t pos_vec = num / 32;
	//	size_t pos_bit = num % 32;

	//	v[pos_vec] |= 1 << pos_bit;
	//}

	num <= N
	//void reset(size_t num)
	//{
	//	if (num > N)
	//		return;

	//	size_t pos_vec = num / 32;
	//	size_t pos_bit = num % 32;

	//	v[pos_vec] &= ~(1 << pos_bit);
	//}

	//bool test(size_t num)
	//{
	//	if (num > N)
	//		return;

	//	size_t pos_vec = num / 32;
	//	size_t pos_bit = num % 32;

	//	return v[pos_vec] & (1 << pos_bit);
	//}

	void set(size_t num)
	{
		if (num >= N)
			num -= N;

		size_t pos_vec = num / 32;
		size_t pos_bit = num % 32;

		v[pos_vec] |= 1 << pos_bit;
	}

	//num <= N
	void reset(size_t num)
	{
		if (num >= N)
			num -= N;

		size_t pos_vec = num / 32;
		size_t pos_bit = num % 32;

		v[pos_vec] &= ~(1 << pos_bit);
	}

	bool test(size_t num)
	{
		if (num >= N)
			num -= N;

		size_t pos_vec = num / 32;
		size_t pos_bit = num % 32;

		return v[pos_vec] & (1 << pos_bit);
	}

private:
	vector<int> v;

};
int main()
{

	FILE* fp1 = fopen("data1.txt", "r");
	FILE* fp2 = fopen("data2.txt", "r");

	//2,147,483,648
	bitset<INT_MAX> bt1;
	bitset<INT_MAX> bt2;

	int num;
	while (fscanf(fp1, "%d", &num) != EOF)
	{
		bt1.set(num);
	}

	while (fscanf(fp2, "%d", &num) != EOF)
	{
		bt2.set(num);
	}

	// i < INT_MAX
	for (int i = -INT_MAX; i < 0; i++)
	{
		if (bt1.test(i) == bt2.test(i) && bt1.test(i) == true)
			cout << i << endl;
	}

	fclose(fp1);
	fclose(fp2);


	return 0;
}

应用三解决方法: 

就是应用一的一个变形,还是两个位图,只不过test方法返回条件可以变一变,这里不多做解释。

布隆过滤器

概念

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

其实和位图差不多,只是多了几个映射位置,并且使用不同哈希函数进行映射,由他们共同判断是否存在。

布隆过滤器在实际应用中主要是用于字符串,IP地址等的过滤。

布隆过滤器存在误判,即使我们使用多个hash函数映射不同位置,仍然可能存在这样一种情况,即某几个位置的bit位为1,我们去查询一个字符串是否存在,正好查到了这几个位置,然而这几个位置实际上映射的是其他字符串,由此也就产生了误判。

这种误判在布隆过滤器中是不可避免的,但是可以降低误判率,通过开辟更多的位图空间。 

实际应用中,比如游戏昵称,通过布隆过滤器先过滤一次我们输入的昵称,如果没找到,那么一定就是未被使用,但是找到了,不一定是被使用了,所以还需要在数据库中进行一次查询。

实现

struct BKDRHash
{
	size_t operator()(const string& s)
	{
		// BKDR
		size_t value = 0;
		for (auto ch : s)
		{
			value *= 31;
			value += ch;
		}
		return value;
	}
};

struct APHash
{
	size_t operator()(const string & s)
	{
		size_t hash = 0;
		for (long 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 DJBHash
{
	size_t operator()(const string & s)
	{
		size_t hash = 5381;
		for (auto ch : s)
		{
			hash += (hash << 5) + ch;
		}

		return hash;
	}
};

template<size_t N,
		 class BKDRHash = BKDRHash,
		 class APHash = APHash,
		 class DJBHash = DJBHash>
class Bloom
{

public:
	void set(const string& value)
	{
		size_t hash1 = BKDRHash()(value) % M;
		size_t hash2 = APHash()(value) % M;
		size_t hash3 = DJBHash()(value) % M;

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

	bool test(const string& value)
	{
		size_t hash1 = BKDRHash()(value) % M;
		if (!bt.test(hash1))
			return false;

		size_t hash2 = APHash()(value) % M;
		if (!bt.test(hash2))
			return false;

		size_t hash3 = DJBHash()(value) % M;
		if (!bt.test(hash2))
			return false;

		return true;
	}

private:
	static const int M = 5 * N;
	bitset<M> bt;

};

应用 

1.应用一

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

近似算法自然就是布隆过滤器

//假设文件1和文件2就是根据哈希切割后的相同编号的文件
//近似算法---布隆过滤器

int main()
{

	FILE* fp1 = fopen("IP_one.txt", "r");
	FILE* fp2 = fopen("IP_two.txt", "r");

	Bloom<10000> bloom;

	char ch[128];
	while (fscanf(fp1, "%s\n", ch) != EOF)
	{
		string s(ch);
		bloom.set(s);

		memset(ch, 0, sizeof(ch));
	}

	while (fscanf(fp2, "%s\n", ch) != EOF)
	{
		string s(ch);
		if (bloom.test(s))
			cout << s << endl;

		memset(ch, 0, sizeof(ch));
	}

	fclose(fp1);
	fclose(fp2);

	return 0;
}

精确算法就需要我们进行哈希切割,使用一个hash函数将query求hash值进入不同分割的文件,也就是说,进入同一个文件的query的hash值相同,要么就是相同的query,要么就是发生了冲突而进入。

两个文件都进行hash分割,并且使用同一个hash函数,那么;两个文件分割后形成的相同编号的文件,就是相同的query或冲突的,我们将他们加载进内存,保存在set中,然后对两个set取交集。

如果说有一个文件即使我们分割后仍然比1GB大,那么我们加载进set,如果没有异常,就说明重复的query很多,冲突的很少,如果出现异常,那么我们就换个hash函数再次对这个文件进行hash切割。

2.应用二

给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址? 与上题条件相同,如何找到top K的IP?如何直接用Linux系统命令实现?

这里我们仍然使用hash切割,这个应用我们简单给出举例使用

//创建随机IP地址文件

int main()
{
	srand((unsigned int)time(0));

	FILE* fp = fopen("IP_two.txt", "w");

	for (int i = 0; i < 10000; i++)
	{
		string s = "2001:0db8:85a3:0000:0000:8a2e:0370:";
		int num = rand() % 10000;
		
		s += to_string(num);
		char ch[128];
		strcpy(ch, s.c_str());

		fprintf(fp, "%s\n", ch);
	}

	fclose(fp);

	return 0;
}

创建文件进行hash分割 

int main()
{

	FILE* fp = fopen("IP.txt", "r");
	
	for (int i = 1; i <= 10; i++)
	{
		string filename("IP");

		filename += to_string(i);
		filename += ".txt";

		FILE* fp = fopen(filename.c_str(), "w");
		fclose(fp);
	}

	int i = 0;
	do{

		char ch[128];
		fscanf(fp, "%s\n", ch);

		string s(ch);
		int hash = BKDRHash()(s) % 10;

		string filename("IP");
		filename += to_string(hash);
		filename += ".txt";

		FILE* fp = fopen(filename.c_str(), "a");
		fprintf(fp, "%s\n", ch);
		fclose(fp);

		i++;
	} while (i < 10000);

	fclose(fp);
}

 找到出现次数最多的ip地址。

#include <map>
using namespace std;

int main()
{

	pair<string, int> Max_Count_Ip(make_pair("", 0));


	for (int i = 0; i <= 10; i++)
	{
		string filename("IP");
		filename += to_string(i);
		filename += ".txt";

		FILE* fp = fopen(filename.c_str(), "r");
		map<string, int> m;

		char ch[128];
		while (fscanf(fp, "%s\n", ch) != EOF)
		{
			string s(ch);
			m[s]++;

			memset(ch, 0, sizeof(ch));
		}

		for (auto& e : m)
		{
			if (e.second > Max_Count_Ip.second)
				Max_Count_Ip = e;
		}
	}

	cout << Max_Count_Ip.first << "------count: " << Max_Count_Ip.second << endl;

	return 0;
}
3.应用三

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

在每个位上加一个计数引用,删除就--,直到为0时reset成0。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Lei宝啊

觉得博主写的有用就鼓励一下吧

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

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

打赏作者

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

抵扣说明:

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

余额充值