面试常见海量数据场景题

  • https://github.com/weitingyuk/LeetCode-Notes-Waiting/blob/main/2021-02-17/TopK.md
  • https://segmentfault.com/a/1190000021109127

个人整理,有一些也是个人理解,并不存在标准答案

一、海量数据词频topk、去重问题

1. 前置知识

1、Trie树

Trie树,即字典树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较。

Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。

前缀树的3个基本性质:

  1. 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
  2. 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
  3. 每个节点的所有子节点包含的字符都不相同。
  4. 如果某些分支比较稀疏则可以折叠(表示多字符前缀)

面试常见海量数据场景题_第1张图片

2、Hash分区

https://iswade.github.io/articles/partition/

哈希分区是最常见的数据分区方式,通过按照数据的key、或者用户指定的一个或者多个字段计算哈希,然后将计算后的哈希与计算节点进行映射,从而将不同哈希值的数据分布到不同节点上。

例如,有3条记录(key1, value1), (key2, value2), (key3, value3) 通过对键进行计算哈希(对 key 进行 md5哈希,然后取前两个字符作为哈希值),哈希桶个数255个,当前有两个分区,偶数哈希值放到分区0,奇数哈希值放到分区1,计算后的数据分布如下图所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2CL3lSVq-1645429748408)(http://xwjpics.gumptlu.work/qinniu_uPic/partition_hash.svg)]

哈希分区需要仔细选取哈希函数,如果用户可以指定哈希字段时,则要考虑字段经过哈希之后散列性是否满足需求。如果选取的哈希函数散列性较好,则可以将数据大致均匀地分布到几个分区中。

哈希分区的一个优点是,保存的元数据很简单,只需要保存桶与分区的映射关系即可。但是缺点也很明显,可扩展性差,如果增加一个节点进行扩容,则需要对所有数据进行重新计算哈希,然后对数据进行重新分布,对于均匀分布的哈希函数而言,一般而言,在扩容时每个分区都需要扩容,通过成倍增加节点,然后通过调整映射关系,重新分布一半的数据到新分区。另一个缺点是,如果哈希函数选择不合理,则很容易出现数据倾斜,导致某个分区数据量很大。

3、位图

位图,就是用一个或多个 bit 来标记某个元素对应的值,而键就是该元素。采用位作为单位来存储数据,可以大大节省存储空间。

位图通过使用位数组来表示某些元素是否存在。它可以用于快速查找,判重,排序等。不是很清楚?我先举个小例子。

假设我们要对 [0,7] 中的 5 个元素 (6, 4, 2, 1, 5) 进行排序,可以采用位图法。0~7 范围总共有 8 个数,只需要 8bit,即 1 个字节。首先将每个位都置 0:

0 0 0 0 0 0 0 0

然后遍历 5 个元素,首先遇到 6,那么将下标为 6 的位的 0 置为 1;接着遇到 4,把下标为 4 的位 的 0 置为 1:

0 0 0 0 1 0 1 0

依次遍历,结束后,位数组是这样的:

0 1 1 0 1 1 1 0

每个为 1 的位,它的下标都表示了一个数:

for i in range(8):
    if bits[i] == 1:
        print(i)

这样我们其实就已经实现了排序。(类似于计数排序,只不过计数排序是一个数组映射,数组的元素代表出现的次数)

对于整数相关的算法的求解,位图法是一种非常实用的算法。假设 int 整数占用 4B,即 32bit,那么我们可以表示的整数的个数为 232。

使用位图的特点:

  • 整数,并给出了整数的范围
  • 统计次数,去重

4、布隆过滤器

https://zhuanlan.zhihu.com/p/43263751

类似于位图单不限于整数,本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”

相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。

布隆过滤器数据结构

布隆过滤器是一个 bit 向量或者说 bit 数组,长这样:

面试常见海量数据场景题_第2张图片

如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成**多个哈希值,**并对每个生成的哈希值指向的 bit 位置 1,例如针对值 “baidu” 和三个不同的哈希函数分别生成了哈希值 1、4、7,则上图转变为:

面试常见海量数据场景题_第3张图片

Ok,我们现在再存一个值 “tencent”,如果哈希函数返回 3、4、8 的话,图继续变为:

面试常见海量数据场景题_第4张图片

值得注意的是,4 这个 bit 位由于两个值的哈希函数都返回了这个 bit 位,因此它被覆盖了。现在我们如果想查询 “dianping” 这个值是否存在,哈希函数返回了 1、5、8三个值,结果我们发现 5 这个 bit 位上的值为 0,说明没有任何一个值映射到这个 bit 位上,因此我们可以很确定地说 “dianping” 这个值不存在。而当我们需要查询 “baidu” 这个值是否存在的话,那么哈希函数必然会返回 1、4、7,然后我们检查发现这三个 bit 位上的值均为 1,那么我们可以说 “baidu” 存在了么?答案是不可以,只能是 “baidu” 这个值可能存在。

这是为什么呢?答案跟简单,因为随着增加的值越来越多,被置为 1 的 bit 位也会越来越多,这样某个值 “taobao” 即使没有被存储过,但是万一哈希函数返回的三个 bit 位都被其他值置位了 1 ,那么程序还是会判断 “taobao” 这个值存在。

3. top K常用的方法

  • 快排+选择排序:排序后的集合中进行查找
    • 时间复杂度: 时间复杂度为O(NlogN)
    • 缺点:需要比较大的内存,且效率低
  • 局部淘汰:取前K个元素并排序,然后依次扫描剩余的元素,插入到排好序的序列中(二分查找),并淘汰最小值。
    • 时间复杂度: 时间复杂度为O(NlogK) (logK为二分查找的复杂度)。
  • 分治法:将10亿个数据分成1000份,每份100万个数据,找到每份数据中最大的K个,最后在剩下的1000*K个数据里面找出最大的K个,100万个数据里面查找最大的K个数据可以使用Partition的方法
    • 时间复杂度: 时间复杂度为O(N+1000*K)
  • Hash法: 如果这10亿个数里面有很多重复的数,先通过Hash法,把这10亿个数字去重复,这样如果重复率很高的话,会减少很大的内存用量,从而缩小运算空间,然后通过分治法或最小堆法查找最大的K个数。
  • 小顶堆: 首先读入前K个数来创建大小为K的小顶堆,建堆的时间复杂度为O(K),然后遍历后续的数字,并于堆顶(最小)数字进行比较。如果比最小的数小,则继续读取后续数字;如果比堆顶数字大,则替换堆顶元素并重新调整堆为最小堆。
    • 时间复杂度: 时间复杂度为O(NlogK)
  • Trie树: 如果是从10亿个重复比较多的单词找高频词汇,数据集按照Hash分区方法分解成多个小数据集,然后使用Trie树统计每个小数据集中的query词频,之后用小顶堆求出每个数据集中出现频率最高的前K个数,最后在所有top K中求出最终的top K。
    • 适用范围:数据量大,重复多,但是数据种类小可以放入内存
    • 时间复杂度:O(Len*N),N为字符串的个数,Len为字符串长度
  • 桶排序:一个数据表分割成许多buckets,然后每个bucket各自排序,或用不同的排序算法,或者递归的使用bucket sort算法。也是典型的divide-and-conquer分而治之的策略。
    • 使用范围:如果已知了数据的范围,那么可以划分合适大小的桶,直接借用桶排序的思路
    • 时间复杂度:O(N*logM),N 为待排序的元素的个数,M为桶的个数
  • 计数排序:计数排序其实是桶排序的一种特殊情况。当要排序的 n 个数据,所处的范围并不大的时候,比如最大值是 k,我们就可以把数据划分成 k 个桶。每个桶内的数据值都是相同的,省掉了桶内排序的时间。
    • 适用范围:只能用在数据范围不大的场景
    • 时间复杂度:O(N)
  • 基数排序:将整数按位数切割成不同的数字,然后按每个位数分别比较。
    • 适用范围:可以对字符串类型的关键字进行排序。
    • 时间复杂度: O(N*M),M为要排序的数据的位数

4. 实际情况

1、单机+单核+足够大内存

  • 顺序遍历(或先用HashMap求出每个词出现的频率)
    • 查找10亿个查询(每个占8B)中出现频率最高的10个,考虑到每个查询词占8B,则10亿个查询次所需的内存大约是10^9 * 8B=8GB内存。如果有这么大内存,直接在内存中对查询次进行排序,顺序遍历找出10个出现频率最大的即可。
    • 优点: 简单快速

2、单机+多核+足够大内存

  • partition + 方法1
    • 直接在内存总使用Hash方法将数据划分成n个partition,每个partition交给一个线程处理,线程的处理逻辑同1类似,最后一个线程将结果归并。
    • 瓶颈:数据倾斜。每个线程的处理速度可能不同,快的线程需要等待慢的线程。
    • 解决的方法:将数据划分成c×n个partition(c>1),每个线程处理完当前partition后主动取下一个partition继续处理,知道所有数据处理完毕,最后由一个线程进行归并。

3、单机+单核+受限内存

  • 分治 + (1)
    • 将原文件中的数据切割成M小文件,如果小文件仍大于内存大小,继续采用Hash的方法对数据文件进行分割,直到每个小文件小于内存大小,这样每个文件可放到内存中处理。采用(1)的方法依次处理每个小文件。

4、多机+受限内存

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

5. 常见具体问题

1、10亿个数中如何高效地找到最大的一个数

将10亿个数据分成1000份,每份100万个数据,找到每份数据中最大的那个数据,最后在剩下的1000个数据里面找出最大的数据。 从100万个数据遍历选择最大的数,此方法需要每次的内存空间为10^6*4=4MB,一共需要1000次这样的比较。(每100万个的topK可以用多协程并发计算)

2、10亿个数中如何高效地找到第K个数

  • 对于top K类问题,通常比较好的方案是分治+hash+小顶堆:

    • 先将数据集按照Hash分区方法分解成多个小数据集

      • Hash(数据)%1000 先对数据Hash再取固定几位作为映射到不同的桶B0,B1,B2,…B999中
      • 分解小问题,因为内存限制可能无法一次性将所有数据加载到内存
    • 然后用小顶堆求出每个数据集中最大的K个数/前K个最大的数(内存允许可以并发多个一起)

      • 每个小堆前K个最大的数一定包含最终的topK,因为原始数据中前K个大的数分到各个小数据集中一定还是在前K个大中
      • 取前K个最大的数就是取大小为K的堆所有元素
    • 最后在所有小堆的所有筛选出的数中,top K中求出最终的top K。

      • 取堆顶即可
  • 如果是top词频可以使用分治+ Trie树/hash +小顶堆:

    • 先将数据集按照Hash分区方法分解成多个小数据集

    • 然后使用Trie树或者Hash统计每个小数据集中的query词频(内存允许可以并发多个一起)

      • Trie树节点记录字母以及当前路径表示字符串的出现次数
    • 之后用小顶堆求出每个数据集中出频率最高的前K个数

      • 遍历Trie树或Hash表
    • 最后在所有top K中求出最终的top K。

  • 时间复杂度:建堆时间复杂度是O(K), 算法的时间复杂度为O(NlogK)

3、有十亿个无符号整型,怎么最快速查出我输入的这个数在其中的最近某个范围

例如int{1,4,5,6,9}, 输入的数是7,则结果为6,9

位图(数据大小范围已知),将10亿个数映射到位图中,然后将读取的数同样映射然后分别从左右两边开始寻找范围的数,最先找到的就是结果

4、在 2.5 亿个整数中找出不重复的整数。注意:内存不足以容纳这 2.5 亿个整数。

位图法

  • 构建两个bitmap,表达如下:

    • 00表示这个数没出现过
    • 01表示这个数出现过一次 (题目需要找的答案)
    • 10表示这个数出现过多次
  • 遍历 2.5 亿个整数,查看位图中对应的位,如果是 00,则变为 01,如果是 01 则变为 10,如果是 10 则保持不变。遍历结束后,查看位图,把对应位是 01 的整数输出即可。

那么这 2 32 2^{32} 232 个整数,总共所需内存为 2 32 2^{32} 232*2b=1GB。因此,当可用内存超过 1GB 时,可以采用位图法

5、如何确定40亿个数中是否存在某数M

给定 40 亿个不重复的没排过序的 unsigned int 型整数,然后再给定一个数,如何快速判断这个数是否在这 40 亿个整数当中?

位图,40 亿个不重复整数,我们用 40 亿个 bit 来表示,初始位均为 0,那么总共需要内存:4,000,000,000b≈512M。

我们读取这 40 亿个整数,将对应的 bit 设置为 1。接着读取要查询的数,查看相应位是否为 1,如果为 1 表示存在,如果为 0 表示不存在。

6、如何统计不同电话号码的个数?

已知某个文件内包含一些电话号码,每个号码为 8 位数字,统计不同号码的个数。

这道题本质还是求解数据重复的问题,对于这类问题,一般首先考虑位图法。

对于本题,8 位电话号码可以表示的号码个数为 108 个,即 1 亿个。我们每个号码用一个 bit 来表示,则总共需要 1 亿个 bit,内存占用约 100M。

思路如下

申请一个位图数组,长度为 1 亿,初始化为 0。然后遍历所有电话号码,把号码对应的位图中的位置置为 1。遍历完成后,如果 bit 为 1,则表示这个电话号码在文件中存在,否则不存在。bit 值为 1 的数量即为 不同电话号码的个数。

整数数据 不同的个数,相同的个数,都可以考虑位图

7、有10亿个无符号整数(unsigned int),2G内存,求中位数

中位数如果排序后就可以转换为:

  • 求长度为N的有序奇数数组的第N/2位数字
  • 求长度为N的有序偶数数组的第(N/2)-1与第N/2两个数的平均

如果没有内存限制则做法是:直接通过排序算法得到对应位置的数

如果有内存限制的做法:

双堆法:

  • 数据量较小的情况
  • 维护两个堆,一个大顶堆,一个小顶堆。大顶堆中最大的数小于等于小顶堆中最小的数;保证这两个堆中的元素个数的差不超过 1。
  • 若数据总数为偶数,当这两个堆建好之后,中位数就是这两个堆顶元素的平均值。当数据总数为奇数时,根据两个堆的大小,中位数一定在数据多的堆的堆顶

分治法:

  • 顺序读取5亿个数,然后判断读取到的数字的最高位为1还是0,为1九划分到f1集合中,为0就划分到f0集合中
  • f1一定大于f0
  • 判断两个子集合的数据个数与原本总数(5亿)的一半的关系,如果大于则中位数在f1集合,小于则在f0集合
    • 如果子集合个数相同,那么就是f1的最小值与f0的最大值的平均值
  • 例如是f0集合,对于f0集合重复上面过程(用次高位划分)直到文件可以被直接加载到内存中
  • 最后排序获得对应位置的中位数

8、如何查询最热门的查询串?如何找出某一天访问百度网站最多的 IP?如何从大量数据中找出高频词?

  • 内存不够用就先Hash分区为小数据集
  • 每个小数据集再通过HashMap/Trie树统计查询次数,并遍历找出最大,如果是topK就找前K个最大
  • 找出所有小数据集中的结果最大值/topK就是结果
  • (注意:topK就用堆实现)

9、给定 a、b 两个文件,各存放 50 亿个 URL,每个 URL 各占 64B,内存限制是 4G。请找出 a、b 两个文件共同的 URL。

类似的题:两个 10G 大小包含 URL 数据的文件,最多使用 1G 内存,将这两个文件合并,并找到相同的 URL

  • 对于A文件的每一条URL数据首先经过Hash(url)%1000,得到a0, a1, a2, …, a999个小数据集;同理对B文件也是,得到b0, b1, b2, …, b999个小数据集;

    作用:

    1. 大数据集的Hash分区
    2. A,B文件中相同URL的数据经过上述Hash算法一定会映射到同一个桶中即 a i a_i ai b i b_i bi大部分URL是相同的,小部分可能因为Hash碰撞不同。
  • 然后对于每对 a i a_i ai b i b_i bi,对 a i a_i ai的每条URL构建HashMap,然后判断 b i b_i bi中是否存在,存在的保留到结果 c i c_i ci

  • 最后合并所有结果 c i c_i ci得到文件C

10、有 20 个数组,每个数组有 500 个元素,并且有序排列。如何在这 20*500 个数中找出前 500 的数?

对于 TopK 问题,最常用的方法是使用堆排序。对本题而言,假设数组降序排列,可以采用以下方法:

  • 首先建立大顶堆,堆的大小为数组的个数,即为 20,把每个数组最大的值存到堆中
  • 接着删除堆顶元素,保存到另一个大小为 500 的数组中,然后向大顶堆插入删除的元素所在数组的下一个元素
  • 重复上面的步骤,直到删除完第 500 个元素,也即找出了最大的前 500 个数。

(与合并k个链表思路相同)

11、两个文件包含无序的数字,数字的大小范围是0-500w左右。如何求两个文件中的重复的数据?

位图,需要内存如下:1M不到

面试常见海量数据场景题_第5张图片

  • 扫描第一个文件,对第一个文件构建位图
  • 扫描第二个文件的整数,一旦映射到位图中的位置为1则说明重复,保存到结果

12、1000 台机器,每台机器 1000 个文件,每个文件存储了 10 亿个整数,如何找到其中最小的 1000 个值?

如果数据范围已知,则可以使用位图

如果数据范围未知,则可以使用topK(Hash分区=>小文件取前topK个数=>topK)

13、十亿的日志,无序的日期,如何按降序排,你是用什么数据结构来做的,你要怎么实现

因为日志值的数字范围时有限的,首先预处理成一样的长度缺少的最前面补充0,然后用基数排序比较好,对于每一位可以用计数排序,从低位向高位来排序,时间复杂度为O(N*K)其中K为日志日期字符串大小

你可能感兴趣的:(面试,面试)