从“堆”里拔出最大值:C++手写 HeapSort 的完全指南

引言

在 8 种基于比较的排序算法里,堆排序(Heap Sort)是唯一一个同时满足“原地”+“O(n log n) 最坏”+“不依赖递归栈深度”的“三好学生”。它既不像快排那样有 O(n²) 的暴脾气,也不像归并那样需要额外内存。优先队列、Top-K、定时器、任务调度器……只要涉及“动态极值”,底层都站着一棵二叉堆。今天,我们就用一份可编译、可扩展、仅头文件的 C++ 实现,把“堆”从原理到寄存器,一口气扒光。

算法原理详解

  1. 什么是二叉堆?
    完全二叉树 + 父节点 ≥ 子节点(最大堆)。用数组存储时,节点 i 的左右子节点分别是 2i+1、2i+2,父节点是 (i-1)/2。
  2. 排序总纲(伪代码)
    HeapSort(A):
    BuildMaxHeap(A) // 把无序数组“堆化”
    for end ← len(A)-1 downto 1:
    swap A[0], A[end] // 最大值丢到末尾
    HeapSize ← HeapSize-1
    MaxHeapify(A, 0) // 新堆顶下沉,恢复堆序
  3. 关键子过程
    BuildMaxHeap:自底向上“下沉”,复杂度 O(n)。
    MaxHeapify:从给定节点向下“沉”,树高 log n,复杂度 O(log n)。
    整个算法没有分治,也没有动态规划,只有“下沉”这一招,却足以让最大元素像“气泡”一样一次次浮到顶端又被摘走。

C++ 实现逐行解析

下面把 HeapSort.hpp 拆成 4 段,每段先贴关键源码,再逐句拆解

① 类壳与接口设计

class HeapSort {
public:
    template<typename T>
    static void sort(T arr[], int size);

    template<typename T>
    static void sort(std::vector<T>& vec);

这两行是策略模式的极简体现:

  • 统一入口,支持原生数组和 std::vector零拷贝重载
  • 模板参数 T 只需满足 operator>不依赖 STL 迭代器概念,嵌入式环境也能用。
  • 全部 static,禁止实例化,工具类最干净的做法

② BuildMaxHeap —— 把“乱树”变“堆山”

for (int i = size / 2 - 1; i >= 0; --i)
    heapify(arr, size, i);
  • size / 2 - 1最后一个非叶节点的索引。
  • 从它开始自底向上下沉,保证每次 heapify 时,左右子树已经是合法最大堆。
  • 这段循环的总比较次数 < 2n,复杂度 O(n)——不是直觉上的 O(n log n),记住这个结论,面试常问。

③ MaxHeapify —— 下沉“叛徒”节点

int largest = root;
int left  = 2 * root + 1;
int right = 2 * root + 2;

if (left < size && arr[left] > arr[largest]) largest = left;
if (right < size && arr[right] > arr[largest]) largest = right;

if (largest != root) {
    std::swap(arr[root], arr[largest]);
    heapify(arr, size, largest);   // 递归
}
  • 假设根最大,再让左右孩子“打擂台”,三选一冠军
  • 如果冠军换人,就把原根“踢下去”,递归处理被踢分支
  • 这里用 std::swap 而非手写字母临时变量,生成器会帮你优化成 xchg 指令,比手写更快。
  • 递归深度 ≤ 树高,n = 1e6 时仅 20 层,栈空间 < 1 KB,嵌入式放心用
  • 若想彻底零递归,可把 heapify 改成 while (true),手动更新 root = largest; 即可。

④ Sort 主体 —— “摘顶”循环

for (int i = size - 1; i > 0; --i) {
    std::swap(arr[0], arr[i]);   // 最大值归位
    heapify(arr, i, 0);          // 剩余 [0, i-1] 继续堆化
}
  • 每次把堆顶(全局最大)与当前末尾交换,堆大小 i 减 1。
  • 被换到根的那个元素可能破坏堆序,于是调用 heapify 把它“沉下去”。
  • 循环 n-1 次,每次下沉 O(log n),总时间 O(n log n)
  • 注意 heapify 的第二个参数是 i 而不是 size防止已排好序的尾部再被比较,这是常见越界 bug 源头

⑤ vector 特供版 —— 零成本抽象

template<typename T>
static void sort(std::vector<T>& vec) {
    int size = vec.size();
    for (int i = size / 2 - 1; i >= 0; --i)
        heapify(vec, size, i);
    ...
}
  • 实现与数组版本一字不改,只是形参从 T* 换成 vector<T>&
  • 传引用避免拷贝;size 提前存到局部变量,省去每轮调用 vec.size() 的微小开销
  • 如果你想让算法泛化到任意随机访问容器,再套一层 template<Container> 即可,迭代器只需支持 operator[]

⑥ 打印调试 —— 非侵入式观察

template<typename T>
static void printArray(const T arr[], int size) {
    for (int i = 0; i < size; ++i) {
        std::cout << arr[i] << (i + 1 < size ? ", " : "\n");
    }
}
  • 三元运算符省一行 if编译器会生成无分支代码,缓存友好。
  • 函数标记为 static不占用全局符号,链接时可被优化掉,零成本调试

复杂度再回顾(结合代码)

阶段源码行次数单次代价总代价
建堆heapify 循环n/2O(log n) 但常数极小O(n)
摘顶for (i = size-1; ...)n-1O(log n)O(n log n)
空间无 new/malloc,仅递归栈1O(log n)O(log n)

实战小灶:把代码嵌进你的项目

#include "HeapSort.hpp"
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v{9, 4, 7, 1, -2, 8, 3};
    HeapSort::sort(v);                    // 一键排序
    HeapSort::printVector(v);             // 验证结果
}
  • 仅需 一个头文件零依赖(除了 <iostream> 用于打印)。
  • C++17/20 下编译,模板实推导依旧兼容 C++11,老项目无缝升级

结语

现在,当你再看到

HeapSort::sort(arr, n);

脑海里应该立刻闪现三幅图:

  1. 最后一棵非叶子树开始“下沉”;
  2. 根与末尾交换,最大值被“摘下”;
  3. 新根一路沉到属于自己的位置。

代码不再是黑箱,而是可视化的“堆山摘顶”流水线。
把它拷贝进你的工程,让最大值像旗杆一样被拔出,再稳稳插到数组末尾——排序,也可以这么有仪式感

#include <iostream>
#include <vector>
#include <algorithm>

/**
 * @class HeapSort
 * @brief 实现堆排序算法的类
 * 
 * 这个类提供了使用堆排序算法对数组进行排序的方法,
 * 堆排序是一种基于比较的排序算法,时间复杂度为 O(n log n)。
 * 
 * 堆排序的基本思想:
 * 1. 构建最大堆:将待排序的数组构建成一个最大堆,使得每个父节点的值都大于或等于其子节点的值
 * 2. 排序过程:
 *    - 将堆顶元素(最大值)与最后一个元素交换
 *    - 减少堆的大小,并对新的堆顶元素进行堆化调整
 *    - 重复上述过程直到堆的大小为1
 * 
 * 堆排序的特点:
 * - 原地排序:不需要额外的存储空间(除了递归栈空间)
 * - 不稳定排序:相同元素的相对位置可能会改变
 * - 时间复杂度:最好、最坏、平均情况都是 O(n log n)
 */
class HeapSort {
public:
    /**
     * @brief 使用堆排序算法对数组进行排序(升序)
     * @tparam T 数组中元素的类型(必须支持比较运算符)
     * @param arr 要排序的数组
     * @param size 数组的大小
     */
    template<typename T>
    static void sort(T arr[], int size) {
        if (size <= 1) return;
        
        // 第一步:构建最大堆
        // 从最后一个非叶子节点开始,自底向上进行堆化
        for (int i = size / 2 - 1; i >= 0; i--) {
            heapify(arr, size, i);
        }
        
        // 第二步:逐个提取堆顶元素
        // 将堆顶元素(最大值)与当前堆的最后一个元素交换
        // 然后减少堆的大小,并对新的堆顶元素进行堆化
        for (int i = size - 1; i > 0; i--) {
            // 将当前堆顶元素(最大值)与最后一个元素交换
            std::swap(arr[0], arr[i]);
            
            // 对剩余元素重新进行堆化,堆的大小减1
            heapify(arr, i, 0);
        }
    }

    /**
     * @brief 使用堆排序算法对向量进行排序(升序)
     * @tparam T 向量中元素的类型(必须支持比较运算符)
     * @param vec 要排序的向量
     */
    template<typename T>
    static void sort(std::vector<T>& vec) {
        if (vec.size() <= 1) return;
        
        int size = vec.size();
        
        // 第一步:构建最大堆
        // 最后一个非叶子节点开始,从右至左、从下至上地进行调整
        for (int i = size / 2 - 1; i >= 0; i--) {
            heapify(vec, size, i);
        }
        
        // 第二步:逐个提取堆顶元素
        for (int i = size - 1; i > 0; i--) {
            std::swap(vec[0], vec[i]);
            heapify(vec, i, 0);
        }
    }

    /**
     * @brief 打印数组的元素
     * @tparam T 数组中元素的类型
     * @param arr 要打印的数组
     * @param size 数组的大小
     */
    template<typename T>
    static void printArray(const T arr[], int size) {
        for (int i = 0; i < size; i++) {
            std::cout << arr[i];
            if (i < size - 1) std::cout << ", ";
        }
        std::cout << std::endl;
    }

    /**
     * @brief 打印向量的元素
     * @tparam T 向量中元素的类型
     * @param vec 要打印的向量
     */
    template<typename T>
    static void printVector(const std::vector<T>& vec) {
        for (size_t i = 0; i < vec.size(); i++) {
            std::cout << vec[i];
            if (i < vec.size() - 1) std::cout << ", ";
        }
        std::cout << std::endl;
    }

private:
    /**
     * @brief 对数组进行堆化调整(最大堆)
     * @tparam T 元素的类型
     * @param arr 要堆化的数组
     * @param size 当前堆的大小
     * @param root 当前要堆化的根节点索引
     * 
     * 堆化过程:
     * 1. 假设当前根节点是最大值
     * 2. 比较根节点与其左右子节点,找到三者中的最大值
     * 3. 如果最大值不是根节点,则交换根节点与最大值节点
     * 4. 递归地对被交换的子节点进行堆化
     */
    template<typename T>
    static void heapify(T arr[], int size, int root) {
        int largest = root;        // 假设根节点是最大值
        int left = 2 * root + 1;   // 左子节点索引
        int right = 2 * root + 2;  // 右子节点索引
        
        // 如果左子节点存在且大于当前最大值
        if (left < size && arr[left] > arr[largest]) {
            largest = left;
        }
        
        // 如果右子节点存在且大于当前最大值
        if (right < size && arr[right] > arr[largest]) {
            largest = right;
        }
        
        // 如果最大值不是根节点,则交换并递归堆化
        if (largest != root) {
            std::swap(arr[root], arr[largest]);
            
            // 递归地对被交换的子节点进行堆化
            heapify(arr, size, largest);
        }
    }

    /**
     * @brief 对向量进行堆化调整(最大堆)
     * @tparam T 元素的类型
     * @param vec 要堆化的向量
     * @param size 当前堆的大小
     * @param root 当前要堆化的根节点索引
     */
    template<typename T>
    static void heapify(std::vector<T>& vec, int size, int root) {
        int largest = root;        // 假设根节点是最大值
        int left = 2 * root + 1;   // 左子节点索引
        int right = 2 * root + 2;  // 右子节点索引
        
        // 如果左子节点存在且大于当前最大值
        if (left < size && vec[left] > vec[largest]) {
            largest = left;
        }
        
        // 如果右子节点存在且大于当前最大值
        if (right < size && vec[right] > vec[largest]) {
            largest = right;
        }
        
        // 如果最大值不是根节点,则交换并递归堆化
        if (largest != root) {
            std::swap(vec[root], vec[largest]);
            
            // 递归地对被交换的子节点进行堆化
            heapify(vec, size, largest);
        }
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值