海量数据求Top k:

这类问题:
  1. 求数据 最大/最小 的前 K 个元素
  2. 求数据 第 K 个最大/最小 的元素
  3. 求数据的前 K 个重复量最多的元素
  4. 求数据的所有重复元素

首先我们要明确几个问题:

1.1. 海量数据是超出内存计算范围的,该如何将数据放入在内存遍历计算

采用分治策略,将大数据划分为内存可容纳的小数据,依次计算

大文件数据要通过哈希映射放入小文件

  1. 当文件数据都是整形数据时,我们可以推算出需要划分出多少个小文件,取一个大于等于文件个数的质数为最终推定的文件数 Z
  2. 将每次从大文件读取的数据对该质数 Z 取余的 +1,写入对应文件编号(1~Z)的文件,这种方式可以确保同一重复的数据只会出现在一个文件里,对该文件的判断结束后则代表该文件所有元素判断结束,直接获取结果即可
  3. 避免出现在判断完一个小文件的所有数据后,其他文件依然存在已经判断且重复的,造成答案不完整,结果不确定的情况

1.2. 采用什么方式去计算数据最大值区间问题:

  • 大小根堆
  1. 采用K个结点小根堆遍历,最终根节点的数据为第K 大数据,所有结点为前K个数据
  2. 采用K个结点大根堆遍历,最终根节点的数据为第K 小数据,所有结点为前K个数
  3. 快速排序的划分:(数据无序)
  • 每次判断的基准位置:
  1. 等于(K-1)时,数组前K个数据即为所求
  2. 基准index位置小于(K-1)时,递归(index +1 , end )
  3. 基准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. 位图:

方法介绍:

  1. 位图法,就是用一个比特位(0或者1)来存储数据的状态,比较适合状态简单,数据量比较大,要求内存使用率低的问题场景。
  2. 位图法解决问题,首先需要知道待处理数据中的最大值,然后按照size = (maxNumber / 8)(byte)+1的大小来开辟一个char类型的数组,当需要在位图中查找某个元素是否存在的时候,首先需要计算该数字对应的数组中的比特位,然后读取值,0表示不存在,1表示已存在。在下面的问题中看具体应用。

方法分析:

  1. 优点:位图相比哈希链表,更加节省地址空间,因为哈希链表不光存储整形数据还需要存储链表的结点
  2. 缺点:就是数据没有多少,但是最大值却很大,比如有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布隆过滤器(高级位图):

  1. Bloom Filter是通过一个位数组+k个哈希函数构成的。
  2. 所以用Bloom Filter,它需要少量的内存就可以判断元素是否存在集合当中,用a文件的数据构建Bloom Filter的位数组中的状态值,然后再读取b文件的数据进行布隆过滤的查找操作就可以了。
  3. Bloom Filter查询元素的过程就是:把元素的值通过k个哈希函数进行计算,得到k个值,然后把k当作位数组的下标,看看相应位数组下标标识的值是否全部是1,如果有一个为0,表示元素不存在(判断不存在绝对正确);如果都为1,表示元素存在(判断存在有错误率)。
  4. Bloom Filter增加元素的过程就是:把元素的值通过k个哈希函数进行计算,得到k个值,然后把k当作位数组的下标,在位数组中把相应k个值修改成1。
  5. Bloom Filter默认只支持add增加和query查询操作,不支持delete删除操作(因为存储的状态位有可能也是其它数据的状态位,删除后导致其它元素查找判断出错)。
  6. Bloom Filter的查找错误率,当然和位数组的大小,以及哈希函数的个数有关系,具体的错误率计算有相应的公式(错误率公式的掌握看个人理解,不做要求)。
  7. Bloom Filter的空间和时间利用率都很高,但是它有一定的错误率,虽然错误率很低,Bloom Filter判断某个元素不在一个集合中,那该元素肯定不在集合里面;Bloom Filter判断某个元素在一个集合中,那该元素有可能在,有可能不在集合当中。

1亿大约是100M字节的数量级,那么10亿就是1G字节的数量级,10亿个整数大约要占用4G大小的内存

1.4. 方法对比:

  1. 无序哈希容器底层采用哈希表存储,将产生冲突的哈希数据通过链表连接起来,所以在保存数据本身的基础上还增加了 指针域,大约需要4G(数据总数)+4G(指针总数)= 8G的内存空间
  2. 位图 他首先寻找数据的最大值,以此作为空间的开辟位图数组的大小,内存的使用量是4G/8 = 500M比上面使用哈希表所占用的内存大大减少                                                                  位图有一个不容忽略的缺点:当数据量很小,但是最大值极大时,将会按照最大值分配空间造成大量空闲空间的产生,浪费
  3. 布隆过滤器相比而言,空间和时间利用率都很高!但是有一部分的错误率

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个最小元素:

快速排序划分,无序对所有元素排序

  1. 求第k小的数字 ,index基准==k时
  2. 求第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中重复

  • 29
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值