一、堆的基本概念
1.1 什么是堆?生活中的比喻
想象一下你有一堆大小不一的苹果,现在要把它们整理成一种特殊的结构:
- 最大堆:最大的苹果永远在最上面,每个苹果都比它下面的所有苹果大
- 最小堆:最小的苹果永远在最上面,每个苹果都比它下面的所有苹果小
这就是堆的基本思想!堆是一种特殊的完全二叉树,它满足以下两个条件:
- 结构性质:它是一棵完全二叉树(除了最后一层,其他层都是满的,最后一层从左到右填充)
- 堆性质:每个节点的值都大于等于(或小于等于)其子节点的值
1.2 堆的类型
堆主要分为两种类型:
-
最大堆(Max Heap):
- 父节点的值 ≥ 子节点的值
- 根节点是堆中的最大值
- 像一个金字塔,塔尖是最大的
-
最小堆(Min Heap):
- 父节点的值 ≤ 子节点的值
- 根节点是堆中的最小值
- 像一个倒金字塔,塔尖是最小的
1.3 为什么需要堆?
堆解决了两个重要问题:
- 快速找到最大/最小值:O(1)时间复杂度
- 高效插入和删除:O(log n)时间复杂度
这就像一个智能的排队系统:
- 医院急诊室:最紧急的病人(最大值)优先处理
- 操作系统任务调度:优先级最高的任务先执行
二、堆的存储方式
2.1 数组表示法
虽然堆在概念上是树形结构,但在计算机中我们通常用数组来存储它!为什么?因为完全二叉树有很好的数学性质:
对于数组中索引为 i 的元素:
- 它的父节点索引 = (i - 1) / 2
- 它的左子节点索引 = 2 * i + 1
- 它的右子节点索引 = 2 * i + 2
示例:最大堆 [100, 80, 90, 50, 60, 70, 40]
数组表示:[100, 80, 90, 50, 60, 70, 40]
树形结构:
100
/ \
80 90
/ \ / \
50 60 70 40
2.2 为什么用数组存储?
- 空间效率:不需要存储指针,节省空间
- 缓存友好:连续内存访问,CPU缓存命中率高
- 计算简单:通过简单计算就能找到父子节点关系
三、堆的基本操作
3.1 堆化(Heapify)
堆化是堆的核心操作,它确保一个节点满足堆的性质。有两种堆化方式:
3.1.1 自顶向下堆化(向下调整)
当某个节点的值变小(最大堆)或变大(最小堆)时,需要向下调整:
void maxHeapify(vector<int>& heap, int size, int index) {
int largest = index; // 初始化最大值为当前节点
int left = 2 * index + 1; // 左子节点
int right = 2 * index + 2; // 右子节点
// 如果左子节点存在且大于当前最大值
if (left < size && heap[left] > heap[largest])
largest = left;
// 如果右子节点存在且大于当前最大值
if (right < size && heap[right] > heap[largest])
largest = right;
// 如果最大值不是当前节点,交换并继续堆化
if (largest != index) {
swap(heap[index], heap[largest]);
maxHeapify(heap, size, largest); // 递归堆化
}
}
过程比喻:就像一个家庭会议,如果孩子比家长更"重要"(值更大),就让"孩子"上位,然后继续检查这个"孩子"是否需要继续调整位置。
3.1.2 自底向上堆化(向上调整)
当在堆末尾插入新元素时,需要向上调整:
void heapifyUp(vector<int>& heap, int index) {
while (index > 0) {
int parent = (index - 1) / 2; // 父节点索引
// 如果当前节点大于父节点(最大堆),交换
if (heap[index] > heap[parent]) {
swap(heap[index], heap[parent]);
index = parent; // 继续向上比较
} else {
break; // 满足堆性质,停止
}
}
}
过程比喻:就像新员工入职,如果比上级能力强,就向上级挑战,赢了就交换位置,直到遇到更强的上级或成为CEO。
3.2 建堆(Build Heap)
将一个无序数组构建成堆,有两种方法:
3.2.1 逐个插入法(O(n log n))
void buildHeapSlow(vector<int>& heap) {
for (int i = 0; i < heap.size(); i++) {
heapifyUp(heap, i); // 对每个元素进行向上调整
}
}
3.2.2 Floyd建堆法(O(n))
更高效的方法是从最后一个非叶子节点开始,向前进行向下调整:
void buildHeap(vector<int>& heap) {
// 从最后一个非叶子节点开始(最后一个节点的父节点)
for (int i = (heap.size() / 2) - 1; i >= 0; i--) {
maxHeapify(heap, heap.size(), i);
}
}
为什么Floyd方法更快?
- 大部分节点都在堆的底部,向下调整的路径很短
- 时间复杂度从O(n log n)降到O(n),这是堆的"魔法"之一
3.3 插入操作(Insert)
- 将新元素添加到数组末尾
- 进行向上调整(heapifyUp)
void insert(vector<int>& heap, int value) {
heap.push_back(value); // 添加到末尾
heapifyUp(heap, heap.size() - 1); // 向上调整
}
过程比喻:新成员加入团队,从最底层开始,如果能力比上级强,就不断向上晋升。
3.4 删除堆顶元素(Extract Max/Min)
- 保存堆顶元素(要返回的值)
- 将最后一个元素移到堆顶
- 进行向下调整(heapifyDown)
int extractMax(vector<int>& heap) {
if (heap.empty()) {
throw runtime_error("Heap is empty!");
}
int max = heap[0]; // 保存最大值
heap[0] = heap.back(); // 将最后一个元素移到堆顶
heap.pop_back(); // 删除最后一个元素
maxHeapify(heap, heap.size(), 0); // 向下调整
return max;
}
过程比喻:CEO离职,让最底层的员工临时接替,然后这个临时CEO需要和下属比较,如果下属更强,就让位,直到找到合适的位置。
四、堆的应用
4.1 优先队列(Priority Queue)
堆最经典的应用就是实现优先队列:
- 医院急诊系统:根据病情严重程度(优先级)安排治疗顺序
- 操作系统任务调度:高优先级任务先执行
- 事件驱动模拟:最早发生的事件先处理
// 使用C++标准库的优先队列(默认最大堆)
#include <queue>
priority_queue<int> maxHeap; // 最大堆
priority_queue<int, vector<int>, greater<int>> minHeap; // 最小堆
// 操作示例
maxHeap.push(30);
maxHeap.push(10);
maxHeap.push(50);
cout << maxHeap.top(); // 输出50
maxHeap.pop(); // 删除50
4.2 堆排序(Heap Sort)
堆排序是一种高效的排序算法,时间复杂度O(n log n):
void heapSort(vector<int>& arr) {
// 1. 构建最大堆
buildHeap(arr);
// 2. 逐个提取最大元素
for (int i = arr.size() - 1; i > 0; i--) {
swap(arr[0], arr[i]); // 将当前最大值移到末尾
maxHeapify(arr, i, 0); // 调整剩余元素为堆
}
}
过程比喻:
- 先把所有人按能力排成金字塔(建堆)
- 让塔尖的人(最强)站到队伍最后
- 重新组织剩下的人,让新的最强者到塔尖
- 重复直到所有人都排好序
4.3 其他应用
- Top K问题:快速找出数组中最大的K个元素
- 中位数查找:使用一个最大堆和一个最小堆
- Dijkstra算法:使用最小堆优化最短路径查找
- 哈夫曼编码:构建最优二叉树
五、手动实现一个堆类
下面是一个完整的最大堆实现:
#include <vector>
#include <algorithm>
#include <stdexcept>
using namespace std;
class MaxHeap {
private:
vector<int> heap;
// 向上调整
void heapifyUp(int index) {
while (index > 0) {
int parent = (index - 1) / 2;
if (heap[index] > heap[parent]) {
swap(heap[index], heap[parent]);
index = parent;
} else {
break;
}
}
}
// 向下调整
void heapifyDown(int index) {
int size = heap.size();
while (true) {
int left = 2 * index + 1;
int right = 2 * index + 2;
int largest = index;
if (left < size && heap[left] > heap[largest])
largest = left;
if (right < size && heap[right] > heap[largest])
largest = right;
if (largest != index) {
swap(heap[index], heap[largest]);
index = largest;
} else {
break;
}
}
}
public:
// 插入元素
void push(int value) {
heap.push_back(value);
heapifyUp(heap.size() - 1);
}
// 删除堆顶元素
void pop() {
if (heap.empty()) {
throw runtime_error("Heap is empty!");
}
heap[0] = heap.back();
heap.pop_back();
if (!heap.empty()) {
heapifyDown(0);
}
}
// 获取堆顶元素
int top() const {
if (heap.empty()) {
throw runtime_error("Heap is empty!");
}
return heap[0];
}
// 检查堆是否为空
bool empty() const {
return heap.empty();
}
// 获取堆的大小
size_t size() const {
return heap.size();
}
// 构建堆
void buildHeap(const vector<int>& arr) {
heap = arr;
for (int i = (heap.size() / 2) - 1; i >= 0; i--) {
heapifyDown(i);
}
}
};
六、堆的时间复杂度分析
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 建堆 | O(n) | Floyd算法 |
| 插入 | O(log n) | 只需要向上调整 |
| 删除堆顶 | O(log n) | 只需要向下调整 |
| 获取堆顶元素 | O(1) | 直接访问数组第一个元素 |
| 堆排序 | O(n log n) | 建堆O(n) + n次删除O(log n) |
为什么建堆是O(n)而不是O(n log n)?
- 大部分节点都在堆的底部,向下调整的路径很短
- 数学证明:总操作次数 ≈ n - log₂(n+1) ≈ O(n)
七、堆的常见问题和优化
7.1 常见问题
-
堆溢出:当堆的大小超过系统限制
- 解决方案:使用动态数组(如vector)
-
重复元素:堆可以处理重复元素,但需要明确定义比较规则
-
稳定性:堆排序不是稳定排序(相同值的元素相对顺序可能改变)
7.2 优化技巧
- 使用迭代代替递归:避免递归深度过大
// 迭代版本的heapifyDown
void heapifyDown(int index) {
int size = heap.size();
while (true) {
int left = 2 * index + 1;
int right = 2 * index + 2;
int largest = index;
if (left < size && heap[left] > heap[largest])
largest = left;
if (right < size && heap[right] > heap[largest])
largest = right;
if (largest != index) {
swap(heap[index], heap[largest]);
index = largest;
} else {
break;
}
}
}
-
批量建堆:直接使用Floyd算法,比逐个插入更高效
-
使用更高效的数据结构:对于特定场景,可以考虑斐波那契堆等
7.3 堆的变种
- 二项堆(Binomial Heap):支持更高效的合并操作
- 斐波那契堆(Fibonacci Heap):理论上有更好的平摊时间复杂度
- d-ary堆:每个节点有d个子节点,减少堆的高度
八、C++标准库中的堆
8.1 std::priority_queue
C++标准库提供了优先队列容器适配器:
#include <queue>
// 默认最大堆
priority_queue<int> maxHeap;
// 最小堆
priority_queue<int, vector<int>, greater<int>> minHeap;
// 自定义比较函数(最大堆)
auto cmp = [](int a, int b) { return a < b; };
priority_queue<int, vector<int>, decltype(cmp)> customHeap(cmp);
8.2 堆算法
C++算法库提供了堆操作函数:
#include <algorithm>
vector<int> vec = {3, 1, 4, 1, 5, 9};
// 建堆(默认最大堆)
make_heap(vec.begin(), vec.end());
// 插入元素
vec.push_back(6);
push_heap(vec.begin(), vec.end());
// 删除堆顶
pop_heap(vec.begin(), vec.end());
vec.pop_back();
// 检查是否是堆
bool isHeap = is_heap(vec.begin(), vec.end());
// 排序堆(变成有序序列)
sort_heap(vec.begin(), vec.end());
九、堆的实际应用案例
9.1 合并K个有序数组
vector<int> mergeKSortedArrays(vector<vector<int>>& arrays) {
// 最小堆,存储元素值和来自哪个数组
priority_queue<pair<int, pair<int, int>>,
vector<pair<int, pair<int, int>>>,
greater<pair<int, pair<int, int>>>> minHeap;
// 初始化堆,放入每个数组的第一个元素
for (int i = 0; i < arrays.size(); i++) {
if (!arrays[i].empty()) {
minHeap.push({arrays[i][0], {i, 0}});
}
}
vector<int> result;
while (!minHeap.empty()) {
auto current = minHeap.top();
minHeap.pop();
result.push_back(current.first);
int arrayIndex = current.second.first;
int elementIndex = current.second.second;
// 如果当前数组还有下一个元素,放入堆中
if (elementIndex + 1 < arrays[arrayIndex].size()) {
minHeap.push({arrays[arrayIndex][elementIndex + 1],
{arrayIndex, elementIndex + 1}});
}
}
return result;
}
9.2 查找数据流中的中位数
class MedianFinder {
private:
priority_queue<int> maxHeap; // 存储较小的一半
priority_queue<int, vector<int>, greater<int>> minHeap; // 存储较大的一半
public:
void addNum(int num) {
if (maxHeap.empty() || num <= maxHeap.top()) {
maxHeap.push(num);
} else {
minHeap.push(num);
}
// 平衡两个堆的大小
if (maxHeap.size() > minHeap.size() + 1) {
minHeap.push(maxHeap.top());
maxHeap.pop();
} else if (minHeap.size() > maxHeap.size()) {
maxHeap.push(minHeap.top());
minHeap.pop();
}
}
double findMedian() {
if (maxHeap.size() == minHeap.size()) {
return (maxHeap.top() + minHeap.top()) / 2.0;
} else {
return maxHeap.top();
}
}
};
十、总结
10.1 关键要点回顾
- 堆的本质:一种特殊的完全二叉树,满足堆性质
- 核心操作:
- 堆化(向上调整和向下调整)
- 建堆(Floyd算法O(n))
- 插入和删除(O(log n))
- 存储方式:用数组存储,通过索引计算父子关系
- 主要应用:
- 优先队列
- 堆排序
- Top K问题
- 中位数查找
10.2 学习建议
- 动手实现:亲自编写堆的各个操作,加深理解
- 可视化工具:使用可视化工具观察堆的调整过程
- 实际应用:尝试用堆解决实际问题,如合并有序数组
- 性能测试:比较不同建堆方法的性能差异
10.3 进阶方向
- 高级堆结构:学习二项堆、斐波那契堆等
- 并行堆:研究多线程环境下的堆操作
- 外部堆:处理无法全部装入内存的大数据
- 持久化堆:支持历史版本查询的堆结构
堆是计算机科学中最重要的数据结构之一,它不仅理论优美,而且应用广泛。掌握了堆,你就掌握了解决许多实际问题的强大工具!
728

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



