10 亿条数据排序:外部排序与 10 路归并完整说明

一、问题背景

在真实生产环境中,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. 归并规则

  1. 弹出堆顶(当前全局最小)

  2. 写入结果集

  3. 从该元素所属分块中读取下一个元素入堆

  4. 重复直到所有分块耗尽


九、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 亿数据排序问题在逻辑、实现、工程层面已经完全闭环。

相关推荐
San813_LDD1 小时前
[C语言]《Dev-C++ 报错解决手册(Day0607 精华版)》
java·前端·javascript
Anastasiozzzz2 小时前
从有限状态机到智能体图:传统 FSM 与 Agent Graph的演进
java·人工智能·python·ai
不懂数据的小白8 小时前
面试题一:【二】异动分析(诊断)
面试
wang09078 小时前
自己动手写一个spring之IOC_2
java·后端·spring
来杯@Java8 小时前
学生选课管理系统(基于springboot+vue前后端分离的项目)计算机毕业设计java
java·spring boot·spring·vue·毕业设计·maven·mybatis
Aphasia3119 小时前
https连接传输流程
前端·面试
不知名的老吴9 小时前
线程的生命周期之线程“插队“
java·开发语言·python
kyriewen9 小时前
CSS Container Queries:彻底告别 @media 写到手软,附 5 个真实布局案例
前端·css·面试
ANnianStriver9 小时前
PetLumina-02-后端开发与前后端联调
java·ai·sa-token