亿级别大数的排序查找

大数问题的分析与解决


https://xiaozhuanlan.com/topic/4953708126

https://xiaozhuanlan.com/topic/2847301659

https://xiaozhuanlan.com/topic/4198326075

技术思路

1. 容量换算

1 byte = 8 bits

1k = 1024 byte = 8*1024 bit

1M = 1024 k = 8*1024*1024 bit = 8388608 bit 800万

100M = 8亿比特

1亿 = 1 0 8 10^8

1亿整数 = 32亿比特 = 400M

int 能存的最大数字为 [ 2 31 , 2 31 1 ] [-2^{31}, 2^{31}-1] ,即 [-2147483648, 2 147 483 647] 。

正整数是 21亿个,int 总共表达的数字超过40亿。

2. 拆分

拆分目标:

  • 将数据拆分到多台机器上进行处理

  • 如果程序运行无法直接加载一个大文件到内存中,就将大文件拆分为小文件

拆分策略:

  1. 按出现的顺序进行拆分
  2. 按散列值进行拆分,选取数据的主键key,然后通过哈希取模 hash(key)%N 得到数据应该拆分到的机器编号,N是机器数量。
  3. 按数据的实际含义进行拆分,例如来自同一个地区的用户更有可能成为朋友,如果让同一个地区的用户尽可能存储在同一个机器上,那么在查找一个用户的好友信息时,就可以避免到多台机器上查找,从而降低延迟。缺点同样是需要使用查找表。

海量数据去重

HashSet

HashSet存储,能以 O ( 1 ) O(1) 的时间复杂度判断数据是否存在。考虑到海量数据,需要使用拆分的方式将数据拆分到多台机器。拆分过程可以用哈希取模实现。

BitSet

存储10亿个整数,需要120M的内存。

BitSet存储。如果海量数据是整数且范围不大时,可以使用BitSet存储,通过构建一定大小的比特数组就可以判断某个整数是否出现。

使用BitSet还可以解决一个整数出现次数的问题,例如使用两个比特数组就可以存储0-3的信息,共有四种状态。判重问题可以简单看成一个数据出现的次数是否为 1,因此一个比特数组就够了。

#include <iostream>
using namespace std;

void memoryPrint(int byteSize)
{
	if (byteSize<1024)
	{
		cout<<"Consumed mempry of Hash is "<<byteSize<<" Byte"<<endl;
	}
	else if(byteSize<1024*1024)
	{
		cout<<"Consumed mempry of Hash is "<<byteSize/1024.0<<" KB"<<endl;
	}
	else if(byteSize<1024*1024*1024)
	{
		cout<<"Consumed mempry of Hash is "<<byteSize/1024.0/1024.0<<" MB"<<endl;
	}
	else if (byteSize<1024*1024*1024*1024)
	{
		cout<<"Consumed mempry of Hash is "<<byteSize/1024.0/1024.0/1024.0<<" GB"<<endl;
	}
	else
	{
		cout<<"too big to show"<<endl;
	}
}

class BitHashSet
{
public:
	BitHashSet(int s){
		size = s;
		// size/32+1, 50个数,用2个int就可以存储了 
		hash = new int[(size>>5)+1];
		memoryPrint(4*((size>>5)+1));
	}
	~BitHashSet(){
		delete[] hash;
	}
	void set(int pos){
		if(pos>=size || pos<0){
			cout<<"exceed the border."<<endl;
			return;
		}
		int highNum = pos>>5;      //pos/32
		int lowerNum = pos & 0x1F; //pos%32,在32bit的哪一位
		hash[highNum] |= 1<<lowerNum; //将1移到 lowernum指定的位数
	}
	bool get(int pos){
		if(pos>=size || pos<0){
			cout<<"exceed the border."<<endl;
			return false;
		}
		int highNum = pos>>5;      //pos/32
		int lowerNum = pos & 0x1F; //pos%32,在32bit的哪一位
		bool isexist = (hash[highNum] & (1<<lowerNum)) != 0;
		if (isexist)
			cout<<pos<<" is marked."<<endl;
		else
			cout<<pos<<" is not contained."<<endl;
		return isexist;
	}

private:
	int size;
	int* hash;
};


int main()
{
	BitHashSet hash(1000000000);  //10亿,109.209MB
	hash.set(100);
	hash.set(20);
	hash.set(1234);
	hash.get(100);
	hash.get(21);

	system("pause");
}

布隆过滤器

能以极小的空间开销解决海量数据判重问题,但是会有一定的误判率。主要应用于网页黑名单、垃圾邮件过滤、爬虫的网址判重系统,redis防止缓存穿透(布隆过滤器用来过滤大量数据库中未存储的key)。

布隆过滤器使用BitSet来存储数据,但是经过了改进,解除了BitSet要求数据范围的限制。

存储时,要求数据先经过k个哈希函数得到k个位置,并将BitSet中对应位置设为1,在查找时,也需要先经过k个哈希函数得到k个位置,如果所有位置上都为1,那么表示这个数据存在。

由于哈希函数的特点,两个不同的数通过哈希函数得到的值可能相同。如果两个数通过 k 个哈希函数得到的值都相同,那么使用布隆过滤器会将这两个数判为相同。

可以知道,令 k (哈希函数数量)和 m(BitSet空间) 都大一些会使得误判率降低,但是这会带来更高的时间和空间开销。

布隆过滤器会误判,也就是将一个不存在的数判断为已经存在,这会造成一定的问题。例如在垃圾邮件过滤系统中,会将一个邮件误判为垃圾邮件,那么就收不到这个邮件。可以使用白名单的方式进行补救。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6O1efoBc-1586340076593)(亿级别大数的排序查找.assets/20190428151811.png)]

Trie 字典树

Trie 树又叫字典树、前缀树和单词查找树,它是一颗多叉查找树,键不是直接保存在节点中,而是由节点在树中的位置决定。

如果海量数据是字符串数据,那么可以用很小的空间开销构建一颗Trie树,空间开销和树高有关。

https://leetcode-cn.com/problems/implement-trie-prefix-tree/solution/shi-xian-trie-qian-zhui-shu-by-leetcode/

海量数据排序

外部排序

将海量数据拆分到多台机器或者多个文件,这些机器或文件称为拆分节点;然后在每个拆分节点上将数据全部读入内存并使用快速排序等方法进行排序;最后在合并节点使用多路归并方法将所有拆分节点的部分排序结果整合成最终的排序结果。外部排序也可以被称为外部归并排序。

如果不进行额外处理,合并节点仍然无法将所有数据读入内存,可以使用最小顶堆来解决这个问题。

  1. 假设有 k 个拆分节点,从这 k 个拆分节点分别读取一个最小的数据到小顶堆中。
  2. 将堆顶数据移出堆并写入合并节点的最终结果文件中。
  3. 确定刚才从堆中移除的数据属于哪个拆分节点,并从该拆分节点再读入一个数据。

但是上面的做法需要频繁地读写磁盘,可以设置输入缓存和输出缓存来解决这个问题。为每个拆分节点都设置一个输入缓存,每次将一部分数据读入输入缓存中,只有当输入缓存数据为空时才再从磁盘读入数据。并设置一个输出缓存,只有输出缓存满时才将数据写出磁盘中。

BitMap / BitSet

如果待排序的数据是整数,或者是其它范围比较小的数据,可以使用 BitMap 对其进行排序。BitMap 相当于一个比特数组,如果某个数据存在时就将对应的比特数组位置设置为 1,最后从头遍历比特数组就能得到一个排序的整数序列。

这种方法只能处理数据不重复的情况,如果数据重复,就要将比特数组转换成整数数组用于计数,这种排序方法叫做计数排序??

Trie 字符串

如果海量数据是字符串,可以使用 Trie 树来完成排序操作。先读入海量字符串数据构建一个 Trie 树,最后按字典序先序遍历 Trie 树就能得到已排序的数据。为了处理数据重复问题,可以使用 Trie 树的节点存储计数信息。

问题1:查找十亿个正整数中重复出现的一个数

位图法:位图法也就是对于出现的数,其中每1bit代表这个数,如果该位为1,则说明该数出现;如果该位为0,则说明该数没有出现。

1 byte = 8 bits

1k = 1024 byte = 8*1024 bit

1M = 1024 k = 8*1024*1024 bit = 8388608 bit 800万

100M = 8亿比特

思路1: 可以建立120M的一个位图,将其所有位设置为0,然后开始遍历这10亿个整数,每遍历一个,则对应到位图中相应的位置1,如果对应到位图中相应的位已经置1了,则说明这个数是要找的那个重复的数。用这种方法,最多就是遍历一遍,将这个10亿个正整数遍历完。而使用的内存为120M左右。

**思路2:**只用100M内存的话,那么用每个bit来表示1个数,0表示没有出现,1表示出现,我们用取模的方式num%8亿来将数映射到8亿个bit中去,最前的2亿个bit会被重复映射,而剩下的6亿个bit只被映射一次(step1,遍历一次);由于10亿个正整数,只有其中1个数重复出现过,因此如果在映射到这后6亿个bit中,若发现某bit位已经是1,那么我们就提前找到这个数了;否则我们可以认为重复的数是那些被映射到前2亿位的数。因此只要在第一遍映射中没有发现重复的数,则接下来我们只需要用4亿个bit来判断重复的数,即,前2亿个bit用来记录num/2亿=0的数,而后2亿个位用来记录num/2亿=4的数,这样同样若发现某bit位已经是1,那么我们就提前找到这个数了(step2,遍历一次)。一共遍历了2遍,时间复杂度O(2n)=O(n)。

问题2: 分割10亿个不重复的整数,查找中位数

问题3:从亿个数中找出前K个最大的数

(1)读取前100个数字,建立最大值堆。
(2)依次读取余下的数,与最大值堆作比较,维持最大值堆。可以每次读取的数量为一个磁盘页面,将每个页面的数据依次进堆比较,这样节省IO时间。
(3)将堆进行排序,即可得到100个有序最大值。

问题4:对含有亿个正整数的文件,怎么将数字进行排序

(1)磁盘合并排序

先将所有数据分成多个小文件,多个小文件采用内部排序后,再用多路合并排序完成排序输出。

​ 总数据为n, 内存中采用内部排序最多m。先分成n/m个小文件,再内部排序,第三部读取所有小文件,每次将最小的数输出即可。

(2)多通道

0~10^k-1

10k~2*10k-1

分成m个通道,读m次,每次读取在通道范围内的数,按顺序写到对应的输出文件,完成排序。

(3)bitmap排序

在内存中开10^7比特,均初始化为0,若出现则设置为1,输出为1的数即可。(此方法去重)

问题5: 20G的文件,找出出现次数最多的数字

如果有一个20g的日志文件,日志文件记录着用户访问过的url,每一行为一个url,给你一台512M的主机,找出出现次数最多的10个url

参考答案及思路:

  1. Top K算法:使用堆排序算法+大顶堆+10个元素的数组。
  • IP地址最多有2^32=4G种取值情况,所以不能完全加载到内存中处理;
  • 可以考虑采用“分而治之”的思想,按照IP地址的Hash(IP)%1024值,把海量IP日志分别存储到1024个小文件中。这样,每个小文件最多包含4MB个IP地址;
  • 对于每一个小文件,可以构建一个IP为key,出现次数为value的Hash map,同时记录当前出现次数最多的那个IP地址;
    最多的10个url

参考答案及思路:

  1. Top K算法:使用堆排序算法+大顶堆+10个元素的数组。
  • IP地址最多有2^32=4G种取值情况,所以不能完全加载到内存中处理;
  • 可以考虑采用“分而治之”的思想,按照IP地址的Hash(IP)%1024值,把海量IP日志分别存储到1024个小文件中。这样,每个小文件最多包含4MB个IP地址;
  • 对于每一个小文件,可以构建一个IP为key,出现次数为value的Hash map,同时记录当前出现次数最多的那个IP地址;
  • 可以得到1024个小文件中的出现次数最多的IP,再依据常规的排序算法得到总体上出现次数最多的IP;
发布了48 篇原创文章 · 获赞 10 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/mhywoniu/article/details/105393574