likes
comments
collection
share

海量数据处理 | 最高频 K 项问题

作者站长头像
站长
· 阅读数 24

引言

最高频 K 项问题是最常见的一类海量数据面试题,问题的形式通常是找到一个大文件或者数据流中出现频率最高的 K 项。对于海量数据类问题,合理利用内存资源和计算资源通常是问题的难点所在,对于最高频 K 项问题,需要考虑的必要条件有两个

  • 是否需要精确的 Top K 结果
  • 数据是离线还是在线的

本文将从最简单的数组最大 K 项问题入手,分别讨论离线在线算法的区别,然后延伸到最高频 K 项并给出一些常见问题的解法。

铺垫:整数数组最大 K 项

整数数组最大 K 项可以使用离线算法和在线算法两种方式解决。

离线算法

使用快速选择算法(Quick Select),快速选择是一种基于快速排序的算法,可以在 O(N) 的时间内找到数组的第K大的数,然后再扫描一次整个数组就可以得到前K大的数。

快速选择算法的大致过程如下:

  1. 选择一个枢轴:从数组中随机选择一个元素作为枢轴。

  2. 分割数组:重排数组,使得所有大于枢轴的元素都位于它的左边,所有小于或等于枢轴的元素位于它的右边。

  3. 递归搜索

    • 如果枢轴的位置恰好是第K大的位置,那么它左边的所有元素就是前K大的元素。
    • 如果枢轴的位置大于K,说明所求的前K大元素都在枢轴的左边,对左边的数组递归执行这一过程。
    • 如果枢轴的位置小于K,说明前K大元素包含了枢轴和它左边的所有元素,还需要在右边的数组中寻找更多的大元素,对右边的数组递归执行这一过程。

使用 C++ 实现的代码如下:

int partition(std::vector<int>& arr, int left, int right) {
    // 选取最右侧的元素作为枢轴
    int pivot = arr[right];
    int i = left;
    for (int j = left; j < right; j++) {
        // 将小于等于枢轴的元素移动到左边
        if (arr[j] <= pivot) {
            std::swap(arr[i], arr[j]);
            i++;
        }
    }
    std::swap(arr[i], arr[right]);
    return i;
}

int quickSelect(std::vector<int>& arr, int left, int right, int K) {
    // 基本边界条件,当数组中只有一个元素时
    if (left == right) {
        return arr[left];
    }

    int pivotIndex = partition(arr, left, right);
    
    // 根据枢轴的位置,决定下一步是左边还是右边
    if (K == pivotIndex) {
        return arr[pivotIndex];
    } else if (K < pivotIndex) {
        return quickSelect(arr, left, pivotIndex - 1, K);
    } else {
        return quickSelect(arr, pivotIndex + 1, right, K);
    }
}

在线算法

离线算法中,需要扫描数组两遍,第一次求得第 K 大,第二遍求得前 K 大。如果数据以数据流的形式给出,只能遍历一次,这种算法即为在线算法。这个问题中,可以使用数据结构堆来解决。

堆(Heap) 是一种特别的完全二叉树,在堆结构中,树的每个父节点都满足特定的顺序关系(堆性质)与其子节点:

  • 在最大堆(max-heap)中,每个父节点的值都大于或等于其子节点的值;
  • 在最小堆(min-heap)中,每个父节点的值都小于或等于其子节点的值。

因此 支持 O(logN) 的插入,O(1) 的求最大值/最小值,O(logN)删除最值(pop)

最直观的想法是使用最大堆,在每次 Top K 操作时,就 pop 堆 K 次。但是当 K 远小于 N 时,必须维护所有整数在堆中,在插入新元素时的时间复杂度不是最优的。

在这个问题场景中,不需要删除操作,数据只增不减,因此维护那些已经低于 K 名的数据是不必要的。可以使用最小堆来维护前 K 名,当插入时判断是否比当前第 K 名大。这样便优化了时间和空间的复杂度。

最高频 K 项的离线算法

基于最大 K 项的离线算法,最直接的做法是哈希表加最小堆。

  1. 用哈希表统计所有项的出现次数
  2. 循环每一个出现过的项,用最大K项的方法,获得最大的前K项。

第一步统计所有单词的出现次数,需要 O(N) 的空间和 O(N) 的时间。第二步需要 O(K) 的空间和 O(NlogK) 的时间。总的时间耗费是 O(N log K),空间耗费是 O(N)。

分布式提速

当面对海量数据时,即使仅仅全部扫描一遍也会消耗大量的时间,面对理论最优的时间复杂度 O(N log K),想要提速只能利用分布式计算资源。

Map Reduce 是谷歌分布式三驾马车(Map Reduce、Big Table 和 Google File System)中用于进行分布式计算的。

MapReduce 主要由两个阶段组成:Map 阶段和 Reduce 阶段,两者之间通常还有一个 Shuffle 阶段:

  1. Map 阶段

    • 输入:输入数据通常是键值对形式的数据集。
    • 处理:Map 函数处理输入数据,每个 Map 函数运行在输入数据的一个子集上。它处理数据并产生中间键值对。
    • 输出:输出是中间键值对,这些键值对根据键进行排序,并准备传递给 Reduce 阶段。
  2. Shuffle 阶段

    • 在 Map 和 Reduce 之间,系统自动进行 Shuffle 操作。
    • Shuffle 过程负责将所有 Map 阶段输出的数据根据键进行排序(如果尚未排序)和分组,确保相同键的所有值都传递到同一个 Reduce 函数。
  3. Reduce 阶段

    • 输入:输入是按键分组的中间数据。
    • 处理:每个 Reduce 函数接收一个键和与该键相关的值集合,然后对这些值进行合并或归纳以形成一个较小的值集(通常是单个值或一个较小的值集)。
    • 输出:输出是经过处理的最终结果,例如键和它的累计结果。

最终,每个 Reducer 都会返回自己数据的 Top K,只需要从多个 Top K 中过一遍就可以得到全体数据的 Top K

处理内存问题

在之前的算法中,空间复杂度始终为 O(N)。也就意味着需要将所有数据加载进内存中,即使使用了分布式计算,也存在无法全部加载的可能。

针对这个问题,可以使用哈希算法解决。利用哈希函数对于同一个 key 会返回一个固定的,无规律的整数值的特点,可以计算每个数据的哈希值,然后对哈希值取模从而将数据分成若干份(取决于取模的数字)。对于每份数据再分别导入内存处理。

最高频 K 项的在线算法

在在线场景中,没有第二次从头访问数据的机会。最直观的想法是对哈希表和堆封装在一起,一边计数一边比较 Top K。

在标准在线算法中,空间复杂度与数据流中流过的数据大小总和有关。如果希望节约空间,则必须牺牲掉一部分精确性,如 Top K 内的排名颠倒或 排名 K+1 的数据误入等。如果对精确性要求较低,则可以选择用精度换空间,如 HyperLogLogMisra-Gries摘要算法 等。

问题实例

最近7天的热门歌曲

问题描述:设计一个听歌统计系统,返回用户 7 天内听的最多 10 首的歌

方案一:离线算法

使用系统日志定时进行统计,统计时使用标准的离线算法:哈希表加最小堆。可以使用分布式计算和哈希算法优化时间和空间。

这种方案的缺点是实时性较差,即榜单能否及时反映出热点数据,因为具体的统计过程很难在较短的时间内完成(如5分钟),如果实时性变高则难以完成。

方案二:在线算法

此问题与基础 Top K 问题的区别在于有时间窗口限制,要及时丢弃7天前的数据,同时7天内跌出前10名的数据有可能重新回到前10名。

针对问题的特点和离线算法的缺点,可以使用基于桶的统计方法。

  • 聚合(Aggregate) :将用户的同个记录,按照1小时为单位进行一次聚合(Aggregate),即整合成一个 Hash 表,Key是歌曲的id,Value是歌曲在这1小时之内被播放的次数,每个小时内的数据就是一个桶(Bucket)

  • 滑动窗口(Sliding Window) :7天的话,只需要在内存中保存 7 * 24 = 144 个桶,随着时间轴的推移,旧的桶则可以被删除。每次需要获得 Top 10 的时候,则将这 144 个桶的结果进行合并即可。

这种方法的优点在于,对实时性的适应度变高了,根据实时性的要求调整桶的间隔即可。

相关问题

  1. 对内存要求高,无法将桶全部加载进内存怎么办? 允许一定结果误差,将每个桶局部统计中最小的部分删除

  2. 具体在什么位置存储桶? 同时存储于内存和硬盘中,如果因为宕机丢失数据可以利用硬盘数据恢复

  3. 每次用户查询都会进行统计吗? 不会,定期完成统计后储存在数据库和缓存中供用户查询

  4. 如果实时性要求非常高,需要精确到秒为单位,如何快速获得最近7天的Top10? 必须利用大内存在哈希表和堆中保存全部数据

转载自:https://juejin.cn/post/7372734627163979786
评论
请登录