一、问题背景
在真实生产环境中,10 亿条数据无法一次性加载到内存中排序。即使单条数据只有 8 字节,10 亿条也需要约 8GB 内存,还未考虑 JVM 对象开销、堆外内存、系统占用等问题。
因此,该问题的本质不是"用什么排序算法",而是:
在内存受限的前提下,如何保证最终结果"全局有序"。
这类问题在工程上有一个标准解法:外部排序(External Sort)。
二、核心结论(先给答案)
10 亿条数据排序的正确做法是:外部排序 = 分块排序 + 多路归并。
-
分块排序:解决"内存放不下"的问题
-
多路归并:解决"全局有序"的问题
⚠️ 只做分块排序,不做归并,一定是错误的。
三、为什么"分块排序"不等于"整体有序"?
1. 反例说明
假设数据被拆成两个分块:
分块 A(已排序):1, 7, 9
分块 B(已排序):4, 5, 6
-
A 内部有序
-
B 内部有序
-
直接拼接结果:
1, 7, 9, 4, 5, 6❌(全局无序)
结论:
分块排序只能得到"局部有序",不能保证"全局有序"。
四、真正保证全局有序的关键:归并(Merge)
1. 归并的核心思想
每一步只输出"当前所有未处理数据中的最小值"。
前提条件:
-
每个分块内部已经有序
-
分块之间不要求任何数值区间关系
五、两路归并示例(手推理解)
输入
A = [1, 7, 9]
B = [4, 5, 6]
归并过程
| 步骤 | A 当前 | B 当前 | 输出 | 说明 |
|---|---|---|---|---|
| 1 | 1 | 4 | 1 | 1 是当前最小 |
| 2 | 7 | 4 | 4 | 4 < 7 |
| 3 | 7 | 5 | 5 | 5 < 7 |
| 4 | 7 | 6 | 6 | 6 < 7 |
| 5 | 7 | - | 7,9 | B 已空,直接追加 |
最终结果:
1, 4, 5, 6, 7, 9
六、从两路归并到 10 路归并
1. 本质没有变化
-
两路归并:每次比较 2 个当前元素
-
10 路归并:每次比较 10 个当前元素
区别仅在于:
比较方式从"if 判断"升级为"小顶堆(PriorityQueue)"。
七、PriorityQueue 是什么?
1. 定义
PriorityQueue 是 Java 提供的 基于堆(Heap)的优先队列实现。
它的核心能力是:
可以在 O(log K) 的时间内,插入元素并获取当前最小值。
2. 为什么多路归并一定要用它?
在 10 路归并中:
-
每一路只暴露一个"当前最小值"
-
每一步都要从这 10 个值中选出最小的
如果不用堆:
-
每次都要遍历 10 路(O(K))
-
K 大时性能不可接受
八、10 路归并完整示例
1. 输入(10 个已排序分块)
F1: [ 1, 20, 30 ]
F2: [ 4, 25, 40 ]
F3: [ 7, 21, 50 ]
F4: [ 2, 18, 60 ]
F5: [ 5, 19, 70 ]
F6: [ 6, 22, 80 ]
F7: [ 3, 23, 90 ]
F8: [ 8, 24,100 ]
F9: [ 9, 26,110 ]
F10: [10, 27,120 ]
2. 初始堆状态
堆中存放每个分块的第一个元素:
1,2,3,4,5,6,7,8,9,10
3. 归并规则
-
弹出堆顶(当前全局最小)
-
写入结果集
-
从该元素所属分块中读取下一个元素入堆
-
重复直到所有分块耗尽
九、Java 实现(核心代码)
class Node {
int value; // 当前值
int fileIndex; // 来自第几个分块
int pos; // 在该分块中的位置
Node(int value, int fileIndex, int pos) {
this.value = value;
this.fileIndex = fileIndex;
this.pos = pos;
}
}
PriorityQueue<Node> heap = new PriorityQueue<>(
Comparator.comparingInt(n -> n.value)
);
归并循环:
while (!heap.isEmpty()) {
Node cur = heap.poll();
result.add(cur.value);
int nextPos = cur.pos + 1;
if (nextPos < blocks[cur.fileIndex].length) {
heap.offer(new Node(
blocks[cur.fileIndex][nextPos],
cur.fileIndex,
nextPos
));
}
}
十、复杂度分析(面试必答)
-
时间复杂度:
O(N log K)-
N:总数据量(10 亿)
-
K:归并路数(10)
-
-
空间复杂度:
O(K)- 堆中最多同时存在 K 个元素
十一、真实生产环境中的外部排序
1. 分块阶段
-
顺序读取磁盘
-
控制单块大小(如 500MB ~ 1GB)
-
内存排序后写回磁盘
2. 归并阶段
-
使用 BufferedInputStream / FileChannel
-
每个文件维护一个游标
-
堆中只保存当前头元素
十二、面试标准总结话术
对于 10 亿条无法一次性加载内存的数据,我会采用外部排序方案:
先进行分块内存排序生成多个有序文件,再通过基于 PriorityQueue 的多路归并,
在 O(N log K) 的复杂度下完成全局有序输出。
十三、关键认知总结
-
分块排序 ≠ 全局排序
-
全局有序由"归并"保证
-
PriorityQueue 是多路归并的核心工具
-
外部排序是工程领域的标准解法
至此,10 亿数据排序问题在逻辑、实现、工程层面已经完全闭环。