Heap
堆
堆是一种基于完全二叉树的基础数据结构,其优化目标是高效访问"极值"元素(最大值或最小值)。堆是堆排序、优先队列等算法的核心,且结构简洁,是编码面试与实际应用中的必备知识点。本文将拆解堆的基础原理,涵盖大根堆与小根堆,并提供可复用的C++实现代码。
1. 什么是堆?
堆由两个核心性质定义:
-
完全二叉树性质:除最后一层外,所有层均被完全填满;最后一层的节点从左到右依次排列(左对齐)。这一性质使得堆可通过数组高效存储(无需指针)。
-
堆序性质:用于约束父子节点间关系的严格规则,据此可将堆分为两类:
- 大根堆(Max-Heap):对任意节点\(u\),其值大于或等于其左右子节点的值。形式化表示为:
\(value(u) \geq value(\text{u的左子节点}) \quad 且 \quad value(u) \geq value(\text{u的右子节点})\)
堆的根节点存储堆中的最大值。 - 小根堆(Min-Heap):对任意节点\(u\),其值小于或等于其左右子节点的值。形式化表示为:
\(value(u) \leq value(\text{u的左子节点}) \quad 且 \quad value(u) \leq value(\text{u的右子节点})\)
堆的根节点存储堆中的最小值。
- 大根堆(Max-Heap):对任意节点\(u\),其值大于或等于其左右子节点的值。形式化表示为:
2. 堆的数组表示法
由于堆是完全二叉树,可采用1-基索引 将其存储在数组中(该方式能简化父子节点索引的计算)。对于索引为\(i\)的节点:
- 左子节点索引:\(2i\)
- 右子节点索引:\(2i + 1\)
- 父节点索引:\(\lfloor i/2 \rfloor\)
3. 堆的核心操作
堆的所有操作均依赖两个辅助函数,用于在堆结构修改后恢复堆序性质:down()(将节点向下调整)和 up()(将节点向上调整)。以下分别针对大根堆与小根堆进行说明。
3.1 辅助函数1:down(int u)——向下调整节点以修复堆序
当节点因"值过小"(大根堆中)或"值过大"(小根堆中)违反堆序性质时,使用 down()函数。该函数会将节点与合适的子节点交换,并递归调整,直至堆序性质恢复。
大根堆的 down()逻辑:
- 初始化\(\text{largest} = u\)(用于追踪\(u\)及其子节点中值最大的节点索引)。
- 若左子节点存在(\(2i \leq\) 堆大小)且左子节点值大于\(\text{largest}\)对应的节点值,则将\(\text{largest}\)更新为左子节点索引。
- 若右子节点存在(\(2i + 1 \leq\) 堆大小)且右子节点值大于\(\text{largest}\)对应的节点值,则将\(\text{largest}\)更新为右子节点索引。
- 若\(\text{largest} \neq u\)(说明\(u\)不是最大值节点),交换\(u\)与\(\text{largest}\)对应的节点值,并调用
down(largest)继续调整新位置的节点。
小根堆的 down()逻辑:
将"追踪最大值"改为"追踪最小值",比较逻辑变为"小于":
- 初始化\(\text{smallest} = u\)(用于追踪\(u\)及其子节点中值最小的节点索引)。
- 若左子节点存在且值小于\(\text{smallest}\)对应的节点值,更新\(\text{smallest}\)为左子节点索引。
- 若右子节点存在且值小于\(\text{smallest}\)对应的节点值,更新\(\text{smallest}\)为右子节点索引。
- 若\(\text{smallest} \neq u\),交换节点值并递归调用
down(smallest)。
3.2 辅助函数2:up(int u)——向上调整节点以修复堆序
当节点因"值过大"(大根堆中)或"值过小"(小根堆中)违反堆序性质时,使用 up()函数。该函数会将节点与父节点交换,并重复调整,直至堆序性质恢复。
大根堆的 up()逻辑:
当节点存在父节点(\(u > 1\),因采用1基索引,根节点索引为1)且节点值大于父节点值时,循环执行:
- 交换当前节点与父节点的值。
- 将\(u\)更新为父节点的索引(\(u = \lfloor u/2 \rfloor\))。
小根堆的 up()逻辑:
当节点存在父节点(\(u > 1\))且节点值小于父节点值时,循环执行:
- 交换当前节点与父节点的值。
- 将\(u\)更新为父节点的索引(\(u = \lfloor u/2 \rfloor\))。
3.3 堆的关键操作(大根堆 vs 小根堆)
所有操作的时间复杂度均为\(O(\log n)\),因为操作仅需遍历堆的高度(含\(n\)个节点的完全二叉树高度为\(\log n\))。
| 操作 | 大根堆操作步骤 | 小根堆操作步骤 |
|---|---|---|
push(x) |
1. 将\(x\)追加到数组末尾(a[ ++ s] = x,\(s\)为堆大小)。 2. 调用 up(s)修复堆序。 |
1. 将\(x\)追加到数组末尾(a[ ++ s] = x)。 2. 调用 up(s)修复堆序。 |
pop() |
1. 交换根节点(\(a[1]\))与最后一个节点(\(a[s]\))。 2. 缩小堆大小(s -- )。 3. 调用 down(1)修复堆序。 |
1. 交换根节点(\(a[1]\))与最后一个节点(\(a[s]\))。 2. 缩小堆大小( s-- )。 3. 调用 down(1)修复堆序。 |
top() |
返回根节点值(\(a[1]\))——即堆中的最大值。 | 返回根节点值(\(a[1]\))——即堆中的最小值。 |
4. C++代码实现
以下是支持大根堆的通用实现,通过模板类适配任意可比较的数据类型(如 int、double)。
#include <iostream>
using namespace std;
const int N = 1e5 + 10; // 堆的最大容量(可根据需求调整)
template<typename T>
struct Heap {
int s; // 堆的当前大小(初始为0)
T a[N]; // 存储堆元素的数组(1基索引)
// 向下调整节点
void down(int u) {
int t = u; // t追踪当前节点及其子节点中的最大值节点索引
// 比较左子节点
if (2 * u <= s && a[2 * u] > a[t]) t = 2 * u;
// 比较右子节点
if (2 * u + 1 <= s && a[2 * u + 1] > a[t]) t = 2 * u + 1;
// 若当前节点不是最大值节点,交换并递归调整
if (t != u) {
swap(a[u], a[t]);
down(t);
}
}
// 向上调整节点
void up(int u) {
// 当节点有父节点且值大于父节点时,循环向上调整
while (u > 1 && a[u / 2] < a[u]) {
swap(a[u / 2], a[u]);
u /= 2; // 移动到父节点索引
}
}
// 插入元素x
void push(T x) {
a[ ++ s] = x; // 追加到数组末尾
up(s); // 向上调整新节点
}
// 删除堆顶元素(最大值)
void pop() {
if (s) { // 堆非空时操作
swap(a[1], a[s -- ]); // 交换根节点与尾节点,缩小堆大小
down(1); // 向下调整新根节点
}
}
// 获取堆顶元素(最大值)
T top() {
return a[1];
}
};
// 定义一个int类型的大根堆
Heap<int> h;
int main() {
int n;
cin >> n; // 输入操作次数
while (n -- ) {
int opt, x;
cin >> opt; // 输入操作类型(1:插入,2:删除,其他:查询堆顶)
if (opt == 1) {
cin >> x;
h.push(x);
} else if (opt == 2) {
h.pop();
} else {
cout << h.top() << endl;
}
}
return 0;
}

浙公网安备 33010602011771号