引言
在 8 种基于比较的排序算法里,堆排序(Heap Sort)是唯一一个同时满足“原地”+“O(n log n) 最坏”+“不依赖递归栈深度”的“三好学生”。它既不像快排那样有 O(n²) 的暴脾气,也不像归并那样需要额外内存。优先队列、Top-K、定时器、任务调度器……只要涉及“动态极值”,底层都站着一棵二叉堆。今天,我们就用一份可编译、可扩展、仅头文件的 C++ 实现,把“堆”从原理到寄存器,一口气扒光。
算法原理详解
- 什么是二叉堆?
完全二叉树 + 父节点 ≥ 子节点(最大堆)。用数组存储时,节点 i 的左右子节点分别是 2i+1、2i+2,父节点是 (i-1)/2。 - 排序总纲(伪代码)
HeapSort(A):
BuildMaxHeap(A) // 把无序数组“堆化”
for end ← len(A)-1 downto 1:
swap A[0], A[end] // 最大值丢到末尾
HeapSize ← HeapSize-1
MaxHeapify(A, 0) // 新堆顶下沉,恢复堆序 - 关键子过程
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/2 | O(log n) 但常数极小 | O(n) |
| 摘顶 | for (i = size-1; ...) | n-1 | O(log n) | O(n log n) |
| 空间 | 无 new/malloc,仅递归栈 | 1 | O(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);
脑海里应该立刻闪现三幅图:
- 最后一棵非叶子树开始“下沉”;
- 根与末尾交换,最大值被“摘下”;
- 新根一路沉到属于自己的位置。
代码不再是黑箱,而是可视化的“堆山摘顶”流水线。
把它拷贝进你的工程,让最大值像旗杆一样被拔出,再稳稳插到数组末尾——排序,也可以这么有仪式感。
#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);
}
}
};
1745

被折叠的 条评论
为什么被折叠?



