想象你是体育老师,要把一群身高不同的学生按身高排队(升序)。但直接排序太累,你想了个妙招:
-
建“高个子金字塔”(建大顶堆):
-
让学生站成完全二叉树队形(数组存储),要求:父节点必须 ≥ 子节点(大顶堆)。
-
从最后一个非叶子节点(倒数第二排)开始调整:
- 比如节点A(身高160)的左右孩子分别是155和175,175更高 → 让175和A交换位置。
- 接着检查前一个非叶子节点,直到根节点。
-
结果:根节点(堆顶)是最高学生(175),但其他节点不一定有序。
-
-
选拔队长 + 重新整队(排序阶段):
- 第1轮:堆顶(175)和队尾交换 → 175站到末尾(已有序)。
- 剩余学生中,根节点(原倒数第二高的学生)可能破坏堆结构 → 重新调整为大顶堆(根节点下沉)。
- 重复:新堆顶(如165)再和倒数第二位置交换 → 165归位,继续调整……
最终效果:每次交换堆顶都让一个高个子归位,队尾逐渐有序,队前保持大顶堆结构。
⚙️ Java代码实现(逐行解析)
java
Copy
public class HeapSort {
// 堆排序入口
public static void heapSort(int[] arr) {
// 1. 构建初始大顶堆(从最后一个非叶子节点开始)
for (int i = arr.length / 2 - 1; i >= 0; i--) {
adjustHeap(arr, i, arr.length); // 调整子树
}
// 2. 排序:交换堆顶与末尾 + 调整剩余堆
for (int j = arr.length - 1; j > 0; j--) {
swap(arr, 0, j); // 堆顶(最大值)放到末尾
adjustHeap(arr, 0, j); // 剩余部分重新调整
}
}
// 调整堆(核心:父节点下沉)
private static void adjustHeap(int[] arr, int parent, int len) {
int temp = arr[parent]; // 暂存父节点值
for (int child = 2 * parent + 1; child < len; child = 2 * child + 1) {
// 找最大子节点:右孩子存在且更大 → 选右孩子
if (child + 1 < len && arr[child] < arr[child + 1]) {
child++;
}
// 子节点 > 父节点?父节点下沉!
if (arr[child] > temp) {
arr[parent] = arr[child]; // 子节点覆盖父节点
parent = child; // 父指针下移到子节点
} else {
break; // 父节点 >= 子节点 → 无需调整
}
}
arr[parent] = temp; // 父节点找到最终位置
}
// 交换元素
private static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
public static void main(String[] args) {
int[] heights = {160, 155, 175, 170, 165};
heapSort(heights);
System.out.println("按身高排队: " + Arrays.toString(heights));
// 输出: [155, 160, 165, 170, 175]
}
}
🔍 关键步骤图解(以数组 [4, 10, 3, 5, 1] 为例)
-
初始建堆:
Copy 4 → 调整节点1(10) 10 / \ 子节点5>4? 交换 / \ 10 3 → 最终大顶堆: 5 3 / \ [10, 5, 3, 4, 1] / \ 5 1 4 1 -
排序阶段:
-
第1轮:堆顶10和末尾1交换 → 数组变
[1, 5, 3, 4, 10](10归位)。 -
调整剩余堆
[1, 5, 3, 4]→ 父节点1下沉,5上浮 → 新堆顶5。 -
第2轮:5和4交换 →
[4, 1, 3, 5, 10](5归位)。 -
继续调整直到全部有序
-
📊 堆排序性能与特点
| 特性 | 说明 |
|---|---|
| ⏱️ 时间复杂度 | O(n log n)(建堆O(n) + n次调整O(log n))45 |
| 📦 空间复杂度 | O(1)(原地排序,无需额外空间)79 |
| ⚠️ 稳定性 | 不稳定(父子交换可能破坏相同值顺序,如 [5₁, 5₂, 1] → 5₂可能先于5₁)9 |
| 💡 适用场景 | 大规模数据、内存受限场景(如嵌入式系统)、Top K问题(如找前10名)510 |
❓ 常见疑问解答
-
为什么升序用大顶堆?
大顶堆的堆顶是最大值,每次交换到末尾,队尾逐渐形成有序区(升序)。若用小顶堆,堆顶是最小值,交换后需放队首,实现更复杂 -
为什么从
n/2-1开始建堆?
完全二叉树中,最后一个非叶子节点索引 =n/2-1(n为长度)。从此处倒序调整,可减少无效比较 -
堆排序 vs 快速排序?
-
堆排序:时间复杂度稳定O(n log n),但缓存命中率低(父子节点内存不连续)
-
快速排序:平均O(n log n),但最坏O(n²);缓存友好但递归栈占用空间
-
💎 总结
堆排序 = “堆化 + 交换下沉”:
- 建堆:从后向前调整非叶子节点,构建大顶堆;
- 排序:堆顶与末尾交换 → 最大值归位 → 剩余部分重新堆化。
🌟 一句话精髓:像选拔队长一样,每次选出最高者放到队尾,剩余队伍重新整队,直到全员有序!
掌握此算法,你不仅能处理海量数据排序,还能高效解决Top K问题(如“10万订单中找金额最高的前10笔”)