海量数据哈希表查重

40 篇文章 7 订阅

  • 查重:数据是否有重复,以及数据重复的次数
  • topK:有几亿个数字。求元素的值,前K大/小,第K大/小
  • 去重:去掉重复多次的数字,数字只保留一份

一、海量数据的查重问题

  1. 哈希表:得看有没有对内存的限制,如果没有限制,就是直接用哈希表解决。比如说 50亿(5G)个整数的查重问题, 10亿个整数内存大约是1G,50亿个整数相当于内存是5G,一个整数4个字节,如果要用一个哈希表把这50亿个数据全部存储下来,就得花20G的内存,链式哈希表每个节点还得有一个地址域,又占4字节,所以总共需要(20G+20G=40G) 的内存空间。哈希表就是空间换时间的这么一个结构

C++STL中的无序容器底层就是通过哈希表实现的,其中主要涉及四个容器:
在这里插入图片描述
在实际解决问题的过程中,如果需要使用哈希表,可以直接使用上面的无序容器,哈希表的增删查的时间复杂度趋近于O(1),效率非常高

  1. 分治思想:如果对内存有要求,就要使用分治思想,对数据的大小进行划分。第1和第2个方法思想是解决查重问题的根本出发点,就是用哈希表。

  2. Bloom Filter:布隆过滤器,节省内存,但是有点误差

  3. 如果是字符串类型的查重,除了哈希表,布隆过滤器,还可以使用TrieTree字典树(前缀树)

https://blog.csdn.net/QIANGWEIYUAN/article/details/88815772

二、无限制哈希表查重(重复出现的数字、重复出现的次数、第几个重复)

int main() {
	const int SIZE = 10000;
	int ar[SIZE] = { 0 };
	for (int i = 0; i < SIZE; ++i) {
		ar[i] = rand();
	}
	unordered_map<int, int> map;
	int n = 1;

	for (int val : ar) {
		if (map[val] > 0) {
			cout << val << "是第" << n <<"个重复的" << endl;
			++n;
		}
		map[val]++;
	}
	for (auto pair : map) {
		if (pair.second > 1) {
			cout << pair.first << "重复 " << pair.second << " 次" << endl;
		}
	}
	return 0;
}

三、内存限制哈希表查重(重复出现的数字以及出现的次数)

场景一:有一个文件,有50亿个整数,内存限制400M,找出文件中重复的数字和重复的次数

我们知道50亿大概是5G,每个整数4字节,就需要20G内存存储,如果使用链式哈希表每个节点还需要4字节存储地址域,总共需要40G内存,这么大的内存是我们不能接受的

我们可以使用分治法,大文件划分成小文件,使得每一个小文件能够加载到内存当中,求出对应的重复的元素,把结果写入到一个存储重复元素的文件当中

40G的大文件可以划分成大约120个(文件的数量最好是素数)400M的小文件(系统默认一个进程使用的文件数不超过1024)

data0.txt
data1.txt
...
data126.txt

当然哈希算法可能是不均匀分配的,导致有的文件存放的整数很多,甚至超过了400M,这时就需要增大小文件的数量,确保使用哈希函数后,每个小文件存储的整数不超过400M

然后遍历大文件的整数,把每一个整数根据哈希映射函数val % 127 = file_index,放到对应序号的小文件当中。整数值相同,通过一样的哈希映射函数,肯定是放在同一个小文件中

这样就从小文件里把数据全部读出来放在内存中,使用哈希表进行查重,可以求出重复出现的数字以及出现的次数,但是不能知道是第几个重复的数字

场景二:有a、b两个大文件,里面都有10亿个整数,内存限制400M,求出在a,b两个文件中都出现的数字

10亿大概是1G,10亿个int整数需要4G,使用哈希表还需要存储地址域,则需要8G内存,显然我们需要使用分治策略

8G / 400M > 20,我们取素数23,将每个大文件中10亿个数据分别用哈希算法val % 23 = file_index存储至23个小文件中

a0.txt       b0.txt
a1.txt       b1.txt
...
a22.txt      b22.txt

a和b两个文件中,值相同的元素,进行哈希映射以后,肯定在相同序号的小文件当中

接下来一组一组处理,读取a0.txt中所有的整数到哈希表,然后从b0.txt中挨个读取,在哈希表中查找,这样就能找到在a0.txt和b0.txt中都出现的数字了。然后处理a1和b1,然后处理a2和b2…直到处理a22和b22,可以求出重复出现的数字以及出现的次数,但是不能知道是第几个重复的数字

此外,我们还可以用a文件的数据构建Bloom Filter的位数组中的状态值,然后再读取b文件的数据进行布隆过滤的查找操作就可以了

四、哈希表unordered_set查重(重复的数字、第几个重复)

问题:有10亿个整数,整数取值范围也是0到10亿,找出第一个重复的数字?

如果只是使用unordered_set只能知道哪个数字重复以及第几个重复,不能确定重复的次数。可以借助unordered_map记录重复的数字以及重复次数

int main()
{
	vector<int> vec;
	for (int i = 0; i < 1000; ++i)
	{
		vec.push_back(rand());
	}

	// 用哈希表解决查重,因为只查重,所以用无序集合解决该问题
	unordered_set<int> hashSet;
	int n = 1;
	for (int val : vec)
	{
		// 在哈希表中查找val
		auto it = hashSet.find(val);
		if (it != hashSet.end())
		{
			cout << *it << "是第"<<n<<"个重复的数据" << endl;
			++n;
		}
		else
		{
			// 没找到
			hashSet.insert(val);
		}
	}

	return 0;
}

五、位图查重(重复的数字、第几个重复)

题目已经告诉了数据的取值范围,最大值是10亿,如果问题没有告知数据最大值,用位图法处理问题,需要先遍历一遍数组找出最大值,根据最大值开辟位图

最大值是10亿,我们得开辟10亿个bit,即125000000字节,大约是120MB,这就很节省空间了

位图法有一个很大的缺点:就是数据没有多少,但是最大值却很大,比如有10个整数,最大值是10亿,那么就得按10亿这个数字计算开辟位图数组的大小,也就是在10个数字中找重复的,就需要120MB的空间,这种极端情况很浪费空间

#include <iostream>
#include <vector>
#include <unordered_set>

using namespace std;

int main()
{
	vector<int> vec;
	for (int i = 0; i < 100000; ++i)
	{
		vec.push_back(rand());
	}

	// 用位图法解决问题
	typedef unsigned int uint;
	uint maxNumber = 1000000000;
	int size = maxNumber / 8 + 1;
	char *p = new char[size]();
	int n = 1;
	
	for (uint i = 0; i < vec.size(); ++i)
	{
		// 计算整数应该放置的数组下标
		int index = vec[i] / 8; 
		// 计算对应字节的比特位
		int offset = vec[i] % 8;
		// 获取相应比特位的数值
		int v = p[index] & (1 << offset);
		if (0 != v)
		{
			cout << vec[i] << "是第"<< n <<"个重复的数据" << endl;
			++n;
		}
		else
		{
			// 表示该数据不存在,把相应位置置1,表示记录该数据
			p[index] = p[index] | (1 << offset);
		}
	}
	delete[]p;
	return 0;
}

六、多个文件外排序,最后合并到一个文件中(不去重)

#include <iostream>
#include <vector>
#include <unordered_map>
#include <stack>
#include <algorithm>
#include <functional>
#include <queue>

using namespace std;

void create_file() {
	srand(time(nullptr));
	FILE* fp_w = fopen("data.dat", "wb");
	for (int i = 0; i < 1000; i++) {
		int data = rand();
		fwrite(&data, sizeof(int), 1, fp_w);
	}
	fclose(fp_w);

	// 打开存储数据的原始文件
	FILE* fp_r = fopen("data.dat", "rb");
	if (fp_r == nullptr)
		return ;

	// 这里由于原始数据量缩小,所以这里文件划分的个数也变小了,11个小文件
	const int FILE_NO = 11;
	FILE* pfile[FILE_NO] = { nullptr };
	for (int i = 0; i < FILE_NO; ++i)
	{
		char filename[20];
		sprintf(filename, "data%d.dat", i + 1);
		pfile[i] = fopen(filename, "wb+");
	}

	// 哈希映射,把大文件中的数据,映射到各个小文件当中
	int data;
	while (fread(&data, sizeof(int), 1, fp_r) > 0)
	{
		int findex = data % FILE_NO;
		fwrite(&data, sizeof(int), 1, pfile[findex]);
	}
	fclose(fp_r);

	// 对每个文件排序并重新写入
	for (FILE* fp : pfile) {
		fseek(fp, 0, SEEK_SET);
		vector<int> nums;
		while (fread(&data, sizeof(int), 1, fp) > 0)
		{
			nums.push_back(data);
		}
		sort(nums.begin(), nums.end());
		fseek(fp, 0, SEEK_SET);
		for (int v : nums) {
			fwrite(&v, sizeof(int), 1, fp);
		}
	}
	for (FILE* fp : pfile) {
		fclose(fp);
	}
}

// 大文件划分小文件(哈希映射)+ 哈希统计 + 小根堆(快排也可以达到同样的时间复杂度)
int main()
{
	 // create_file();
	const int FILE_NO = 11;
	FILE* pfile[FILE_NO] = { nullptr };
	for (int i = 0; i < FILE_NO; ++i)
	{
		char filename[20];
		sprintf(filename, "data%d.dat", i + 1);
		pfile[i] = fopen(filename, "rb");
	}
	
	FILE* fp_dst = fopen("sorted.dat", "wb+");
	priority_queue<int, vector<int>, greater<int>> minHeap;
	int data;
	
	// 在FILE_NO个文件都读取一个数字到最小堆,并把文件指针偏移回原位,保持最小堆中放的都是当前文件指针所指元素
	for (int i = 0; i < FILE_NO; i++) {
		fseek(pfile[i], 0, SEEK_SET);
		fread(&data, sizeof(int), 1, pfile[i]);
		fseek(pfile[i], 0, SEEK_SET);
		minHeap.push(data);
	}

	while (!minHeap.empty()) {
		// 取出堆顶元素,写入最终文件
		int top = minHeap.top();
		fwrite(&top, sizeof(int), 1, fp_dst);
		minHeap.pop();
		// 遍历所有的文件指针,读取文件指针指向的元素
		// 若和刚刚的堆顶元素不同,则重新偏移回来
		// 若相等,则把读取新的元素放入最小堆,把文件指针再次偏移回来
		for (int i = 0; i < FILE_NO; i++) {
			if (fread(&data, sizeof(int), 1, pfile[i]) > 0) {
				if (top != data) {
					fseek(pfile[i], 0 - sizeof(int), SEEK_CUR);
				}
				else {
					if (fread(&data, sizeof(int), 1, pfile[i]) > 0) {
						minHeap.push(data);
						fseek(pfile[i], 0 - sizeof(int), SEEK_CUR);
					}
				}
			}
		}
	}

	

	fclose(fp_dst);
	for (FILE* fp : pfile) {
		fclose(fp);
	}

	//fp_dst = fopen("sorted.dat", "rb");
	//int n = 0;
	//while (fread(&data, sizeof(int), 1, fp_dst) > 0)
	//{
	//	n++;
	//	cout << data << endl;
	//}
	//cout << "n = " << n << endl;
	return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

bugcoder-9905

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

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

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

打赏作者

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

抵扣说明:

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

余额充值