排序---堆排序(Heap Sort)

一、堆的基础概念

详细的堆讲解请参阅这篇文章数据结构---堆

堆排序是一种基于堆(Heap)数据结构的排序算法,其核心思想是利用堆的特性实现高效排序。在讲解堆排序前,需先明确堆的定义与性质:

  • 堆的定义:堆是一种完全二叉树(除最后一层外,每一层都被完全填充,且最后一层的节点靠左排列),且满足"堆序性":

    • 大顶堆:每个父节点的值 ≥ 其左右子节点的值(根节点为最大值)。
    • 小顶堆:每个父节点的值 ≤ 其左右子节点的值(根节点为最小值)。
  • 堆的存储 :堆通常用数组实现(无需显式构建二叉树)。对于数组中索引为 i 的节点:

    • 左子节点索引:2i + 1
    • 右子节点索引:2i + 2
    • 父节点索引:(i - 1) // 2(整数除法)

例如,数组 [10, 5, 3, 4, 1] 可表示为大顶堆,其结构如下:

复制代码
      10
    /   \
   5     3
  / \
 4   1
二、堆排序的核心思想

堆排序的基本流程分为三步:

  1. 建堆:将待排序数组转换为大顶堆(或小顶堆,此处以大顶堆为例)。
  2. 排序
    • 交换堆顶(最大值)与堆尾元素,此时最大值已放到正确位置。
    • 缩小堆的范围(排除已排序的堆尾元素),重新调整剩余元素为大顶堆。
  3. 重复:重复步骤2,直到所有元素排序完成。

其本质是通过反复提取堆中的最大值(或最小值),实现整体有序。

三、关键操作:堆的调整(下沉操作)

堆排序的核心是"调整堆"的操作(也称"下沉"),即当堆的结构被破坏时,通过将节点向下移动,恢复堆的性质。

调整堆的步骤(以大顶堆为例):

  1. 设当前节点索引为 i,堆的有效范围为 [0, len)len 为当前堆的大小)。
  2. 找到节点 i 的左子节点(left = 2i + 1)和右子节点(right = 2i + 2)。
  3. ileftright 中找到值最大的节点,记为 max_idx
  4. max_idx ≠ i,交换 imax_idx 的值,此时 max_idx 位置的堆结构可能被破坏,需递归(或迭代)调整 max_idx 位置。
四、堆排序的完整步骤

以数组 [4, 10, 3, 5, 1] 为例,演示堆排序的全过程:

步骤1:建堆(构建大顶堆)

建堆需从最后一个非叶子节点开始,依次向前调整每个节点,确保每个子树都是大顶堆。

  • 数组长度 n = 5,最后一个非叶子节点索引为 (n//2 - 1) = 1(对应值为10)。

    • 调整索引1(值10):其左右子节点为3(值5)和4(值1),10已是最大值,无需调整。
    • 调整索引0(值4):其左右子节点为1(值10)和2(值3)。最大值为10(索引1),交换4和10,数组变为 [10, 4, 3, 5, 1]。交换后需检查索引1的子树:其左子节点4(值5)大于4,交换4和5,数组变为 [10, 5, 3, 4, 1],此时堆结构恢复。

    建堆完成后,数组为 [10, 5, 3, 4, 1](大顶堆)。

步骤2:排序过程
  • 第1轮

    • 交换堆顶(10)与堆尾(1),数组变为 [1, 5, 3, 4, 10](10已排序)。
    • 缩小堆范围至 [0, 3],调整堆顶(1):
      1. 子节点为1(5)和2(3),最大值为5(索引1),交换1和5 → [5, 1, 3, 4, 10]
      2. 调整索引1(1):子节点为3(4),交换1和4 → [5, 4, 3, 1, 10]
  • 第2轮

    • 交换堆顶(5)与堆尾(1),数组变为 [1, 4, 3, 5, 10](5已排序)。
    • 缩小堆范围至 [0, 2],调整堆顶(1):
      1. 子节点为1(4)和2(3),最大值为4(索引1),交换1和4 → [4, 1, 3, 5, 10]
  • 第3轮

    • 交换堆顶(4)与堆尾(3),数组变为 [3, 1, 4, 5, 10](4已排序)。
    • 缩小堆范围至 [0, 1],调整堆顶(3):
      1. 子节点为1(1),3已是最大值,无需调整。
  • 第4轮

    • 交换堆顶(3)与堆尾(1),数组变为 [1, 3, 4, 5, 10](3已排序)。

最终排序结果:[1, 3, 4, 5, 10]

五、C++实现代码

以下是堆排序的C++实现,包含堆调整、建堆和排序主函数:

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

// 调整堆(大顶堆):将以i为根的子树调整为大顶堆
void adjustHeap(vector<int>& arr, int i, int len) {
    int temp = arr[i];  // 保存当前节点值
    // 从左子节点开始遍历
    for (int k = 2 * i + 1; k < len; k = 2 * k + 1) {
        // 若右子节点存在且大于左子节点,选择右子节点
        if (k + 1 < len && arr[k] < arr[k + 1]) {
            k++;
        }
        // 若子节点大于父节点,交换并继续调整子树
        if (arr[k] > temp) {
            arr[i] = arr[k];
            i = k;  // 记录交换后的位置,继续向下调整
        } else {
            break;  // 父节点已是最大值,无需继续
        }
    }
    arr[i] = temp;  // 将原节点值放到最终位置
}

// 堆排序主函数
void heapSort(vector<int>& arr) {
    int n = arr.size();
    // 1. 建堆:从最后一个非叶子节点向前调整
    for (int i = n / 2 - 1; i >= 0; i--) {
        adjustHeap(arr, i, n);
    }
    // 2. 排序:反复交换堆顶与堆尾,调整堆
    for (int j = n - 1; j > 0; j--) {
        swap(arr[0], arr[j]);  // 交换堆顶(最大值)与堆尾
        adjustHeap(arr, 0, j);  // 调整剩余j个元素为大顶堆
    }
}

// 测试函数
int main() {
    vector<int> arr = {4, 10, 3, 5, 1};
    cout << "排序前:";
    for (int num : arr) {
        cout << num << " ";
    }
    cout << endl;

    heapSort(arr);

    cout << "排序后:";
    for (int num : arr) {
        cout << num << " ";
    }
    cout << endl;

    return 0;
}
六、算法分析
  1. 时间复杂度

    • 建堆:需调整 n/2 个节点,每个节点的调整深度为 O(log n),总时间 O(n)(数学推导可证明)。
    • 排序:共 n-1 次调整,每次调整时间 O(log n),总时间 O(n log n)
    • 整体时间复杂度:O(n log n)(最好、最坏、平均情况均为此值,稳定性优于快速排序)。
  2. 空间复杂度O(1),仅使用常数级额外空间(原地排序)。

  3. 稳定性 :不稳定排序。例如 [2, 2, 1] 排序时,两个2的相对位置可能变化。

堆排序的应用场景
  • Top K问题 :需从大量数据中找出前K个最大/小值时,堆排序可高效实现(时间 O(n log K))。
  • 内存受限场景:因原地排序特性,适合内存紧张的环境。
  • 实时数据处理:可动态维护堆结构,支持高效插入和删除操作。
与其他排序算法的对比
算法 时间复杂度(平均) 空间复杂度 稳定性
堆排序 O(n log n) O(1) 不稳定
快速排序 O(n log n) O(log n) 不稳定
归并排序 O(n log n) O(n) 稳定

堆排序的优势在于最坏时间复杂度稳定且空间开销小,但实际性能略逊于快速排序(因缓存局部性较差)。


堆排序是一种基于堆结构的高效排序算法,通过建堆和反复调整堆实现排序,具有 O(n log n) 的稳定时间复杂度和 O(1) 的空间复杂度。其核心是堆的调整操作,理解这一过程是掌握堆排序的关键。在需要稳定性能和低空间开销的场景中,堆排序是理想选择。

相关推荐
失忆已成习惯.37 分钟前
西农数据结构第四次实习题目参考
数据结构·算法·图论
yesyesido44 分钟前
3D在线魔方模拟器
科技·算法·3d·生活·业界资讯·交友·帅哥
王老师青少年编程1 小时前
线性DP第12课:线性DP应用案例实践:数字三角形
c++·动态规划·dp·线性dp·csp·信奥赛·数字三角形
是苏浙1 小时前
蓝桥杯备战day1
算法
汉克老师1 小时前
CCF-NOI2025第一试题目与解析(第二题、序列变换(sequence))
c++·算法·动态规划·noi
hweiyu001 小时前
数据结构:字典
数据结构
A charmer1 小时前
内存泄漏、死锁:定位排查工具+解决方案(C/C++ 实战指南)
c语言·开发语言·c++
断剑zou天涯1 小时前
【算法笔记】KMP算法
java·笔记·算法
程序员东岸1 小时前
《数据结构——排序(下)》分治与超越:快排、归并与计数排序的终极对决
数据结构·c++·经验分享·笔记·学习·算法·排序算法