一、为什么会诞生堆排序?("原因"篇)
回顾之前的算法:
-
选择排序:每轮遍历找最值,时间复杂度 O(n²),瓶颈在于"找最值"太慢。
-
快速排序 :平均极快,但最坏会退化为 O(n²)(尤其有序数据),且不稳定。
-
归并排序 :稳定且保证 O(n log n),但需要 O(n) 额外内存(空间换时间)。
科学家们想要一个 "既保证最坏 O(n log n),又不占额外内存" 的算法。于是,堆排序应运而生------它用堆 这种数据结构,将"找最值"的时间从 O(n) 优化到了 O(log n)。
二、核心原理:什么是堆?("是什么"篇)
堆是一棵完全二叉树,并且满足堆序:
-
大根堆 :每个父节点的值 ≥ 其两个子节点的值。堆顶(根节点)就是全局最大值。
-
小根堆 :每个父节点的值 ≤ 其子节点的值,堆顶是全局最小值。
堆在内存中不是用链式树结构 ,而是用数组存储,利用下标关系:
-
父节点下标 =
(i - 1) / 2 -
左子节点下标 =
2 * i + 1 -
右子节点下标 =
2 * i + 2
算法两步走:
-
建堆(Build Heap):将无序数组原地调整为一个大根堆(O(n) 时间)。
-
排序(Sort):将堆顶(最大值)与数组末尾交换,堆大小减 1,然后对堆顶执行"下沉(Sift Down)"恢复堆序,重复 n-1 次。
三、C++ 代码实现("怎么用"篇)
cpp
#include <iostream>
#include <vector>
using namespace std;
// 下沉调整:将节点 i 向下调整,使其满足大根堆性质
void heapify(vector<int>& arr, int n, int i) {
int largest = i; // 假设当前节点最大
int left = 2 * i + 1; // 左子节点
int right = 2 * i + 2; // 右子节点
// 如果左子节点存在且大于当前最大值,更新
if (left < n && arr[left] > arr[largest])
largest = left;
// 如果右子节点存在且大于当前最大值,更新
if (right < n && arr[right] > arr[largest])
largest = right;
// 如果最大值不是当前节点,交换并递归下沉
if (largest != i) {
swap(arr[i], arr[largest]);
heapify(arr, n, largest); // 递归调整受影响的子树
}
}
// 堆排序主函数
void heapSort(vector<int>& arr) {
int n = arr.size();
// 1. 建堆:从最后一个非叶子节点开始,向上执行下沉(O(n))
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(arr, n, i);
}
// 2. 排序:反复取堆顶放到末尾
for (int i = n - 1; i > 0; i--) {
// 将堆顶(最大值)与当前未排序区间最后一个元素交换
swap(arr[0], arr[i]);
// 堆大小减 1,从堆顶开始下沉恢复堆序
heapify(arr, i, 0);
}
}
(注:建堆时从 n/2 - 1 开始,因为叶子节点(>= n/2)无需下沉。)
四、堆排序的核心优势(为什么选它)
| 对比维度 | 堆排序 | 快速排序 | 归并排序 |
|---|---|---|---|
| 最坏时间复杂度 | ✅ O(n log n) | ❌ O(n²)(有序数据) | ✅ O(n log n) |
| 平均时间复杂度 | O(n log n) | O(n log n)(常数更小) | O(n log n) |
| 空间复杂度 | ✅ O(1)(原地) | O(log n)(递归栈) | ❌ O(n)(辅助数组) |
| 稳定性 | ❌ 不稳定 | ❌ 不稳定 | ✅ 稳定 |
| 缓存友好度 | ❌ 差(跳跃访问) | ✅ 极好(顺序访问) | ✅ 较好 |
堆排序的绝对优势 :确定性 + 内存零开销。在内存极度受限的嵌入式环境、实时系统中,堆排序是首选。
五、短板与避坑(面试深度加分项)
-
缓存不友好 :堆排序访问内存是跳跃式的(
i->2i+1),CPU 缓存命中率远低于快排,因此工程级std::sort不采用纯堆排,只作为内省排序中防止快排退化的"保险丝"。 -
不稳定 :
swap(arr[0], arr[i])是远程交换,会打乱相等元素的相对顺序。 -
STL 现成工具 :C++ 标准库提供了
<algorithm>中的std::make_heap、std::push_heap、std::pop_heap、std::sort_heap,工程中如需堆操作,直接调库即可,无需手写。
六、总结
堆排序是"用数据结构的强度来弥补暴力枚举的弱点 "的典型范例。面试手撕时,只要写出 heapify + 建堆循环 + 排序循环,并主动指出"快排虽快但会退化,堆排牺牲一点缓存换来了绝对稳健",就能让面试官眼前一亮。
资源推荐