【数据结构与算法】堆排序底层原理

👨‍💻 关于作者:会编程的土豆

"不是因为看见希望才坚持,而是坚持了才看见希望。"

你好,我是会编程的土豆,一名热爱后端技术的Java学习者。

📚 正在更新中的专栏:

💕作者简介:后端学习者

先看代码:

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:

  1. 交换堆顶 8 和末尾 4:

4, 6, 5, 8, 9

  1. 对剩余堆 [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


核心理解

  1. 每次把堆顶(最大值)放到末尾
  2. 剩余部分继续维护大顶堆
  3. 循环完成后,整个数组就是升序排列

所以排序阶段就是"不断提最大值 + 调整剩余堆",只要记住这一点,heapSort 的流程就很直观了。

相关推荐
cmpxr_1 天前
【C】数组名、函数名的特殊
c语言·算法
KAU的云实验台1 天前
【算法精解】AIR期刊算法IAGWO:引入速度概念与逆多元二次权重,可应对高维/工程问题(附Matlab源码)
开发语言·算法·matlab
会编程的土豆1 天前
【数据结构与算法】再次全面了解LCS底层
开发语言·数据结构·c++·算法
低频电磁之道1 天前
解决 Windows C++ DLL 导出类不可见的编译错误
c++·windows
大熊背1 天前
如何利用Lv值实现三级降帧
算法·自动曝光·lv·isppipeline
大尚来也1 天前
驾驭并发:.NET多线程编程的挑战与破局之道
java·前端·算法
向阳而生,一路生花1 天前
深入浅出 JDK7 HashMap 源码分析
算法·哈希算法
君义_noip1 天前
信息学奥赛一本通 4150:【GESP2509七级】⾦币收集 | 洛谷 P14078 [GESP202509 七级] 金币收集
c++·算法·gesp·信息学奥赛·csp-s
Ricky_Theseus1 天前
静态链接与动态链接
c++
摸个小yu1 天前
【力扣LeetCode热题h100】链表、二叉树
算法·leetcode·链表