外部排序与大规模数据处理(MapReduce/BSP)

共 21 题
📑 题目列表 21 题
#
★★★

1. k 路归并如何用最小堆在每轮选全局最小维持 O(n log k)

k 路归并如何用最小堆在每轮选全局最小,从而维持 O(n log k) 的时间复杂度?

  • 最小堆维护 k 个候选
  • 每轮取堆顶 O(log k)
  • 总复杂度 O(n log k)

k 路归并把 k 个已排序序列(归并段)合并成一个有序序列。用最小堆存放每个序列当前的最小元素(共 k 个),每轮:弹出堆顶(全局最小)写入输出,再从该序列取下一个元素入堆,重复直到全部取完。每轮取堆顶和插入都是 O(log k),共 n 个元素,总复杂度 O(n log k)。相比两两归并 O(n log k),k 路归并减少归并趟数。

最小堆维护 k 个"当前最小",从堆顶取全局最小,从来源序列取下一个补入,保证有序。每次操作 O(log k),n 个元素总 O(n log k)。k 路归并是外部排序多路归并的核心,降低 I/O 趟数。

import java.util.*;
// k 路归并:list 为 k 个已排序序列,数组元素为 [值, 序列号]
int[] kWayMerge(List<int[]> lists) {
    PriorityQueue<int[]> pq = new PriorityQueue<>((a,b) -> a[0]-b[0]);
    int[] idx = new int[lists.size()];
    for (int i = 0; i < lists.size(); i++) if (lists.get(i).length > 0)
        pq.offer(new int[]{lists.get(i)[0], i});
    List<Integer> res = new ArrayList<>();
    while (!pq.isEmpty()) {
        int[] cur = pq.poll();
        res.add(cur[0]);
        int i = cur[1];
        if (++idx[i] < lists.get(i).length)
            pq.offer(new int[]{lists.get(i)[idx[i]], i});
    }
    return res.stream().mapToInt(x->x).toArray();
}
#
★★★

2. 如何用外部排序思路做超大数据的中位数/Top K

如何用外部排序思路在数据超出内存时求超大数据集的中位数或 Top K?

  • 分块 + 外部归并
  • 数值范围分桶
  • Top K 的堆/分桶

超大数据求中位数:若内存不足,先对数据分块排序并用外部归并得到有序流,再统计元素个数定位中位数;或按数值范围分桶(分段统计),逐桶确定中位数所在桶后,只在桶内精确排序。Top K:用规模为 K 的最小堆流式扫描,或分桶 + 每桶计数定位,再在目标桶内选 K。外部排序思路是"分块进内存 + 部分在内存处理 + 归并/定位",避免一次加载全部。

核心是"内存放不下时,用分块与分桶把问题缩小到内存可处理"。中位数用桶计数定位区间,Top K 用堆或按桶选择。外部排序保证了数据有序流,配合定位算法即可处理超大数据。复杂度以磁盘 I/O 为主。

#
★★★

3. 替换选择(replacement selection)中如何用败者树在内存中生成接近两倍内存长度的初始归并段,减少归并趟数

替换选择(replacement selection)如何用败者树在内存中生成接近两倍内存长度的初始归并段,从而减少归并趟数?

  • 替换选择思想
  • 败者树/堆
  • 近似两倍内存的归并段

替换选择(replacement selection)是"自然归并"的加强:内存中维护一个淘汰树(堆/败者树),初始放入 M 个元素。反复:输出当前最小元素到当前归并段,同时读入一个新元素,若新元素 ≥ 刚输出的元素,则加入当前归并段;否则放入"下一段"候选。这样一个归并段可包含内存容量 M 的两倍左右元素(因为不断用更大的元素替换已输出的最小元素),从而初始归并段更长、趟数更少。败者树加速每次取最小。

普通 2-路/多路归并的初始归并段长度 = 内存大小 M;替换选择让已输出的元素被"更大的元素"替代,输出序列保持递增,可产生平均约 2M 长度的归并段。归并段变长 → 段数变少 → 归并趟数减少,减少磁盘 I/O。败者树让取最小 O(log M)。

#
★★

4. 为何内存不足以装下数据时不能简单用内部快排

为什么内存不足以装下数据时不能简单使用内部快排?

  • 内部快排需要全量入内存
  • 内存不足
  • 磁盘 I/O 代价

内部快排(如快速排序)需要将全部数据加载到内存中操作,当数据总量超过可用内存时无法一次性载入,因此不能直接使用内部快排。此时需用外部排序:分块读入内存排序(生成归并段)→ 写回磁盘 → 多路归并,只在内存中处理部分数据。外部排序把磁盘作为辅助存储,通过控制 I/O 分块与归并处理超大数据。

内部排序假设数据全在内存(随机访问 RAM),超大数据时内存溢出。外部排序用"分块 + 归并"把内存需求降到 O(内存),通过磁盘顺序读写处理全量。核心差异是内存假设与 I/O 管理。

#
★★

5. 分布式排序(如 Hadoop Terasort)如何做分区+局部+归并

分布式排序(如 Hadoop Terasort)如何通过分区、局部排序与归并完成?

  • 分区策略
  • 局部排序
  • 全局归并

分布式排序(如 Terasort)分三步:一是分区(partition),把数据按 key 的范围划分到不同 reducer,保证每个 reducer 处理一个有序区间;二是局部排序,每个 reducer 对自己分到的数据内部排序;三是归并/拼接,各 reducer 输出按分区顺序拼接,即为全局有序。分区使每台机器只负责一部分区间,避免全局比较,通过采样确定分区边界(如 Terasort 用采样估计 key 分布)。

关键是"分区决定全局顺序":通过采样把 key 空间分成有序区间,每分区只排自己的数据,最后拼接。局部排序 + 分区 + 归并把全局排序拆成可并行子任务,避免跨机器比较。Terasort 用采样优化分区边界,使各分区数据量均衡。

#
★★

6. 外排序的 I/O 复杂度以"磁盘页传送次数"而非比较次数计

为什么外部排序的 I/O 复杂度以"磁盘页传送次数"而非比较次数来计量?

  • 磁盘 I/O 昂贵
  • 页传送次数
  • 比较在内存中

外部排序中,磁盘 I/O(页传送)比内存比较慢几个数量级,是性能瓶颈,因此用"磁盘页传送次数"衡量复杂度,而非比较次数(比较在内存中,代价可忽略)。典型外排序 I/O 复杂度:对 n 个元素、内存 M、页大小 B,排序生成归并段需 O((n/B) log_M(n/B)) 次页传送。分析关注的是读/写磁盘的页数,减少趟数即减少 I/O。

内存中比较 CPU 时间相比磁盘 I/O 微不足道,故评估外排序以 I/O 为主。I/O 复杂度刻画"读写的页数",与页大小、内存容量、归并路数相关。优化目标(减少趟数、顺序 I/O)本质是减少页传送次数。

#
★★

7. 外部排序(External Sort)为何要"分块排序+多路归并"

外部排序(External Sort)为什么要"分块排序 + 多路归并"?

  • 内存限制
  • 分块排序
  • 多路归并

外部排序数据超内存,无法一次全部排序,故把数据分块,每块读入内存排序后写回磁盘,生成多个有序归并段;再用多路归并把这些归并段合并成一个完整有序序列。分块排序利用有限内存生成有序段,多路归并把多个有序段合并,减少归并趟数、降低磁盘 I/O。若只用两路归并,段数多时趟数多、I/O 大;多路归并(k 路)一次合并 k 段,减少趟数。

"分块 + 归并"是外部排序的基本框架:分块解决内存限制,归并解决有序段合并。路数 k 越大,趟数越少(log_k 段数),但需更多内存缓冲。多路归并用堆/败者树选最小,平衡 I/O 与内存。

#
★★

8. 归并时如何利用"归并段已有序"做顺序 I/O 而非随机

归并时如何利用"归并段已有序"的性质做顺序 I/O 而非随机 I/O?

  • 归并段有序性
  • 顺序读取
  • 顺序 I/O 优势

归并段已有序,因此归并时只需顺序读取每个归并段:从段起点沿顺序向后读,记录当前最小元素,用完后继续读下一个。顺序 I/O 比随机 I/O 快得多(磁盘顺序读无需寻道/旋转,SSD 顺序写也快)。通过为每个归并段分配缓冲、顺序预读,避免在段内随机跳转读取,从而把归并的 I/O 从随机变为顺序。

关键利用"段内有序":归并只取各段当前最小,天然按顺序消费。用缓冲 + 顺序预读,让磁盘顺序传送大批页,减少寻道。这是外部排序优化 I/O 的核心技巧。

#
★★

9. 当 k 很大时如何用"赢者树/败者树"降低归并比较次数

当 k 很大时,如何用"赢者树/败者树"降低归并比较次数?

  • 赢者树/败者树
  • 每次取最小 O(log k)
  • 减少比较与 I/O

k 路归并时,若用朴素线性扫描 k 个候选找最小,每次 O(k),n 个元素 O(nk)。用赢者树或败者树(完全二叉树,内部节点记录胜者/败者),每次取最小只需 O(log k) 比较,且更新(替换来源序列下一个元素)只需 O(log k)。当 k 很大(归并路数多)时,这显著降低比较开销,同时败者树比堆更利于缓冲与 I/O 流水。

赢者树/败者树把"找最小"从 O(k) 降到 O(log k),支持大路数归并。败者树在兄弟比较时更简单(败者上浮),适合外部归并的缓冲管理。k 大时这是关键优化。

#
★★

10. 双缓冲与异步预读中归并时如何让磁盘读写与比较计算重叠,减少等待 I/O 的停顿

归并时如何用双缓冲与异步预读让磁盘读写与比较计算重叠,减少等待 I/O 的停顿?

  • 双缓冲
  • 异步预读
  • 计算与 I/O 重叠

双缓冲:为每个归并段分配两个缓冲,一个被当前比较/归并使用,另一个同时异步预读下一批数据。当当前缓冲用尽,立即切换到已预读的缓冲,同时后台继续预读下一批,从而让磁盘 I/O 与归并计算重叠,隐藏 I/O 延迟。异步预读(prefetch)在计算的同时提前读取后续数据,避免 CPU 等待磁盘。这样减少因 I/O 引起的停顿,提高吞吐。

核心是"让 I/O 与计算并行":双缓冲使一个缓冲在计算、另一个在 I/O,交替使用。异步预读提前发起 I/O,利用计算时间完成读取。这消除了"计算→等待 I/O→计算"的串行停顿,是外部排序性能优化的关键。

#
★★

11. 分布式 Top-K 中精确解为何需要全局归并或分桶,近似解如何用 sketch 与误差上界交换成本

分布式 Top-K 中,精确解为何需要全局归并或分桶?近似解如何用 sketch 与误差上界交换成本?

  • 精确解需全局归并/分桶
  • 近似解用 sketch
  • 误差上界

分布式 Top-K 精确解:各机器局部统计后,需全局归并(把各节点 Top 候选汇总再精确排序)或分桶(按值范围分桶,定位 Top-K 所在桶后精确取),因为每个节点的局部 Top 不一定全局 Top,需全局汇总。近似解用 sketch(如 Bloom 过滤器、Count-Min Sketch、HyperLogLog)在每台机器上做压缩统计,以有界误差(如 ε 误差、δ 概率)交换成本,得到近似 Top-K,无需全局精确归并,省内存与通信。

精确解需保证全局正确 → 必须汇总比较(全局归并/分桶);近似解接受误差上界 → 用 sketch 压缩统计,误差受 ε、δ 约束,成本大幅降低。这是"精确 vs 近似"在分布式场景的权衡。

#

12. 如何用 MapReduce 做分布式外排序与多路归并

如何用 MapReduce 做分布式外排序与多路归并?

  • MapReduce 排序
  • 分区与排序
  • 归并

MapReduce 做分布式排序:map 阶段把每个键值对按 key 划分到分区(buffer 内排序),shuffle 阶段同一 key 的数据到同一 reducer,map 端与 reduce 端都做局部排序(外排序,分块写盘再归并),reduce 阶段各分区内部有序。整个输出按分区顺序拼接即全局有序。MapReduce 框架本身内置"按 key 排序",配合分区实现分布式外排序与多路归并。

MapReduce 的 shuffle 天然按 key 分区排序,reduce 端对每个分区做归并。多路归并体现在 reduce 端把多个 map 输出(已排序段)合并。通过合理分区(采样)保证各分区键区间连续,即得全局有序。

#

13. Combiner 为何能在 map 端预聚合降低网络 shuffle 量

Combiner 为什么能在 map 端预聚合,从而降低网络 shuffle 量?

  • Combiner 是本地 reducer
  • map 端预聚合
  • 降低网络传输

Combiner 是 map 端执行的"迷你 reducer",在每个 map 任务本地对输出按 key 做预聚合(如求和、计数),再把聚合后的结果传给 reducer。因为聚合是结合律的(如求和、取最大),可以先在 map 端合并,减少传给 reducer 的键值对数量,从而降低网络 shuffle 量(网络中传输的数据量)与 reduce 端负载。Combiner 必须满足"结果可再聚合"(即与 reducer 可交换结合)。

map 端每个 key 可能产生很多重复键,Combiner 先合并 → 传往 shuffle 的数据大幅减少。前提是聚合函数在局部与全局可结合(如 sum、max、count),否则结果错误。这是 MapReduce 优化网络 I/O 的关键技巧。

#

14. MapReduce 的 map/shuffle/reduce 三阶段数据流动

MapReduce 的 map、shuffle、reduce 三阶段的数据流动是怎样的?

  • map 阶段
  • shuffle 阶段
  • reduce 阶段

MapReduce 数据流动:map 阶段把输入分割成键值对,map 函数处理生成中间键值对,本地排序并分区;shuffle 阶段把相同 key 的键值对通过网络传输到同一 reducer(含分区、排序、合并),进行数据分发;reduce 阶段对每个 key 的 value 列表做聚合,输出最终结果。数据流:输入 → map → 中间键值对 → shuffle(按 key 分组)→ reduce → 输出。

三阶段核心是"键值对"模型:map 产生中间键值对,shuffle 按 key 分组传输,reduce 聚合。shuffle 是数据流动的关键(网络 I/O、排序、合并)。理解此流程是理解 MapReduce 的基础。

#

15. PageRank 在 MapReduce 上的迭代实现与收敛判定

PageRank 如何在 MapReduce 上迭代实现?收敛判定如何做?

  • 迭代式算法
  • map/reduce 传播 rank
  • 收敛判定

PageRank 在 MapReduce 上实现为多轮迭代:每轮 map 阶段,每个节点把自己的 rank 值按出边均分传播给邻居;reduce 阶段汇总每个节点收到的所有 rank 值,加上阻尼因子,计算新 rank。每轮一轮 MR,重复直到收敛。收敛判定:比较相邻两轮所有节点 rank 的变化量(如 L1 范数或最大差值)小于阈值 ε,或达固定迭代次数。

PageRank 是幂迭代,需多轮 MR。每轮全局传播 rank 并聚合,收敛通过相邻轮 rank 差判断。缺点:每轮需全量落盘与重读,迭代开销大,这正是 MapReduce 不适合迭代的原因之一。

#

16. Pregel(BSP)的"超步+消息传递"如何优化图计算

Pregel(BSP)的"超步 + 消息传递"模型如何优化图计算?

  • 超步(superstep)
  • 消息传递
  • 顶点中心计算

Pregel 采用 BSP(整体同步并行)模型:计算分多个超步(superstep),每个超步内所有顶点并行执行(接收消息、更新状态、向邻居发送消息),超步结束同步屏障。顶点只在本地计算,通过消息传递与邻居交互,避免中间结果落盘。相比 MapReduce 每轮全量落盘,Pregel 的顶点状态常驻内存、消息在内存传递,大幅减少 I/O,适合迭代图计算。

BSP 的"超步 + 消息"让图计算以顶点为中心、消息为通信,迭代状态保留在内存,避免每轮磁盘读写。同步屏障保证一致性。适合 PageRank、BFS、连通分量等迭代图算法。

#

17. WordCount 如何体现"键值对"在 shuffle 按 key 聚合

WordCount 如何体现"键值对"在 shuffle 阶段按 key 聚合?

  • map 产生 (word,1)
  • shuffle 按 word 分组
  • reduce 求和

WordCount 中,map 阶段把每个词输出为键值对 (word, 1);shuffle 阶段把所有相同 word 的键值对按 key 分组,传输到同一 reducer(自动聚合为 (word, [1,1,1,...]));reduce 阶段对每个 word 的 value 列表求和,得到 (word, count)。这清晰体现"键值对 + shuffle 按 key 聚合"的模型:map 产生中间键值对,shuffle 按 key 分组,reduce 聚合。

WordCount 是 MapReduce 的经典示例,展示三阶段:map 生成 (word,1),shuffle 按 word 分组,reduce 求和。shuffle 按 key 聚合是核心,把相同 key 的 value 汇集到同一 reducer。体现"数据以键值对流动、按 key 分组处理"。

#

18. 为何 MapReduce 不适合迭代图算法(每轮全量落盘)

为什么 MapReduce 不适合迭代图算法?

  • 每轮全量落盘
  • 迭代中间结果
  • I/O 开销巨大

迭代图算法(如 PageRank、连通分量)需要多轮迭代,每轮迭代中,MapReduce 都要把中间结果从 map 端写盘、经 shuffle 传输、reduce 端读盘,中间结果全量落盘。每轮 I/O 开销巨大,迭代几十轮则整体极慢。而且图状态无法在内存中持续维护,每轮都要重新读取。因此 MapReduce 不适合需要大量迭代、状态常驻的图算法。

MapReduce 是"批处理"模型,每轮作业独立、中间结果落盘;迭代需多轮作业,导致重复 I/O。而 Pregel/BSP 让状态常驻内存、消息传递,避免每轮落盘。故迭代图算法用 Pregel 而非 MapReduce。

#

19. 在稀疏/稠密图上 BSP 与 MapReduce 的性能差异来源

在稀疏/稠密图上,BSP 与 MapReduce 的性能差异来源是什么?

  • 稀疏 vs 稠密
  • 消息/负载
  • 内存与 I/O

稀疏图边少、消息少,BSP 的顶点状态常驻内存、消息传递开销小,表现好;MapReduce 每轮仍需全量落盘与 shuffle,I/O 开销大,稀疏图优势不明显。稠密图边多、消息数量大,BSP 的同步屏障与消息传递可能成为瓶颈(消息风暴),MapReduce 的批量落盘/归并反而可能更稳定。性能差异源于:消息量、内存占用、通信 vs 落盘 I/O 的权衡。

BSP 优化"内存状态 + 消息",但稠密图消息多时(每顶点 O(度) 消息)通信/内存压力大;MapReduce 用批量磁盘 I/O 换取内存,稀疏图下 I/O 劣势明显、稠密图下相对稳定。差异本质是"通信驱动 vs 落盘驱动"。

#

20. MapReduce 的 straggler 中慢任务拖慢整体作业,推测执行与备份任务如何缓解

MapReduce 的 straggler(掉队任务)为什么拖慢整体作业?推测执行与备份任务如何缓解?

  • straggler
  • 木桶效应
  • 推测执行/备份

MapReduce 作业完成取决于最慢的任务(straggler),因 reducer 需等待所有 map 输出,落后者成为木桶短板拖慢整体。引起 straggler 的原因:机器负载不均、网络抖动、磁盘慢、数据倾斜。缓解:推测执行(speculative execution)——当某任务明显慢于同阶段其他任务时,在另一台机器上启动一个备份任务,两者并行,先完成的胜出;备份任务(backup task)可对冲单点变慢,提高整体完成速度。

分布式作业受最慢任务约束(木桶效应),straggler 是主要性能杀手。推测执行"宁可多跑一个备份"来对冲延迟,用额外资源换取时限。这是 Hadoop 等系统的关键容错/优化机制。

#

21. 数据库 ORDER BY 中 sort-merge 如何利用外排序与 LIMIT 提前终止,索引扫描为何可免排序

数据库 ORDER BY 中,sort-merge 如何利用外排序与 LIMIT 提前终止?索引扫描为何可免排序?

  • sort-merge 排序
  • LIMIT 提前终止
  • 索引天然有序

数据库 ORDER BY 当数据量大时用外部排序(sort-merge):先对数据分块排序生成有序段,再多路归并得到有序结果。若带 LIMIT k,可在归并过程中只取前 k 个元素就提前终止,不必排完整数据,节省大量 I/O。若查询利用了索引(如 B+ 树索引),索引本身按键有序,扫描索引即可按序输出,天然免排序,且可结合 LIMIT 直接取前 k 个。

sort-merge 分块 + 归并应对大数据;LIMIT 让归并到 k 个即停,减少排序量。索引有序使 ORDER BY 无需显式排序,复杂度降低。这是数据库优化 ORDER BY 的关键机制。