top K、重复、排序问题

①Top K问题:分治+Trie树/Hash_map+小顶堆。采用Hash(x)%M将原文件分割成小文件,如果小文件太大则继续Hash分割,直至可以放入内存。然后使用Trie树来Hash统计每个小数据集中的query词频,之后用小顶堆求出每个数据集中出现频率最高的前K个数,最后在所有top K中求出最终的top K。

②重复问题:BitMap位图 或 Bloom Filter布隆过滤器 或 Hash_set集合。每个元素对应一个bit处理。

③排序问题:外排序 或 BitMap位图。分割文件+文件内排序+文件之间归并。

④小内存问题:在有限的内存实现对较大的数据进行排序

 

对于大文件大数据要注意内存能够装入的大小:

1TB=1024GB=10242MB=10243KB=10244B=8*1024*1024*1024*1024Bit

 

TopK问题

1、10亿个整数中找出最大的10000个数

方法1:先拿10000个数建最小堆,然后循环遍历剩余元素,如果大于堆顶的数(10000中最小的),将这个数替换堆顶,并调整结构使之仍然是一个最小堆,这样,遍历完后,堆中的10000个数就是所需的最大的10000个。建堆时间复杂度是O(mlogm),算法的时间复杂度为O(nmlogm)(n为10亿,m为10000)。小根堆是最好的方法

方法2:采用快速排序的思想,每次分割之后只考虑比标兵值大的那一部分,直到大的部分在比10000多且不能分割的时候,采用传统排序算法排序,取前10000个。复杂度为O(10000w*10000)。

方法3:局部淘汰法。取前10000个元素并排序,然后依次扫描剩余的元素,插入到排好序的序列中,并淘汰最小值。复杂度为O(10000w * lg10000)  (lg10000为二分查找的复杂度,这种方法无需考虑)。

 

2、有1亿个浮点数,如果找出其中最大的10000个?(考虑内存空间)

方法一:最容易想到的方法是将数据全部排序,然后在排序后的集合中进行查找,最快的排序算法的时间复杂度一般为O(nlogn),如快速排序。但是在32位的机器上,每个float类型占4个字节,1亿个浮点数就要占用400MB的存储空间,对于一些可用内存小于400M的计算机而言,很显然是不能一次将全部数据读入内存进行排序的。其实即使内存能够满足要求,该方法也并不高效,因为题目的目的是寻找出最大的10000个数即可,而排序却是将所有的元素都排序了,做了很多的无用功。

 

方法二:第二种方法采用最小堆。首先读入前10000个数来创建大小为10000的最小堆,建堆的时间复杂度为O(mlogm)(m为数组的大小即为10000),然后遍历后续的数字,并与堆顶(最小)数字进行比较。如果比最小的数小,则继续读取后续数字;如果比堆顶数字大,则替换堆顶元素并重新调整堆为最小堆。整个过程直至1亿个数全部遍历完为止。然后按照中序遍历的方式输出当前堆中的所有10000个数字。该算法的时间复杂度为O(nmlogm),空间复杂度是10000(常数)。

 

方法三:第三种方法是分治法,将1亿个数据分成100份,每份100万个数据,找到每份数据中最大的10000个,最后在剩下的100*10000=100万个数据里面找出最大的10000个。如果100万数据选择足够理想,那么可以过滤掉1亿数据里面99%的数据。

100万个数据里面查找最大的10000个数据的方法如下:

用快速排序的方法,将数据分为2堆,如果大的那堆(指partition右边的数组)个数N大于10000个,继续对大堆快速排序partition一次分成2堆,如果大堆个数N小于10000个,就在小的那堆里面快速排序一次,找第10000-n大的数字;递归以上过程,就可以找到第1w大的数。参考上面的找出第1w大数字,就可以类似的方法找到前10000大数字了。此种方法需要每次的内存空间为10^6*4=4MB,一共需要101次这样的比较。

 

方法四:第四种方法是Hash法。如果这1亿个书里面有很多重复的数,先通过Hash法,把这1亿个数字去重复,这样如果重复率很高的话,会减少很大的内存用量,从而缩小运算空间,然后通过分治法或最小堆法查找最大的10000个数。

 

 

3、有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。

①分治:顺序读文件,对每个词x取Hash(x)%2000,按照该值存到2000个小文件中。每个文件是500k左右。如果有文件超过了1M则继续分割。O(N)

 

②Trie树/Hash_map:字符串用Trie树最好。对每个小文件,统计其中出现的词频。O(N)*(平均字符长度),长度一般是常数,也就是O(N). 

 

③小顶堆:用容量为100的小顶堆,以频率为value值插入,取每个文件出现频率最大的100个词,把这100个词及相应的频率存入文件。最差O(N)*lg(100),也就是O(N).

注:2,3步骤合起来需要一轮磁盘存取过程。存入文件的个数可以缩减一下,因为主要开销在磁盘读取上,减少文件读取次数,可以在每个文件存取最大容量的字符数量,比如这道题1*(M/16字节字符串长度+频率(int)8字节)的数存到一个文件中。比如20000个词存在一个文件中,可以缩减到10个文件。这样最后一步只需要读取10次就可以了。

 

④归并:将得到的10个文件里面的数进行归并,取前100个词。注:我觉得其实不需要多路归并,因为只需要找top100的数,归并排序首先是nlgn的复杂度,第二是频繁的磁盘存取,这里最好是还是在内存建立容量为100的小顶堆,依次读文件,遍历每个文件中的元素更新小顶堆,这样只需10次存取,并且时间复杂度是nlog100,也就是O(n)的。

 

注释:为什么说用Trie树好, Trie树是空间换时间,而这道题是空间敏感,那么为什么我们还要使用呢?

1.字符串会通过一个hash算法(BKDRHash,APHash,DJBHash,JSHash,RSHash,SDBMHash,可以自己看一下,基本就是按位来进行hash的)映射为一个正整数然后对应到hash表中的一个位置,表中记录的value值是次数,这样统计次数只需要将字符串hash一下找到对应位置把次数+1就行了。如果这样的话hash中是不是不用存储字符串本身?如果不存储字符串本身,那应该是比较省空间的。而且效率的话因为Tire树找到一个字符串也是要按位置比较一遍,所以效率差不多呀。但是,其实字符串的hash是要存储字符串本身的,不管是开放地址法还是散列表法,都无法做到不冲突。除非桶个数是字符串的所有情况26^16,那是肯定空间不够的,因此hash表中必须存着字符串的值,也就是key值。字符串本身,那么hash在空间上肯定是定比不过Trie树的,因为Trie树对公共前缀只存储一次。

2.为什么说Trie树是空间换时间呢,我觉得网上这么说不甚合理,这句话其实是相对于二叉查找树来说的,之所以效率高,是因为二叉查找树每次查找都要比较大小,并且因为度为2,查找深度很大,比较次数也多,因此效率差。而Trie树是按位进行hash的,比如26个字母组成的字符串,每次找对应位的字符-‘a’就是位置了。而且出度是26,查找深度就是字符串位数,查找起来效率自然就很快。但是为啥说是空间换时间,是因为字符串的Trie树若想存储所有的可能字符串,比如16位,一个点要对应下一位26种情况,也就是26个分支,也得26^16个位置,所以空间是很大的。但是Trie树的话可以采用依次插入的,不需要每个点记录26个点,而是只存在有值的分支,Trie树节点只要存频率次数,插入的流程就是挨个位子找分支,没有就新建,有就次数+1就行了。因此空间上很省,因为重复前缀就统计一次,而效率很高,O(length)。

 

4、现有海量日志数据,要提取出某日访问百度次数最多的那个IP(可以将题干简化,假设日志中仅包含IP数据,也就是说待处理的文件中包含且仅包含全部的访问IP,但内存空间有限,不能全部加载,假设只有512MB)

这是一道典型的分治思想的题目,这种问题处理起来套路比较固定,对于大部分的数据量比较大的前提的问题而言,分治都是一个可选的解决方案,但不一定是最优的,解决方法基本划分为三步走:

第一:如何有效的划分数据

第二:如何在子集上解决问题

第三:如何合并结果

①分治:IP是32位,共有232个IP。访问当天的日志,将IP取出来,采用Hash,比如模1000,把所有IP存入1000个小文件。

②Hash_map:统计每个小文件中出现频率最大的IP,记录其频率。

③小顶堆:这里用一个变量即可。在这1000个小文件各自最大频率的IP中,直接找出频率最大的IP。

 

 

5、海量数据分布在100台电脑中,想个办法高效统计出这批数据的TOP10。

注:主要不同点在于分布式

分析:虽然数据已经是分布的,但是如果直接求各自的Top10然后合并的话,可能忽略一种情况,即有一个数据在每台机器的频率都是第11,但是总数可能属于Top10。所以应该先把100台机器中相同的数据整合到相同的机器,然后再求各自的Top10并合并。

①分治:顺序读每台机器上的数据,按照Hash(x)%100重新分布到100台机器内。接下来变成了单机的topk问题。单台机器内的文件如果太大,可以继续Hash分割成小文件。

②Hash_map:统计每台机器上数据的频率。

③小顶堆:采用容量为10的小顶堆,统计每台机器上的Top10。然后把这100台机器上的TOP10组合起来,共1000个数据,再用小顶堆求出TOP10。

 

 

6、一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前10个词,请给出思想,给出时间复杂度分析。 

①分治:一万行不算多,不用分割文件。(此处如果是有上亿行,无法一次性装内存才需要使用分治法)

②Trie树:统计每个词出现的次数,时间复杂度是O(n*le)  (le表示单词的平均长度)

③小顶堆:容量为10的小顶堆,找出词频最多的前10个词,时间复杂度是O(n*lg10)  (lg10表示堆的高度)。(如果有用过分治,则需要先统计每个文件的前10个词,再统一计算)

总的时间复杂度是 O(n*le)与O(n*lg10)中较大的那一个。

 

 

top K问题总结:很适合采用MapReduce框架解决,用户只需编写一个Map函数和两个Reduce 函数,然后提交到Hadoop(采用Mapchain和Reducechain)上即可解决该问题。具体而言,就是首先根据数据值或者把数据hash(MD5)后的值按照范围划分到不同的机器上,最好可以让数据划分后一次读入内存,这样不同的机器负责处理不同的数值范围,实际上就是Map。得到结果后,各个机器只需拿出各自出现次数最多的前N个数据,然后汇总,选出所有的数据中出现次数最多的前N个数据,这实际上就是Reduce过程。对于Map函数,采用Hash算法,将Hash值相同的数据交给同一个Reduce task;对于第一个Reduce函数,采用HashMap统计出每个词出现的频率,对于第二个Reduce 函数,统计所有Reduce task,输出数据中的top K即可。

 

 

重复问题

1. 给定a、b两个文件,各存放50亿个url,每个url各占64字节,内存限制是4G,让你找出a、b文件共同的url?

分析:每个文件的大小约为5亿×64≈320G,远远大于内存大小。考虑采取分而治之的方法。

方法1:

①分治:遍历文件a,对每个url求Hash%1000,根据值将url分别存储到1000个小文件中,每个小文件约为300M。文件b采用同样hash策略分到另外的1000个小文件中。上述两组小文件中,只有相同编号的小文件才可能有相同元素。

②Hash_set:读取a组中一个小文件的url存储到hash_set中,然后遍历b组中相同编号小文件的每个url,查看是否在刚才构建的hash_set中。如果存在,则存到输出文件里。

 

方法2:

如果允许有一定的错误率,可以使用Bloom filter,使用位数组,4G内存大概可以表示340亿bit。将其中一个文件中的url使用Bloom filter映射为这340亿bit,然后挨个读取另外一个文件的url,检查是否在Bloom filter中。如果是,那么该url应该是共同的url(注意会有一定的错误率)。

 

 

2. 在2.5亿个整数中找出不重复的整数,注意内存不足以容纳这2.5亿个整数。

分析:2.5亿个整数(4Byte)大概是954MB,也不是很大。当给出的整数个数更加大的时候,可以使用下面的方法一,否则方法二较为合适。

方法1:这种方法适合用在当给出的整数个数更大的时候,当前题目可不用这种方法

采用2-Bitmap,每个数分配2bit,00表示不存在,01表示出现一次,10表示多次,11无意义。整数范围为232个,则需要内存2*232÷8byte÷1024kb÷1024mb÷1024gb=1G。然后扫描这2.5亿个整数,查看Bitmap中相对应位,如果是00变01,01变10,10保持不变。全部扫描完后,查看Bitmap,把对应位是01的整数输出。

 

方法2:

分治法,Hash分割成小文件处理。注意hash保证了每个文件中的元素一定不会在其他文件中存在。利用Hash_set,在小文件中找出不重复的整数,再进行归并。

 

 

3. 一个文件包含40亿个整数,找出不包含的一个整数。分别用1GB内存和10MB内存处理。

    全部存放需要:40 * 10^8 * 4B = 16GB (大约值,因为不是按照2的幂来做单位换算)

1GB内存:使用Bitmap:对于32位的整数,共有232个,每个数对应一个bit,共需0.5GB内存。遍历文件,将每个数对应的bit位置1。最后查找0bit位即可。

10MB内存:10MB = 8 × 107bit

https://blog.csdn.net/shangqing1123/article/details/47680527

如果我们只有10MB的内存,明显使用Bit Map也无济于事了。 内存这么小,注定只能分块处理。我们可以将这么多的数据分成许多块, 比如每一个块的大小是2^20bit(一个bit代表一个数,10M内存中、一次存一个块),那么第一块保存的就是0到2^20 -1bit的数,第2块保存的就是2^20到2^21 -1的数……实际上我们并不保存这些数,而是给每一个块设置一个计数器。 这样每读入一个数,我们就在它所在的块对应的计数器加1。处理结束之后, 我们找到一个块,它的计数器值小于块大小(2^20), 说明了这一段里面一定有数字是文件中所不包含的。然后我们单独处理这个块即可。

也就是设置一个bucket[块数=4096],再设置一个value[每块中的数的数量],遍历数字的时候取余2^20得出在其在哪个bucket,再根据这个bucket去给bucket [i]+1,最后检测到bucket[i]中值的数量<2^20,就说明这里面有不包含的数,那么就得在这里面进行bitmap查找

值的确定:

10MB——>2^23Byte。一个整数4个字节,因此,最多包含2^21个元素的数组。

bitSize=(2^32/blockNum)<=2^21,所以,blockNum>=2^11。

2^11<=bitSize<=2^21

int bitSize=1048576=1024*1024=2^20   第一次扫描时每个块范围的大小

int blockNum=4096=4*1024=4*2^10    第一次扫描时块的个数

①分治:将所有整数分段,每2^20bit个数对应一个小文件,共4096个小文件。

②第一次遍历所有整数,对每个数取余2^20得i,再让bucket[i]++;

③遍历bucket,找到一个bucket的值小于2^20的bucket[i],那么不存在的数就在这里面,记录当前bucket的位置已经开始的值start

④再遍历所有整数,把整数范围在bucket[i]的数存入一个数组range[bitSize]

⑤遍历range[],找到值为0的数对应的索引,计算出其数值

 

 

4. 有10亿个URL,每个URL对应一个非常大的网页,怎样检测重复的网页?

分析:不同的URL可能对应相同的网页,所以要对网页求Hash。1G个URL+哈希值,总量为几十G,单机内存无法处理。

①分治:根据Hash%1000,将URL和网页的哈希值分割到1000个小文件中,注意:重复的网页必定在同一个小文件中。

②Hash_set:顺序读取每个文件,将Hash值加入set集合,如果已存在则为重复网页。 

 

 

 

排序问题

1. 有10个文件,每个文件1G,每个文件的每一行存放的都是用户的query,每个文件的query都可能重复。要求按照query的频度排序。

方法1: 

①分治:顺序读10个文件,按照Hash(query)%10的结果将query写入到另外10个文件。新生成的每个文件大小为1G左右(假设hash函数是随机的)。

②Hash_map:找一台内存为2G左右的机器,用Hash_map(query, query_count)来统计次数。

③内排序:利用快速/堆/归并排序,按照次数进行排序。将排序好的query和对应的query_count输出到文件中,得到10个排好序的文件。

④多路归并:将这10个文件进行归并排序。

 

方法2:

一般query的总量是有限的,只是重复的次数比较多。对于所有的query,一次性就可能加入到内存。这样就可以采用Trie树/Hash_map等直接统计每个query出现的次数,然后按次数做快速/堆/归并排序就可以了

 

 

2. 一共有N个机器,每个机器上有N个数。每个机器最多存O(N)个数并对它们操作。如何找到这N^2个数的中位数?(中位数:排序后的中间的那个数)

方法1: 32位的整数一共有232个

①分治:把0到232-1的整数划分成N段,每段包含232/N个整数。扫描每个机器上的N个数,把属于第一段的数放到第一个机器上,属于第二段的数放到第二个机器上,依此类推。 (如果有数据扎堆的现象,导致数据规模并未缩小,则继续分割)

②找中位数的机器:依次统计每个机器上数的个数并累加,直到找到第k个机器,加上其次数则累加值大于或等于N2/2,不加则累加值小于N2/2。

③找中位数:设累加值为x,那么中位数排在第k台机器所有数中第N2/2-x位。对这台机器的数排序,并找出第N2/2-x个数,即为所求的中位数。

复杂度是O(N2)。

 

方法2:

①内排序:先对每台机器上的数进行排序。

②多路归并:将这N台机器上的数归并起来得到最终的排序。找到第N2/2个数即是中位数。

复杂度是O(N2*lgN)。

 

 

小内存问题

1、设计一个算法,实现两个10g大文件在10m的内存中将两个大文件中重复的值放进第三个文件

       ①对第一个10g文件进行hash存储,将值存放到10g/4m=2560个文件中,每个文件4m,序号1.2.3...

       ②再对第二个10g文件进行同样操作

       ③将同序号的两个4m左右的文件放到内存中,对他们进行遍历比较(因为他们都是hash存储到同一个序号的文件,所以如果有相同的值肯定能够比较出来,其他序号的文件不可能有相同的值了),比较到相同的值后将其存放到剩余的2m内存中,最后再将这2m内存中的内容写回磁盘,然后继续读取序号为2的两个文件进行同样的过程

 

2、2G内存对1000G数据进行排序

假设要对1000G数据用2G内存进行排序

方法:每次把2G数据从文件传入内存,用一个“内存排序”算法排好序后,再写入外部磁盘的一个2G的文件中,之后再从1000G中载入第二个2G数据。循环500遍。就得到500个文件,每个文件2G,文件内部都是有序的。

然后进行归并排序,比较第1/500和2/500的文件,分别读入750MB进入内存,内存剩下的500MB用来临时存储生成的数据,直到将两个2G文件合并成4G,再进行后面两个2G文件的归并……。另外,也可以用归并排序的思想同时对500个2G的文件直接进行归并

问:还能进行优化吗?

优化思路:

  1. 增设一个缓冲buffer,加速从文件到内存的转储

        假设这个buffer已经由系统帮我们优化了

  1. 使用流水线的工作方式,假设从磁盘读数据到内存为L,内存排序为S,排完写磁盘为T,因为L和S都是IO操作,比较耗时间,所以可以用流水线,在IO操作的同时内存也在进行排序
  2. 以上流水线可能会出现内存溢出的问题,所以需要把内存分为3部分。即每个流水线持有2G/3的内存。
  3. 在归并排序上进行优化,最后得到的500个2G文件,每次扫描文件头找最小值,最差情况要比较500次,平均时间复杂度是O(n),n为最后得到的有序数组的个数,优化思路是:维护一个大小为n的“最小堆”,每次返回堆顶元素(当前文件头数值最小的那个值),判断弹出的最小值是属于哪个文件的,将哪个文件此时的头文件所指向的数再插入最小堆中,文件指针自动后移,插入过程为logn,最小堆返回最小值为o(1),运行时空间复杂度为o(n)

参考链接:https://blog.csdn.net/juzihongle1/article/details/70212243

猜你喜欢

转载自blog.csdn.net/qq_35642036/article/details/82854063
今日推荐