文章目录
- 前言
- 从40个亿中产生一个不存在的整数
- 位图存储数据的原理
- 使用10MB来存储
- 如何确定分块的区间
- 用2GB内存在20亿的整数中找到出现次数最多的数
- 从100亿个URL中查找的问题
- 40亿个非负整数中找出两次的数。
- 总结
前言
提示:人生之中总有空白,但有时,我们称它为人生的副歌。在一些或长或短的时间段里,您听不见它,于是以为已经忘记了这段副歌。然后,有一天,在您独自一人,周边又没有什么可以分散注意力的时候,这段副歌忽然又响起来。就像儿歌的歌词,它依旧充满魔力。 --帕特里克·莫迪亚诺《隐形墨水》
理解前面的题目,是不是很难,但是抓住重点理解起来是很容易的,这里再强调一些经典问题(查找
从40个亿中产生一个不存在的整数
题目要求:给定一个输入文件,包含40亿个非负整数,请设计一个算法,产生一个不存在该文件的整数,假如你有1GB的内存,你该怎么完成这个任务。
- 进阶:如果只有10MB的内存呢,你该怎么处理
这里不用写代码,如果你能将方法说明白,我们看看这里要怎么做。
位图存储数据的原理
假设用哈希表来存储出现过的数,如果是40亿个数都不相同,那么哈希表的记录数为40亿条,存一个32为的整数需要4B,所以最差的情况需要40亿*4B = 160亿字节,大约需要16GB的空间,这很不符合要求。
40亿*4B = 160亿字节,大约需要16GB的空间
40亿/8字节 = 5亿字节,大约需要0.5GB的空间,就可以存储了。
1 字节 = 8 位
1 B(字节)= 8 bit(位)
1 GB(千兆字节)= 1024 MB = 1024 * 1024 KB = 1024 * 1024 * 1024 B
数据量很大,采用位的方式(俗称位图)存储数据是常用的思路,那位图如何存储元素呢?我们可以使用BitMap的方式来表示数出现的情况,具体来说,是申请一个长度位 4295967295 的类型的数组bitArr(就是boolean类型),bitArr上的每一个位置只可以表示0或者1状态,8个bit为1B,所以长度就是 4295967295 的bit数组占用空间为500MB,这样就满足题目要求了。
4294967295 是一个能容纳 40亿个不同数的最小 2 的幂次减 1的值。也就是说,2的32次方(4294967296)可以表示的不同数的个数是40亿,再减去1就得到了4294967295。
那么使用bitArr需要注意什么?就是遍历这40一个数,遇到所有的数时,就把bitArr相对应得位置设置为1.例如遇到1024,就将bitArr[1024] = 1。
遍历完成后,再次遍历bitArr,看看哪个位置上没有被设置为1,这个数就是不存在40亿个数中。当然如果bitArr[5243] == 0,就可以将这个数输出出来。
位存储得核心是:我们存储得并不是这40亿个数据本身,而是其对应得位置。这一点明白就很简单了不是。
使用10MB来存储
如果只有10MB得内存,使用位图就不行了,就需要另辟蹊径了。这里我们采用分块得思想,拿时间换空间,通过两次遍历来搞定。
40亿个数需要500MB得空间,如果只有10MB得空间至少需要50块才可以。
一般来说,我们划分得是使用2得整数倍,因此分成64块最合理。
首先就是将0~4294967295(2^32 -1)这个范围平均分成64个区间,每个区间是67108864个数,例如
- 第0区间(0~67108863)
- 第1区间(67108864~134217728)
- …
- 第i区间(67108864*i~67108864(I+ 1) - 1)
- 第63区间(4227858432~4294967295)
因为一共40亿个数,所以,如果统计罗在每个区间上得数有多少,肯定有至少一个区间上得计数是小于67108864。利用这一点就可以找出其中一个没有出现过得数,具体过程是通过两次遍历来搞定。
第一次遍历:先申请长度为64得整数数组countArr[0…63],countArr[i]用来统计区间i上得有多少个,遍历40亿个数,根据当前数是多少决定哪个区间上得计数增加。例如67108865,67108865/67108864 = 1,所以第1区间上得计数增加countArr[1]++,遍历完这40亿个数之后遍历countArr,必然会有某个位置上得值(countArr[i]小于67108864,表示第i区间上至少有一个数没有出现过。我们肯定会找到至少一个这样得区间。
此时使用得内存就是countArr的大小(64*4B)是一个非常小的数。
假设找到第37区间上的计数小于67108864,那么我们对这40亿个数据进行第二次遍历
- 申请长度为67108864的BitMap,这占用的空间大约是8M,记为bitArr[0…67108863].
- 遍历这40亿个数,此时只需要关注落在第37区间上的数,即为num(num 满足 num/67108864 == 37),其他区间的数全部忽略。
- 如果步骤2的num在第37区间,将bitArr[num - 67108864*37]的值设置为1,也就是只做第37区间上的数的bitArr映射。
- 遍历完40亿个数之后,在bitArr上必然存在没有被设置1的位置,假设第i个位置上面的值没有被设置成1,那么对应的67108864*37 + i,这个数就是没有出现过的数。
总结一下进阶解法:
- 根据10MB 的内存限制,确定统计区间的大小,就是第二次遍历时bitArr大小
- 利用区间计数的方式,找到那个计数不去的区间,这个区间上肯定有没有出现的数
- 对这个区间上的数做bitmap映射,在遍历一遍bitmap,找到一个没有出现的数即可
如何确定分块的区间
上面的例子中,我们可以采用连词遍历,第一次将数据分成64块刚好解决问题。那么我们为什么不是128、32,16或者其他类型呢?
这里主要是保证第二次遍历每块都能放进去10MB的空间中,
2^23 < 10MB < 2 ^ 24,而2^23 = 8388608大约是8MB,也就是说我们依次分块的大小只能为8MB左右,在上面我们也看到了,第二次遍历如果分块是64,刚好满足要求。
所以这里我们最少要分64块,当然如果分成128块,256块也是可以的。
用2GB内存在20亿的整数中找到出现次数最多的数
题目要求:有一个包含20亿个全是32位整数的大文件,在其中找出出现次数最多的数。
要求:内存限制为2GB
想要在很多整数中找出现次数最多的数,通常的做法是使用哈希表出现的每一个数做词频统计,哈希表的key是某个整数,value是这个整数出现的次数。就本题目来说,一共20亿个数,哪怕只有一个数出现20依次,用32为的整数也是可以便是出现依次而不会产生溢出,所以哈希表的key需要占用4B,value也是4B。那么哈希表中的一条数据就要占用8B,当哈希表中的记录数有20亿时,需要至少1.6GB的内存。
如果20亿个数中不同数超过2亿中,最极端的情况时20亿个数都不同,那么在哈希变种可能要产生20亿条数据,显然这样的内存时不够用的,所以一次性用哈希表统计20亿个数的办法行不通的。
解决办法是把包含20亿个数的大文件用哈希函数分成16个小文件,根基哈希函数的性质,同一种数不能能被散列到不同的小文件上,同时每个小文件中不同的数一定不会找过2亿中,假设哈希函数足够优秀。然后对每一个小文件用哈希表来统计其中每种出现的次数,我们就能得到16个小文件中各自出现最多的数,还有各自的统计次数。接下来就是在个16个小文件各自的第一名相互比较,找到次数最多的那个。
把一个大的集合通过哈希函数分配到多台机器中,或者分配到多个文件中,这种技巧也是处理大数据面试时最常见的方法之一。但是到底分配多少台机器,分配到多少个文件中,在解题一定要先确定下来。可能在与面试官沟通的过程中面试官指定,也可能时根据具体的限制来确定,比如本体确定分成16个文件,就是根据内存限制2GB的条件来确定的。
从100亿个URL中查找的问题
题目:有一个包含100亿URL的大文件,假设每个URL占用时64B,请找出其中重复的URL。
补充题目:某搜索公司一天的用户搜索词汇时海量的(百亿数据量),请设计出一种可以求出每天热门top100词汇的可行办法。
解答:原问题的加法使用解决大数据问题的一种常规套路:把大文件通过哈希函数分配到机器或者通过哈希函数把大文件拆成小文件。一直进行这种划分,直到满足结果要求的资源限制。首先需要确认面试官时候存在资源上限,时间等。在明确限制要求之后,可以将每条URL通过哈希函数分配到若干台机器中或者拆分成小文件。这里的“若干”是由具体的资源限制计算出来的数量。
例如:将100亿字节的大文件通过哈希函数分配到100台机器上面,然后每一台机器分别统计分给自己的URL中是否有重复的URL,同时哈希函数的性质决定了同一条URL不可能分给不同的机器;或者将单机上的大文件通过哈希函数拆分成1000个小文件,对每个小文件再利用哈希表遍历,找出重复的URL;还可以在非给机器差分完后对进行排序,排序后再看看是否有重复的URL出现,总之,牢记一点,很多大数据问题都离不开分流,要么是使用哈希函数将大文件的内容分配给不同的机器;要么是用哈希函数将大文件拆分成小文件,然后处理每个小文件的集合。
补充问题:
最开始还是用哈希分流的思想来处理,把包含百亿数量的词汇文件分流到不同的机器上面,具体多少机器由面试官规定的资源或者给出的限制来决定,对每一台机器来说,如果分到的数据量依然很大的话,比如出现内存不够或者其他问题可以继续使用哈希函数把每台机器的分流文件再次拆分成小文件处理。处理小文件的时候,通过哈希表统计每种词及其频率,哈希表记录建立完成后,在遍历哈希表,遍历哈希表的过程中使用大小为100的小根堆来选出每个小文件的top100(整体未排序的top100)。每一个文件都有自己词频的小根堆,将小根堆的的词按照词频排序,就得到了每个小文件排序后的top100.然后把各个小文件排序后的top100进行外排序或者继续使用小根堆,就可以挑选出每台机器上的top100.不同机器之间的top100在进行外排序或者使用小根堆。最终求出整个百亿数据量中top100**.对于TopK问题,除了使用哈希函数分流和用哈希表做词频统计外,还经常使用堆结构和外排序的手段进行处理。**
40亿个非负整数中找出两次的数。
题目要求:32位无符号整数的范围是0~4294967295,现在又10亿个无符号整数,可以使用最多1GB的内存,找出所有出现了两次的数。
本体可以看看第一题的进阶,这里出现了限制在两次。
首先,可以使用bitmap的方式来表示出现的情况,具体来说,是申请一个长度为4294967295 * 2的bit类型数组bitArr,用2个位置来表示一个数出现的词频,1B占用8bit,所以长度为4294967295 * 2的bit类型的数组占用1GB的空间,那么怎么使用这个bitArr数组呢?遍历这40亿个无符号数,如果初次遇到就把bitArr[num*2]和bitArr[num * 2 ]设置成01,第二次遇到设置成10,第三次遇到设置11.当然以后在遇到就不关系了。遍历完成之后,再次遍历bitArr,如果发现bitArr[i * 2+1]和bitArr[i * 2]设置为10,那么i就是出现两次的数。
总结
提示:大数据的处理方式;海量数据问题;大数据压缩;大数据分块;数据流处理
如果有帮助到你,请给题解点个赞和收藏,让更多的人看到 ~ ("▔□▔)/
如有不理解的地方,欢迎你在评论区给我留言,我都会逐一回复 ~
也欢迎你 关注我 ,喜欢交朋友,喜欢一起探讨问题。