
👨💻 关于作者:会编程的土豆
"不是因为看见希望才坚持,而是坚持了才看见希望。"
你好,我是会编程的土豆,一名热爱后端技术的Java学习者。
📚 正在更新中的专栏:
-
《数据结构与算法》😊😊😊
-
《leetcode hot 100》🥰🥰🥰🤩🤩🤩
-
《数据库mysql》
💕作者简介:后端学习者
先看代码:
cpp
#include <iostream>
#include <vector>
using namespace std;
// 下沉调整:维护大顶堆性质
void heapify(vector<int>& arr, int i, int heapSize) {
int largest = i; // 假设当前节点最大
int left = 2 * i + 1; // 左子节点
int right = 2 * i + 2; // 右子节点
// 找出父、左、右三者中的最大值
if (left < heapSize && arr[left] > arr[largest])
largest = left;
if (right < heapSize && arr[right] > arr[largest])
largest = right;
// 如果最大值不是父节点,交换并递归调整
if (largest != i) {
swap(arr[i], arr[largest]);
heapify(arr, largest, heapSize);
}
}
void heapSort(vector<int>& arr) {
int n = arr.size();
// 1. 建堆:从最后一个非叶子节点开始下沉
for (int i = n / 2 - 1; i >= 0; --i)
heapify(arr, i, n);
// 2. 排序:逐个将堆顶(最大值)移到末尾
for (int i = n - 1; i > 0; --i) {
swap(arr[0], arr[i]); // 交换堆顶和末尾
heapify(arr, 0, i); // 调整剩余堆
}
}
// 测试
int main() {
vector<int> arr = {4, 6, 8, 5, 9};
heapSort(arr);
for (int x : arr)
cout << x << " ";
// 输出:4 5 6 8 9
return 0;
}
很多人第一次学堆排序的时候,都会卡在一个地方:
heapify 这玩意到底在干什么?
代码不长,但就是理解不了。
这篇文章我们不背模板,从"过程"出发,把堆排序彻底讲清楚。
一、先搞清楚:什么是堆?
堆是一种完全二叉树,分两种:
1. 大顶堆
父节点 >= 子节点
例如:
9
/ \
6 8
/ \
5 4
特点:
堆顶(根)是最大值
2. 用数组表示堆
这是重点!
如果用数组存:
index: 0 1 2 3 4
value: 9 6 8 5 4
关系是:
左子节点 = 2*i + 1
右子节点 = 2*i + 2
二、heapify:核心操作到底在干嘛?
来看函数:
void heapify(vector<int>& arr, int i, int heapSize)
作用一句话总结:
让以 i 为根的子树,变成一个大顶堆
具体做了什么?
int largest = i;
int left = 2*i + 1;
int right = 2*i + 2;
先找出:
-
当前节点
-
左孩子
-
右孩子
然后找最大值:
cpp
if (left < heapSize && arr[left] > arr[largest])
largest = left;
if (right < heapSize && arr[right] > arr[largest])
largest = right;
如果最大值不是父节点
cpp
swap(arr[i], arr[largest]);
heapify(arr, largest, heapSize);
这一步非常关键:
把"更大的那个"提上来,然后继续往下调整
举个例子
原数组:
[4, 6, 8, 5, 9]
假设现在在 i=0:
4
/ \
6 8
/
5
最大的是 8 → 交换:
8
/ \
6 4
/
5
然后继续对 4 进行 heapify。
三、第一步:建堆(最容易忽略的细节)
cpp
for (int i = n / 2 - 1; i >= 0; --i)
heapify(arr, i, n);
为什么从 n/2 - 1 开始?
因为:
n/2 之后的节点全是叶子节点(不需要调整)
这个过程在做什么?
从下往上,把每个子树都变成大顶堆。
最终效果:
整个数组变成一个大顶堆
四、第二步:排序过程
cpp
for (int i = n - 1; i > 0; --i) {
swap(arr[0], arr[i]);
heapify(arr, 0, i);
}
每一轮发生了什么?
1. 把最大值放到最后
cpp
swap(arr[0], arr[i]);
因为堆顶是最大值。
2. 缩小堆的范围
cpp
heapify(arr, 0, i);
只对前 i 个元素继续维护堆。
举个完整过程
初始:
[4, 6, 8, 5, 9]
建堆后:
[9, 6, 8, 5, 4]
第一轮:
swap → [4, 6, 8, 5, 9]
heapify → [8, 6, 4, 5, 9]
第二轮:
swap → [5, 6, 4, 8, 9]
heapify → [6, 5, 4, 8, 9]
继续下去:
最终结果:
[4, 5, 6, 8, 9]
五、复杂度分析
-
建堆:O(n)
-
每次调整:O(log n)
-
总复杂度:O(n log n)
六、堆排序的特点
优点:
-
不需要额外空间(原地排序)
-
最坏情况也是 O(n log n)
缺点:
-
不稳定排序
-
常数略大(比快排慢一点)
七、最容易犯的错误
1. heapify 写错边界
left < heapSize
right < heapSize
2. 忘记递归
heapify(arr, largest, heapSize);
3. 建堆起点写错
n/2 - 1
不是 n-1!
八、总结一句话
堆排序的本质就是:不断把"当前最大值"放到数组末尾
排序过程再次重述:
假设数组:
cpp
[4, 6, 8, 5, 9]
第一步:建堆
建完堆后,数组变成大顶堆:
cpp
[9, 6, 8, 5, 4]
对应树:
cpp
9
/ \
6 8
/ \
5 4
堆顶 9 是最大值。
第二步:把最大值放到末尾
cpp
swap(arr[0], arr[i]);
i = 4(数组末尾):
交换 9 和 4:
[4, 6, 8, 5, 9]
现在最后一位已经排好了(9)
第三步:调整剩余堆
我们把"剩余堆"的长度设为 i = 4,然后对堆顶做 heapify:
cpp
heapify(arr, 0, 4)
根节点 4,比它的两个子节点 6 和 8 小,所以我们找到最大子节点 8,交换:
cpp
交换 4 和 8:
[8, 6, 4, 5, 9]
然后继续对 4 的位置(下沉)做 heapify(如果还有子节点比它大就再交换)。这里 4 的子节点是 5,比 4 大,所以交换:
cpp
[8, 6, 5, 4, 9]
剩余堆调整完毕,最大值在堆顶(8)
第四步:重复上述操作
下一轮 i = 3:
- 交换堆顶 8 和末尾 4:
4, 6, 5, 8, 9
- 对剩余堆 [4,6,5] heapify:
- 根 4,比子节点 6 大小不对,交换:
6,4,5,8,9
- 根 4 下沉到叶子,不需要调整了
最终堆:
6,4,5
第五步:继续交换堆顶和末尾,直到排序完成
- i=2:交换 6 和 5 → heapify → [5,4]
- i=1:交换 5 和 4 → heapify → [4]
最终数组有序:
4,5,6,8,9
核心理解
- 每次把堆顶(最大值)放到末尾
- 剩余部分继续维护大顶堆
- 循环完成后,整个数组就是升序排列
所以排序阶段就是"不断提最大值 + 调整剩余堆",只要记住这一点,heapSort 的流程就很直观了。