数据结构-排序算法-堆排序(重点比赛面试经常考)

文章目录

  • [1. 堆排序](#1. 堆排序)
    • [1.1 基本思想](#1.1 基本思想)
    • [1.2 堆的前置必备知识(必看懂,不然学不会堆排序)](#1.2 堆的前置必备知识(必看懂,不然学不会堆排序))
      • [1.2.1 完全二叉树](#1.2.1 完全二叉树)
      • [1.2.2 父子节点下标公式(死记常用)](#1.2.2 父子节点下标公式(死记常用))
      • [1.2.3 大顶堆 & 小顶堆](#1.2.3 大顶堆 & 小顶堆)
  • [2. 堆排序整体流程](#2. 堆排序整体流程)
  • [3. 堆排序代码实现](#3. 堆排序代码实现)
    • [3.1 完整可运行代码](#3.1 完整可运行代码)
  • [4. 代码逐模块精细精讲(难点全拆透)](#4. 代码逐模块精细精讲(难点全拆透))
    • [4.1 AdjustHeap 向下堆化函数(最难核心)](#4.1 AdjustHeap 向下堆化函数(最难核心))
    • [4.2 初始建堆代码逐行精讲](#4.2 初始建堆代码逐行精讲)
    • [4.3 排序交换循环 for (int i = n - 1; i > 0; i--)](#4.3 排序交换循环 for (int i = n - 1; i > 0; i--))
  • [5. 复杂度与稳定性详细分析](#5. 复杂度与稳定性详细分析)
    • [5.1 时间复杂度](#5.1 时间复杂度)
    • [5.2 空间复杂度](#5.2 空间复杂度)
  • [6. 堆排序核心特点总结](#6. 堆排序核心特点总结)

1. 堆排序

1.1 基本思想

堆排序属于选择排序的优化版本。

简单选择排序每一轮都要遍历整个无序区间找最大值,效率低;堆排序利用完全二叉树 + 堆的特性,不用逐个遍历,就能快速锁定最值,把时间复杂度从 O(n2) 优化到 O(nlogn)。

核心逻辑就两句话:

  • 把普通数组构建成大顶堆,堆顶天然就是当前最大值;
  • 堆顶和数组末尾交换,把最大值固定到有序区间,剩余部分重新调堆,循环往复完成排序。

1.2 堆的前置必备知识(必看懂,不然学不会堆排序)

1.2.1 完全二叉树

数组逻辑上映射为完全二叉树,节点按层从左到右依次存放,没有空缺位置。

堆排序所有下标计算,都建立在完全二叉树规则上。

1.2.2 父子节点下标公式(死记常用)

设当前节点下标为 i:

  • 左孩子下标:2i+1
  • 右孩子下标:2i+2
  • 父节点下标:(i−1)/2

1.2.3 大顶堆 & 小顶堆

大顶堆 :每一个父节点 ≥ 左右孩子,堆顶是整棵树最大值 堆排序升序必须用大顶堆

小顶堆:每一个父节点 ≤ 左右孩子,堆顶是整棵树最小值一般用于降序、TopK 问题

2. 堆排序整体流程

  1. 初始建堆 :把无序数组整体调整为大顶堆
  2. 交换固定最值:堆顶最大值 和 无序区间最后一个元素交换,末尾变成有序区间;
  3. 重新堆化:排除已经排好序的末尾元素,只对前面无序区间从根节点重新向下调整为大顶堆;
  4. 重复交换 + 堆化,直到所有元素全部有序。

3. 堆排序代码实现

3.1 完整可运行代码

c 复制代码
#include <stdio.h>

// 向下堆化:将以parent为根的子树调整为大顶堆
void AdjustHeap(int arr[], int parent, int n)
{
    int temp = arr[parent];    // 保存当前父节点值
    int child = 2 * parent + 1;// 先找左孩子

    // 循环往下逐层调整
    while (child < n)
    {
        // 1. 选出左右孩子中更大的那个
        if (child + 1 < n && arr[child] < arr[child + 1])
        {
            child++;
        }

        // 2. 父节点已经比最大孩子还大,无需调整,直接退出
        if (arr[child] <= temp)
        {
            break;
        }

        // 3. 大孩子上位,占据父节点位置
        arr[parent] = arr[child];

        // 往下继续遍历子树
        parent = child;
        child = 2 * parent + 1;
    }
    // 把最初的父节点值放入最终正确空位
    arr[parent] = temp;
}

// 堆排序主逻辑
void HeapSort(int arr[], int n)
{
    // 第一步:初始建大顶堆
    for (int i = (n - 2) / 2; i >= 0; i--)
    {
        AdjustHeap(arr, i, n);
    }

    // 第二步:交换堆顶 + 反复堆化
    for (int i = n - 1; i > 0; i--)
    {
        // 堆顶最大值 和 无序区间末尾交换
        int tmp = arr[0];
        arr[0] = arr[i];
        arr[i] = tmp;

        // 对前i个无序元素重新堆化
        AdjustHeap(arr, 0, i);
    }
}

// 打印数组
void PrintArr(int arr[], int n)
{
    for (int i = 0; i < n; i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

// 测试用例
int main()
{
    int arr[] = {49, 38, 65, 97, 76, 13, 27, 49};
    int n = sizeof(arr) / sizeof(arr[0]);

    printf("排序前数组:");
    PrintArr(arr, n);

    HeapSort(arr, n);

    printf("排序后数组:");
    PrintArr(arr, n);

    return 0;
}

3.2 运行结果:

c 复制代码
排序前数组:49 38 65 97 76 13 27 49 
排序后数组:13 27 38 49 49 65 76 97 

4. 代码逐模块精细精讲(难点全拆透)

这部分是堆排序的核心难点,我带你逐行、逐逻辑啃透代码,结合大顶堆规则,看懂每一行的作用。

4.1 AdjustHeap 向下堆化函数(最难核心)

作用:只把以 parent 为根的一棵子树,调整成大顶堆,不影响其他子树。

c 复制代码
// 向下堆化:将以parent为根的子树调整为大顶堆
void AdjustHeap(int arr[], int parent, int n)
{
    int temp = arr[parent];    // 保存当前父节点值
    int child = 2 * parent + 1;// 定位左孩子节点下标

    // 循环向下调整,直到越界或满足堆规则
    while (child < n)
    {
        // 步骤1:选择左右孩子中更大的那个
        if (child + 1 < n && arr[child] < arr[child + 1])
        {
            child++;
        }

        // 步骤2:父节点 ≥ 最大孩子,无需调整,直接退出
        if (arr[child] <= temp)
        {
            break;
        }

        // 步骤3:大孩子上移,覆盖父节点位置
        arr[parent] = arr[child];

        // 步骤4:继续向下遍历,处理下一层子树
        parent = child;
        child = 2 * parent + 1;
    }
    // 步骤5:将原始父节点值放入最终正确位置
    arr[parent] = temp;
}

逐行拆解:

  1. int temp = arr[parent]; 先保存根节点原值,防止后续孩子节点覆盖后丢失;
  2. **int child = 2 * parent + 1;**默认先取左孩子,完全二叉树规则;
  3. if (child + 1 < n && arr[child] < arr[child + 1]) child++;
    同时存在左右孩子时,选更大的那个孩子,符合大顶堆父大子小的规则;
  4. if (arr[child] <= temp) break;
    如果最大的孩子都比父节点小,说明这棵子树已经满足大顶堆,不用再往下调整;
  5. 孩子上位、迭代往下找把大孩子赋值到父节点位置,再把指针下移到孩子节点,继续往下调整子子孙孙;
  6. **arr[parent] = temp;**循环结束后,空出来的位置放入最初保存的父节点值,调整完成。

4.2 初始建堆代码逐行精讲

作用:把无序数组,整体构建成一个完整的大顶堆。

c 复制代码
// 第一步:初始建大顶堆
for (int i = (n - 2) / 2; i >= 0; i--)
{
    AdjustHeap(arr, i, n);
}
  1. i = (n - 2) / 2
    这是最后一个非叶子节点的下标!
    叶子节点没有子节点,天然满足大顶堆,不需要调整;
    只需要调整所有非叶子节点,从最后一个开始,效率最高。
  2. i >= 0
    从后往前,倒序遍历所有非叶子节点。
  3. AdjustHeap(arr, i, n)
    对每一个非叶子节点,执行向下堆化,保证每一棵子树都是大顶堆;
    遍历完成后,整个数组就是标准大顶堆。

4.3 排序交换循环 for (int i = n - 1; i > 0; i--)

c 复制代码
// 第二步:交换堆顶 + 反复堆化
for (int i = n - 1; i > 0; i--)
{
    // 堆顶最大值 和 无序区间末尾交换
    int tmp = arr[0];
    arr[0] = arr[i];
    arr[i] = tmp;

    // 对前i个无序元素重新堆化
    AdjustHeap(arr, 0, i);
}
  • 每轮把堆顶最大值和当前末尾 i 位置交换,最大值直接落户到有序区间;
  • 交换后破坏了堆结构,所以调用 AdjustHeap(arr, 0, i);
  • 只调整前 i 个元素,后面已经排好序的不再参与;
  • 从根节点 0 开始堆化,不用重新整棵树建堆,节省时间。

5. 复杂度与稳定性详细分析

5.1 时间复杂度

  • 初始建堆:O(n)
  • 每一次交换后堆化:单次 O(logn),共执行 n 次
  • 整体最好 / 最坏 / 平均:稳定 O(nlogn)

重点:不管数组原本有序、逆序、乱序,堆排序效率几乎不变,碾压 O(n2) 的插入、选择排序。

5.2 空间复杂度

仅使用 temp、parent、child 临时变量,没有开辟额外数组。
空间复杂度 O(1),属于原地排序。

6. 堆排序核心特点总结

  1. 底层依赖完全二叉树 + 大顶堆,理解下标父子关系是关键;
  2. 核心三步:建大顶堆 → 堆顶末尾交换 → 从根重新堆化;
  3. 时间稳定 O(nlogn)、空间 O(1),原地排序;
  4. 代码难点集中在 AdjustHeap 向下堆化初始建堆起点;
  5. 不适合小规模数据(常数开销大),适合大数据排序、面试手写、TopK 问题
  6. 不稳定排序,不适合要求保留重复元素相对顺序的场景。
相关推荐
洛水水几秒前
【数据结构】红黑树详解
数据结构·红黑树
炸膛坦客1 分钟前
嵌入式 - 数据结构与算法:(1-9)数据结构 - 队列(Queue)
c语言·数据结构
AbandonForce36 分钟前
哈希表(HashTable,散列表)个人理解
开发语言·数据结构·c++·散列表
代码中介商42 分钟前
栈结构完全指南:顺序栈实现精讲
c语言·开发语言·数据结构
样例过了就是过了1 小时前
LeetCode热题100 编辑距离
数据结构·c++·算法·leetcode·动态规划
khalil10202 小时前
代码随想录算法训练营Day-46 动态规划13 | 647. 回文子串、516.最长回文子序列、动态规划总结
数据结构·c++·算法·leetcode·动态规划·回文子串·回文子序列
Raink老师2 小时前
用100道题拿下你的算法面试(链表篇-4):合并 K 个有序链表
算法·链表·面试
风止何安啊2 小时前
手写 URL 解析器,面试官到底想考什么?
前端·javascript·面试
richard_yuu3 小时前
数据结构|二叉树层序遍历 & 线索二叉树:吃透二叉树进阶核心考点
数据结构
凯瑟琳.奥古斯特3 小时前
力扣2760 C++滑动窗口解法
数据结构·c++·算法·leetcode·职场和发展