Java堆排序:用二叉树思维排序
Java 堆排序:用二叉树思维排序
借优先级队列的壳,排最大最小的序
引言
堆排序(Heap Sort)把数组当作完全二叉树,通过不断”堆化”把最大/最小元素顶到根,然后扔到末尾。
完美结合了快排的速度和归并的空间优势:
- 时间复杂度稳定 O(n log n)
- 空间复杂度 O(1),原地排序
- 代价是不稳定,常数比快排大
它是 TopK 问题的最优解(无需全排序),也是 Java PriorityQueue 的底层数据结构。
这篇文章,我会把堆排序讲透:
- 用 ASCII 图演示堆化(heapify)和堆排序的完整过程
- 推导为什么 siftDown 是核心操作
- 列出 5 个高频易错点(尤其是建堆起始下标)
- 配套 LeetCode 215(TopK)和 347(前 K 个高频元素)
1. 核心思想:用数组模拟完全二叉树
1.1 堆的定义
堆是一种完全二叉树,满足:
- 大顶堆:父节点 ≥ 子节点(堆顶是最大值)
- 小顶堆:父节点 ≤ 子节点(堆顶是最小值)
1.2 数组存储完全二叉树
1 | |
关键公式(0-indexed):
- 左子节点:
2 * i + 1 - 右子节点:
2 * i + 2 - 父节点:
(i - 1) / 2 - 最后一个非叶子节点:
n / 2 - 1
1.3 ASCII 图解:堆排序完整过程
以数组 [12, 11, 13, 5, 6, 7] 为例,目标是升序:
第 1 步:建大顶堆
1 | |
第 2 步:排序
1 | |
1.4 三个关键观察
- **建堆是 O(n)**——不是直觉的 O(n log n)(自底向上的均摊复杂度更低)
- **排序是 O(n log n)**——n 趟交换 + 每趟 siftDown 是 O(log n)
- 要排升序必须用大顶堆——堆顶是最大值,扔到末尾正好是最大
2. 完整 Java 实现
1 | |
3. 复杂度证明
3.1 建堆复杂度:O(n)(不是 O(n log n))
直觉上建堆是 O(n log n),但实际上更精确:
| 节点层 | 节点数 | siftDown 最大深度 |
|---|---|---|
| 倒数第 1 层(叶子) | n/2 | 0 |
| 倒数第 2 层 | n/4 | 1 |
| 倒数第 3 层 | n/8 | 2 |
| … | … | … |
| 根 | 1 | log n |
总比较次数:
1 | |
3.2 排序复杂度:O(n log n)
- n 趟交换
- 每趟 siftDown 最坏下沉 log n 层
- 总:O(n log n)
3.3 总复杂度:O(n log n)
- 建堆 O(n) + 排序 O(n log n) = O(n log n)
3.4 空间复杂度
- **O(1)**,原地排序(siftDown 用的 largest/left/right 是常数变量)
3.5 稳定性:不稳定
- siftDown 时的交换会破坏相同元素的相对顺序
- 不稳定排序
4. 五个高频易错点
易错点 1:建堆起始下标写成 n - 1
1 | |
原理:叶子节点(i ≥ n/2)没有子节点,已经是堆。
易错点 2:用小顶堆排升序
1 | |
记忆口诀:升序用大顶堆,降序用小顶堆。
易错点 3:siftDown 里忘记传 heapSize
1 | |
易错点 4:siftDown 写成迭代版时漏掉递归
1 | |
易错点 5:左右子节点比较时漏掉等号
1 | |
5. TopK 问题:堆排序的最强战场
堆排序的真正价值不是全排序,而是 TopK 问题——找最大/最小的 K 个元素,无需全排序。
5.1 求前 K 大元素(小顶堆解法)
思路:维护大小为 K 的小顶堆,堆顶是最小的;遍历数组时,如果当前元素比堆顶大,就替换堆顶。
1 | |
复杂度:O(n log k),比全排序 O(n log n) 快得多。
5.2 求前 K 大元素(大顶堆解法)
思路:把所有元素放入大顶堆,弹出 K 次。
1 | |
复杂度:O(n log n),没有利用 K 小的优势。
5.3 两种解法对比
| 解法 | 时间 | 适用场景 |
|---|---|---|
| 小顶堆(size=K) | O(n log k) | K 较小(k << n) |
| 大顶堆(size=n) | O(n log n) | K 接近 n |
工程首选:小顶堆。
6. 适用场景与禁区
6.1 适用场景
- TopK 问题:小顶堆 O(n log k) 是最优解
- **需要 O(1) 空间的 O(n log n)**:堆排序的独特优势
- 优先级队列底层:Java
PriorityQueue用的就是堆 - 数据流中的中位数:维护两个堆(大顶堆 + 小顶堆)
6.2 不适用场景
- 要求稳定排序:堆排序不稳定
- 数据基本有序:堆排序对有序数据没优势,反而比插入排序慢
- 小数据量:常数大,不如插入排序
7. 面试常见变形
变形 1:siftUp(上浮,用于插入)
1 | |
适用:往堆里插入新元素。
变形 2:数据流中的中位数(Hard)
思路:维护两个堆——大顶堆存较小的一半,小顶堆存较大的一半
1 | |
复杂度:addNum O(log n),findMedian O(1)。
变形 3:堆化字符串(LeetCode 767)
思路:字符频率统计 + 大顶堆 + 贪心插入
8. LeetCode 真题实战
题目 1:215. 数组中的第 K 个最大元素(中等)
解法 1:堆排序解法(全排序)
1 | |
结果:n=10000 时能 AC(O(n log n) 比快排的 O(n²) 稳定)。
解法 2:小顶堆解法(推荐)
1 | |
复杂度:O(n log k),比全排序快得多。
题目 2:347. 前 K 个高频元素(中等)
解法:哈希表统计频率 + 小顶堆选 TopK
1 | |
复杂度:O(n log k)。
题目 3:295. 数据流的中位数(困难)
解法:套用变形 2。
总结
一句话精髓
堆排序 = 建大顶堆 + 堆顶扔末尾 + siftDown 维护堆。
三个关键洞察
- TopK 问题才是堆的主战场——全排序用堆排不如快排
- 升序用大顶堆,降序用小顶堆——反了排出来是反的
- siftDown 是核心——理解了下沉,TopK/中位数/优先级队列都通了
下一步行动
- 手写小顶堆解 TopK(变形 1),体会 O(n log k) 的优势
- 实现数据流中位数(变形 2),这是大厂常考 Hard 题
- 在 JDK 源码搜索
PriorityQueue,看 siftDown 的工业实现
本文由 AI 对话整理精炼而成
整理时间:2026-06-18
原文来源:DeepSeek