这类问题:
- 求数据 最大/最小 的前 K 个元素
- 求数据 第 K 个最大/最小 的元素
- 求数据的前 K 个重复量最多的元素
- 求数据的所有重复元素
首先我们要明确几个问题:
1.1. 海量数据是超出内存计算范围的,该如何将数据放入在内存遍历计算
采用分治策略,将大数据划分为内存可容纳的小数据,依次计算
大文件数据要通过哈希映射放入小文件:
- 当文件数据都是整形数据时,我们可以推算出需要划分出多少个小文件,取一个大于等于文件个数的质数为最终推定的文件数 Z
- 将每次从大文件读取的数据对该质数 Z 取余的 +1,写入对应文件编号(1~Z)的文件,这种方式可以确保同一重复的数据只会出现在一个文件里,对该文件的判断结束后则代表该文件所有元素判断结束,直接获取结果即可
- 避免出现在判断完一个小文件的所有数据后,其他文件依然存在已经判断且重复的,造成答案不完整,结果不确定的情况
1.2. 采用什么方式去计算数据最大值区间问题:
- 大小根堆
- 采用K个结点小根堆遍历,最终根节点的数据为第K 大数据,所有结点为前K个数据
- 采用K个结点大根堆遍历,最终根节点的数据为第K 小数据,所有结点为前K个数
- 快速排序的划分:(数据无序)
- 每次判断的基准位置:
- 等于(K-1)时,数组前K个数据即为所求
- 基准index位置小于(K-1)时,递归(index +1 , end )
- 基准index位置大于(K-1)时,递归(begin , index -1 )
1. 问题1:有一组10亿个整数,整数取值范围也是0到10亿,找出第一个重复的数字?
分析:1亿大约是100M字节的数量级,那么10亿就是1G字节的数量级,10亿个整数大约要占用4G大小的内存,如果对内存有限制,就需要用到分治法的思想分段求解;如果没有内存限制要求,大可以用哈希表或者位图法来解决这样的问题
1.1. 哈希表:
有序哈希容器的底层模型是红黑树,无序哈希容器底层模型就是哈希表
如果需要使用哈希表,可以直接使用的无序容器,哈希表的增删查的时间复杂度趋近于O(1),效率非常高。
unordered_map<>,键值对中key存数据,value存出现次数,遍历value>1
int main()
{
/*
假设这个vector中,放了原始的待查重的数据
为了让程序更快的运行出结果,此处缩小了数据量
*/
vector<int> vec;
for (int i = 0; i < 100000; ++i)
{
vec.push_back(rand());
}
// 用哈希表解决查重,因为只查重,所以用无序集合解决该问题
//方法1:
//unordered_map<int,int> hashSet;
//for (int val : vec)
//{
// if (hashSet[val]++ == 1)
// {
// cout << val << "是第一个重复的数据" << endl;
// break; // 如果要找所有重复的数字,这里就不用break了
// }
//}
//方法2:
unordered_set<int>hash;
for (int val : vec)
{
auto it = find(hash.begin(), hash.end(), val);
if (it != hash.end())
{
cout << val << "是第一个重复的数据" << endl;
break; // 如果要找所有重复的数字,这里就不用break了
}
else
hash.insert(val);
}
return 0;
}
1.2. 位图:
方法介绍:
- 位图法,就是用一个比特位(0或者1)来存储数据的状态,比较适合状态简单,数据量比较大,要求内存使用率低的问题场景。
- 位图法解决问题,首先需要知道待处理数据中的最大值,然后按照size = (maxNumber / 8)(byte)+1的大小来开辟一个char类型的数组,当需要在位图中查找某个元素是否存在的时候,首先需要计算该数字对应的数组中的比特位,然后读取值,0表示不存在,1表示已存在。在下面的问题中看具体应用。
方法分析:
- 优点:位图相比哈希链表,更加节省地址空间,因为哈希链表不光存储整形数据还需要存储链表的结点
- 缺点:就是数据没有多少,但是最大值却很大,比如有10个整数,最大值是10亿,那么就得按10亿这个数字计算开辟位图数组的大小,太浪费内存空间
#include <iostream>
#include <vector>
#include <unordered_set>
using namespace std;
int main()
{
/*
假设这个vector中,放了原始的待查重的数据
为了让程序更快的运行出结果,此处缩小了数据量
*/
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]();
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] << "是第一个重复的数据" << endl;
break; // 如果要找所有重复的数字,这里就不用break了
}
else
{
// 表示该数据不存在,把相应位置置1,表示记录该数据
p[index] = p[index] | (1 << offset);
}
}
delete[]p;
return 0;
}
1.3. Bloom Filter布隆过滤器(高级位图):
- Bloom Filter是通过一个位数组+k个哈希函数构成的。
- 所以用Bloom Filter,它需要少量的内存就可以判断元素是否存在集合当中,用a文件的数据构建Bloom Filter的位数组中的状态值,然后再读取b文件的数据进行布隆过滤的查找操作就可以了。
- Bloom Filter查询元素的过程就是:把元素的值通过k个哈希函数进行计算,得到k个值,然后把k当作位数组的下标,看看相应位数组下标标识的值是否全部是1,如果有一个为0,表示元素不存在(判断不存在绝对正确);如果都为1,表示元素存在(判断存在有错误率)。
- Bloom Filter增加元素的过程就是:把元素的值通过k个哈希函数进行计算,得到k个值,然后把k当作位数组的下标,在位数组中把相应k个值修改成1。
- Bloom Filter默认只支持add增加和query查询操作,不支持delete删除操作(因为存储的状态位有可能也是其它数据的状态位,删除后导致其它元素查找判断出错)。
- Bloom Filter的查找错误率,当然和位数组的大小,以及哈希函数的个数有关系,具体的错误率计算有相应的公式(错误率公式的掌握看个人理解,不做要求)。
- Bloom Filter的空间和时间利用率都很高,但是它有一定的错误率,虽然错误率很低,Bloom Filter判断某个元素不在一个集合中,那该元素肯定不在集合里面;Bloom Filter判断某个元素在一个集合中,那该元素有可能在,有可能不在集合当中。
1亿大约是100M字节的数量级,那么10亿就是1G字节的数量级,10亿个整数大约要占用4G大小的内存
1.4. 方法对比:
- 无序哈希容器底层采用哈希表存储,将产生冲突的哈希数据通过链表连接起来,所以在保存数据本身的基础上还增加了 指针域,大约需要4G(数据总数)+4G(指针总数)= 8G的内存空间
- 位图 他首先寻找数据的最大值,以此作为空间的开辟位图数组的大小,内存的使用量是4G/8 = 500M,比上面使用哈希表所占用的内存大大减少 位图有一个不容忽略的缺点:当数据量很小,但是最大值极大时,将会按照最大值分配空间造成大量空闲空间的产生,浪费
- 布隆过滤器相比而言,空间和时间利用率都很高!但是有一部分的错误率
2. 问题2:计算大量数据中的重复次数最多的前K位重复元素:
map哈希表+priority_queue优先级队列
//小根堆:(出最小,留最大)map+priority_queue
using P = pair<int, int>;
//构造函数对象的方法2:创建类,包含对()重载的函数,称之为函数对象
class Com
{
public:
bool operator()(const P& first, const P& second)
{
return first.second > second.second;
}
};
int main()
{
int k = 10;
vector<int>v(100000);
for (int i = 0; i < 100000; i++)
{
v[i]=(rand() + i);
}
unordered_map<int, int>map;
for (int val : v)
{
map[val]++;
}
//自定义储存数对的优先级队列,
//在构造过程中需要我们自行完成优先级队列的排序函数对象
priority_queue<P,vector<P>,Com>minhead;
// 有两种方式构造函数对象:
//拉姆达表达式是函数也可以充当函数对象,对于只是用在优先级队列中的函数对象,
无需构造一个只包含一个重载运算符的类
//using Func = function<bool(P&, P&)>;
//using Minhead=priority_queue<P, vector<P>, Func>;
//Minhead min([](P& a, P& b)->bool {
// return a.second > b.second;
// });
for (auto it : map)
{
if(minhead.size() < k)
while (minhead.size() < k)
{
minhead.push(it);
}
else
{
if (it.second > minhead.top().second)
{
minhead.pop();
minhead.push(it);
}
}
}
//第十大个k的重复元素:
cout << "第十大个重复元素"<< minhead.top().first << endl;
while (!minhead.empty())
{
cout << "数字:" << minhead.top().first << "次数:" << minhead.top().second;
minhead.pop();
}
return 0;
}
3. 问题3:计算大量无序元素的前K个最小元素、第K个最小元素:
快速排序划分,无序对所有元素排序
- 求第k小的数字 ,index基准==k时
- 求第k大的数字原理相同,index基准==vector.size()-k;
//快速分割:
int Quikone(vector<int>&v, int begin, int end)
{
int jude = v[begin];
while (begin < end)
{
while (begin<end && v[end]>=jude)
end--;
if (end > begin)
v[begin++] = v[end];
while (begin < end && v[begin] <= jude)
begin++;
if (begin < end)
v[end--] = v[begin];
}
v[end] = jude;
return end;
}
int Quiksort(vector<int>& v, int begin, int end, int k)
{
int index = Quikone(v, begin, end);
if (index == k - 1)
return index;
else if (index > k - 1)
index = Quiksort(v, begin, index - 1, k);
else
index = Quiksort(v, index + 1, end, k);
}
int main()
{
int k = 5;
vector<int>v;
for (int i = 0; i < 10000; i++)
{
v.push_back(rand() + i);
}
unordered_map<int, int>map;
//前五个最小的元素
//快速排序:
Quiksort(v, 0, v.size() - 1, k);
//第k的元素:
cout << "第k个元素" << v[k-1] << endl;
for(int i=0;i<10;i++)
{
cout << "数字:" << v[i] << " ";
}
cout << endl;
return 0;
}
4. 问题4:模拟超出内存一次计算的大文件数据统计重复次数前k
int main()
{
//判断重复量最大的前十个数据
int topk = 10;
FILE* pf1 = fopen("data.dat", "wb");
for (int i = 0; i < 200000; ++i)
{
int data = rand();
fwrite(&data, 4, 1, pf1);
}
fclose(pf1);
FILE* pf = fopen("data.dat", "rb");
if (pf == nullptr)
{
printf("open file error");
return 0;
}
const int Num = 11;
FILE* pfile[Num] = {};
for (int i = 0; i < Num; i++)
{
char filename[20];
sprintf_s(filename,sizeof(filename),"data[%d].dat", i + 1);
pfile[i] = fopen(filename, "wb+");
}
int data;
while (fread(&data, 4, 1, pf) > 0)//从大文件读取所有
{
int index = data % Num;//数字对应哈希映射进入下标文件写入
fwrite(&data, 4, 1, pfile[index]);
}
unordered_map<int, int>map;
using P = pair<int, int>;
using Func = function<bool(P&,P&)>;
using Minheap = priority_queue<P, vector<P>, Func>;
Minheap minheap([](P& a, P& b)->bool {
return a.second > b.second;
});
for (int i = 0; i < Num; i++)
{
//逐个恢复小文件的内部文件指针至开头
fseek(pfile[i], 0, SEEK_SET);
//将该文件的数据写入哈希计数表
while (fread(&data, 4, 1, pfile[i]) > 0)
{
map[data]++;
}
//开始小根堆
auto it = map.begin();
for (; it != map.end() ; it++ )
{
int k = 0;
if (minheap.empty())//填满根堆
{
while(k++<topk&& it != map.end())
minheap.push(*it);
}
if (it->second > minheap.top().second)//依次替换
{
minheap.pop();
minheap.push(*it);
}
}
fclose(pfile[i]);
map.clear();
}
while (!minheap.empty())
{
cout << minheap.top().first << ":" << minheap.top().second<<endl;
minheap.pop();
}
fclose(pf);
return 0;
}
5. 问题5:如果分为两个大文件,均保存大量整形数据,希望你比对这两个文件中的所有重复元素
与上面的思路解法基本一致,但是将两个A、B大文件,分别创建可存储的若干相同数量的a、b小文件,
重点!采用哈希映射方式将大文件数据写入小文件,这一步骤可以保证AB文件中的重复元素均处于a、b相同下标的小文件中,只需遍历每个下标相同的小文件:(哈希map遍历完a后,遍历b时若查找到对一个key大于0的value值),则可知该元素是在AB中重复