算法通关村第十五关——海量数据场景下的热门算法题(白银)

算法通关村第十五关——海量数据场景下的热门算法题(白银)

  • 算法通关村第十五关——海量数据场景下的热门算法题(白银)
    • 1 从40个亿中产生一个不存在的整数
      • 1.1 位图存储大数据的原理
      • 1.2 使用10MB来存储
      • 1.3 如何确定分块的区间
    • 2 用 2GB 内存在 20 亿个整数中找到出现次数最多的数
    • 3 从100 亿个 URL中查找的问题
    • 4 40 亿个非负整数中找到出现两次的数

算法通关村第十五关——海量数据场景下的热门算法题(白银)

1 从40个亿中产生一个不存在的整数

题目要求:给定一个输入文件,包含40亿个非负整数,请设计一个算法,产生一个不存在该文件中的整数,假设你有1GB的内存来完成这项任务。

进阶:如果只有10MB的内存可用,该怎么办?

本题不用写代码,如果能将方法说清楚就很好了 ,我们接下来一步步分析该如何做。

1.1 位图存储大数据的原理

假设用哈希表来保存出现过的数,如果 40 亿个数都不同,则哈希表的记录数为 40 亿条,存一个 32 位整数需要 4B,所以最差情况下需要 40 亿*4B=160 亿字节,大约需要16GB 的空间,这是不符合要求的。

  • 40 亿*4B=160 亿字节,大约需要16GB

  • 40 亿/8 字节=5亿字节,大约0.5GB的数组就可以存下40亿个。

如果数据量很大,采用位方式(俗称位图)存储数据是常用的思路,那位图如何存储元素的呢? 我们可以使用 bit map 的方式来表示数出现的情况。具体地说, 是申请一个长度为 4 294 967 295 的 bit 类型的数组 bitArr(就是boolean类型),bitArr 上的每个位置只可以表示 0 或1 状态。8 个bit 为 1B,所以长度为 4 294 967 295 的 bit 类型的数组占用 500MB 空间,这就满足题目给定的要求了。

那怎么使用这个 bitArr 数组呢?就是遍历这 40 亿个无符号数,遇到所有的数时,就把 bitArr 相应位置的值设置为 1。例如,遇到 1000,就把bitArr[1000]设置为 1。

遍历完成后,再依次遍历 bitArr,看看哪个位置上的值没被设置为 1,这个数就不在 40 亿个数中。例如,发现 bitArr[8001]==0,那么 8001 就是没出现过的数,遍历完 bitArr 之后,所有没出现的数就都找出来了。

位存储的核心是:我们存储的并不是这40亿个数据本身,而是其对应的位置。这一点明白

的话,整个问题就迎刃而解了。

1.2 使用10MB来存储

如果现在只有 10MB 的内存,此时位图也不能搞定了,我们要另寻他法。这里我们使用分块思想,时间换空间,通过两次遍历来搞定。

40亿个数 需要500MB的空间,那如果只有10MB的空间,至少需要50个块才可以。

一般来说,我们划分都是使用2的整数倍,因此划分成64个块是合理的。

首先,将0~4 294 967 295(2^32) 这个范围是可以平均分成 64 个区间的,每个区间是 67 108 864 个数,例如:

第0 区间(0~67 108 863)

第 1 区间(67 108 864~134 217 728)

i 区间(67 108 864´I~67 108 864´(i+1)-1),

……,

第 63 区间(4 227 858 432~4 294 967 295)。

因为一共只有 40 亿个数,所以,如果统计落在每一个区间上的数有多少,肯定有至少一个区间上的计数少于67 108 864。利用这一点可以找出其中一个没出现过的数。具体过程是通过两次遍历来搞定:

第一次遍历,先申请长度为 64 的整型数组 countArr[0…63],countArr[i]用来统计区间 i上的数有多少。遍历 40 亿个数,根据当前数是多少来决定哪一个区间上的计数增加。

例如,如果当前数是 3 422 552 090 , 3 422 552 090/67 108 864=51 , 所以第 51 区间上的计数增加countArr[51]++。遍历完 40 亿个数之后,遍历 countArr,必然会有某一个位置上的值(countArr[i]) 小于 67 108 864,表示第 i 区间上至少有一个数没出现过。我们肯定会找到至少一个这样的区间。

此时使用的内存就是countArr 的大小(64*4B),是非常小的。

假设找到第 37 区间上的计数小于 67 108 864,那么我们对这40亿个数据进行第二次遍历:

  1. 申请长度为 67 108 864 的 bit map,这占用大约 8MB 的空间,记为 bitArr[0…67108863]。

  2. 遍历这 40 亿个数,此时的遍历只关注落在第 37 区间上的数,记为 num(num满足num/67 108 864==37),其他区间的数全部忽略。

  3. 如果步骤 2 的 num 在第 37 区间上,将 bitArr[num - 67108864*37]的值设置为 1,也就是只做第 37 区间上的数的 bitArr 映射。

  4. 遍历完 40 亿个数之后,在 bitArr 上必然存在没被设置成 1 的位置,假设第 i 个位置上的值没设置成 1,那么 67 108 864´37+i 这个数就是一个没出现过的数。

总结一下进阶的解法:

  1. 根据 10MB 的内存限制,确定统计区间的大小,就是第二次遍历时的 bitArr 大小。

  2. 利用区间计数的方式,找到那个计数不足的区间,这个区间上肯定有没出现的数。

  3. 对这个区间上的数做 bit map 映射,再遍历bit map,找到一个没出现的数即可。

1.3 如何确定分块的区间

在上面的例子中,我们看到采用两次遍历,第一次将数据分成64块刚好解决问题。那我们为什么不是128块、32块、16块或者其他类型呢?

这里主要是要保证第二次遍历时每个块都能放进这10MB的空间中。223<10MB<224,而2^23=8388608大约为8MB,也就说我们一次的分块大小只能为8MB左右。在上面我们也看到了,第二次遍历时如果分为64块,刚好满足要求。

所以在这里我们最少要分成64块,当然如果分成128块、256块等也是可以的。

2 用 2GB 内存在 20 亿个整数中找到出现次数最多的数

题目要求:有一个包含 20 亿个全是 32 位整数的大文件,在其中找到出现次数最多的数。

要求,内存限制为 2GB。

想要在很多整数中找到出现次数最多的数,通常的做法是使用哈希表对出现的每一个数做词频统计,哈希表的 key 是某一个整数,value 是这个数出现的次数。就本题来说,一共有 20 亿个数,哪怕只是一个数出现了 20 亿次,用 32 位的整数也可以表示其出现的次数而不会产生溢出,所以哈希表的 key 需要占用 4B,value 也是 4B。那么哈希表的一条记录(key,value)需要占用 8B,当哈希表记录数为 2 亿个时,需要至少 1.6GB 的内存。

如果 20 亿个数中不同的数超过 2 亿种,最极端的情况是 20 亿个数都不同,那么在哈希表中可能需要产生 20 亿条记录,这样内存会不够用,所以一次性用哈希表统计 20 亿个数的办法是有很大风险的。

解决办法是把包含 20 亿个数的大文件用哈希函数分成 16 个小文件,根据哈希函数的性质,同一种数不可能被散列到不同的小文件上,同时每个小文件中不同的数一定不会大于 2 亿种, 假设哈希函数足够优秀。然后对每一个小文件用哈希表来统计其中每种数出现的次数,这样我们就得到了 16 个小文件中各自出现次数最多的数,还有各自的次数统计。接下来只要选出这16 个小文件各自的第一名中谁出现的次数最多即可。

把一个大的集合通过哈希函数分配到多台机器中,或者分配到多个文件里,这种技巧是处理大数据面试题时最常用的技巧之一。但是到底分配到多少台机器、分配到多少个文件,在解题时一定要确定下来。可能是在与面试官沟通的过程中由面试官指定,也可能是根据具体的限制来确定,比如本题确定分成 16 个文件,就是根据内存限制 2GB 的条件来确定的。

3 从100 亿个 URL中查找的问题

题目:有一个包含 100 亿个 URL 的大文件,假设每个 URL 占用 64B,请找出其中所有重复的 URL。

补充问题:某搜索公司一天的用户搜索词汇是海量的(百亿数据量),请设计一种求出每天热门 Top 100 词汇的可行办法。

**解答:**原问题的解法使用解决大数据问题的一种常规方法:把大文件通过哈希函数分配到机器, 或者通过哈希函数把大文件拆成小文件,一直进行这种划分,直到划分的结果满足资源限制的要求。

  • 首先,你要向面试官询问在资源上的限制有哪些,包括内存、计算时间等要求。

  • 在明确了限制要求之后,可以将每条 URL 通过哈希函数分配到若干台机器或者拆分成若干个小文件, 这里的“若干”由具体的资源限制来计算出精确的数量。

例如,将 100 亿字节的大文件通过哈希函数分配到 100 台机器上,然后每一台机器分别统计分给自己的 URL 中是否有重复的 URL,同时哈希函数的性质决定了同一条 URL 不可能分给不同的机器;或者在单机上将大文件通过哈希函数拆成 1000 个小文件,对每一个小文件再利用哈希表遍历,找出重复的 URL;还可以在分给机器或拆完文件之后进行排序,排序过后再看是否有重复的 URL 出现。总之,牢记一点,很多大数据问题都离不开分流,要么是用哈希函数把大文件的内容分配给不同的机器,要么是用哈希函数把大文件拆成小文件,然后处理每一个小数量的集合。

补充问题:

最开始还是用哈希分流的思路来处理,把包含百亿数据量的词汇文件分流到不同的机器上,具体多少台机器由面试官规定或者由更多的限制来决定。

对每一台机器来说,如果分到的数据量依然很大,比如,内存不够或存在其他问题:

  1. 可以再用哈希函数把每台机器的分流文件拆成更小的文件处理。
  2. 处理每一个小文件的时候,通过哈希表统计每种词及其词频,哈希表记录建立完成后,再遍历哈希表
  3. 遍历哈希表的过程中使用大小为 100 的小根堆来选出每一个小文件的 Top 100(整体未排序的 Top 100)。每一个小文件都有自己词频的小根堆(整体未排序的 Top 100),将小根堆里的词按照词频排序,就得到了每个小文件的排序后 Top 100。
  4. 然后把各个小文件排序后的 Top 100 进行外排序或者继续利用小根堆,就可以选出每台机器上的 Top100。
  5. 不同机器之间的 Top 100 再进行外排序或者继续利用小根堆,最终求出整个百亿数据量中的 Top 100。
  6. 对于 Top K 的问题,除用哈希函数分流和用哈希表做词频统计之外,还经常用堆结构和外排序的手段进行处理。

4 40 亿个非负整数中找到出现两次的数

题目要求:32 位无符号整数的范围是 0~4 294 967 295,现在有 40 亿个无符号整数,可以使用最多 1GB的内存,找出所有出现了两次的数。

本题可以看做第一题的进阶问题,这里将出现次数限制在了两次。

  1. 首先,可以用 bit map 的方式来表示数出现的情况。
  2. 具体地说,是申请一个长度为4 294 967 295x2 的bit 类型的数组bitArr,用 2 个位置表示一个数出现的词频,1B 占用 8 个bit, 所以长度为 4 294 967 295x2 的 bit 类型的数组占用 1GB 空间。怎么使用这个 bitArr 数组呢?
  3. 遍历这 40 亿个无符号数,
    1. 如果初次遇到 num,就把bitArr[num * 2 + 1]和 bitArr[num * 2]设置为 01,
    2. 如果第二次遇到 num,就把bitArr[num * 2+1]和bitArr[num * 2]设置为 10,
    3. 如果第三次遇到 num, 就把bitArr[num * 2+1]和bitArr[num * 2]设置为 11。
    4. 以后再遇到 num,发现此时 bitArr[num * 2+1]和 bitArr[num * 2]已经被设置为 11,就不再做任何设置。
  4. 遍历完成后,再依次遍历 bitArr,如果发现bitArr[i * 2+1]和bitArr[i * 2]设置为 10,那么 i 就是出现了两次的数。

你可能感兴趣的:(数据结构,算法,算法,java,数据结构,笔记)