文章目录
- 查重:数据是否有重复,以及数据重复的次数
- topK:有几亿个数字。求元素的值,前K大/小,第K大/小
- 去重:去掉重复多次的数字,数字只保留一份
一、海量数据的查重问题
- 哈希表:得看有没有对内存的限制,如果没有限制,就是直接用哈希表解决。比如说 50亿(5G)个整数的查重问题, 10亿个整数内存大约是1G,50亿个整数相当于内存是5G,一个整数4个字节,如果要用一个哈希表把这50亿个数据全部存储下来,就得花20G的内存,链式哈希表每个节点还得有一个地址域,又占4字节,所以总共需要(20G+20G=40G) 的内存空间。哈希表就是空间换时间的这么一个结构
C++STL中的无序容器底层就是通过哈希表实现的,其中主要涉及四个容器:
在实际解决问题的过程中,如果需要使用哈希表,可以直接使用上面的无序容器,哈希表的增删查的时间复杂度趋近于O(1),效率非常高
-
分治思想:如果对内存有要求,就要使用分治思想,对数据的大小进行划分。第1和第2个方法思想是解决查重问题的根本出发点,就是用哈希表。
-
Bloom Filter:布隆过滤器,节省内存,但是有点误差
-
如果是字符串类型的查重,除了哈希表,布隆过滤器,还可以使用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;
}