Heap

堆是一种基于完全二叉树的基础数据结构,其优化目标是高效访问"极值"元素(最大值或最小值)。堆是堆排序、优先队列等算法的核心,且结构简洁,是编码面试与实际应用中的必备知识点。本文将拆解堆的基础原理,涵盖大根堆与小根堆,并提供可复用的C++实现代码。

1. 什么是堆?

堆由两个核心性质定义:

  1. 完全二叉树性质:除最后一层外,所有层均被完全填满;最后一层的节点从左到右依次排列(左对齐)。这一性质使得堆可通过数组高效存储(无需指针)。

  2. 堆序性质:用于约束父子节点间关系的严格规则,据此可将堆分为两类:

    • 大根堆(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的右子节点})\)
      堆的根节点存储堆中的最小值

2. 堆的数组表示法

由于堆是完全二叉树,可采用1-基索引 将其存储在数组中(该方式能简化父子节点索引的计算)。对于索引为\(i\)的节点:

  • 左子节点索引:\(2i\)
  • 右子节点索引:\(2i + 1\)
  • 父节点索引:\(\lfloor i/2 \rfloor\)

3. 堆的核心操作

堆的所有操作均依赖两个辅助函数,用于在堆结构修改后恢复堆序性质:down()(将节点向下调整)和 up()(将节点向上调整)。以下分别针对大根堆与小根堆进行说明。

3.1 辅助函数1:down(int u)——向下调整节点以修复堆序

当节点因"值过小"(大根堆中)或"值过大"(小根堆中)违反堆序性质时,使用 down()函数。该函数会将节点与合适的子节点交换,并递归调整,直至堆序性质恢复。

大根堆的 down()逻辑:

  1. 初始化\(\text{largest} = u\)(用于追踪\(u\)及其子节点中值最大的节点索引)。
  2. 若左子节点存在(\(2i \leq\) 堆大小)且左子节点值大于\(\text{largest}\)对应的节点值,则将\(\text{largest}\)更新为左子节点索引。
  3. 若右子节点存在(\(2i + 1 \leq\) 堆大小)且右子节点值大于\(\text{largest}\)对应的节点值,则将\(\text{largest}\)更新为右子节点索引。
  4. \(\text{largest} \neq u\)(说明\(u\)不是最大值节点),交换\(u\)\(\text{largest}\)对应的节点值,并调用 down(largest)继续调整新位置的节点。

小根堆的 down()逻辑:

将"追踪最大值"改为"追踪最小值",比较逻辑变为"小于":

  1. 初始化\(\text{smallest} = u\)(用于追踪\(u\)及其子节点中值最小的节点索引)。
  2. 若左子节点存在且值小于\(\text{smallest}\)对应的节点值,更新\(\text{smallest}\)为左子节点索引。
  3. 若右子节点存在且值小于\(\text{smallest}\)对应的节点值,更新\(\text{smallest}\)为右子节点索引。
  4. \(\text{smallest} \neq u\),交换节点值并递归调用 down(smallest)

3.2 辅助函数2:up(int u)——向上调整节点以修复堆序

当节点因"值过大"(大根堆中)或"值过小"(小根堆中)违反堆序性质时,使用 up()函数。该函数会将节点与父节点交换,并重复调整,直至堆序性质恢复。

大根堆的 up()逻辑:

当节点存在父节点(\(u > 1\),因采用1基索引,根节点索引为1)且节点值大于父节点值时,循环执行:

  1. 交换当前节点与父节点的值。
  2. \(u\)更新为父节点的索引(\(u = \lfloor u/2 \rfloor\))。

小根堆的 up()逻辑:

当节点存在父节点(\(u > 1\))且节点值小于父节点值时,循环执行:

  1. 交换当前节点与父节点的值。
  2. \(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++代码实现

以下是支持大根堆的通用实现,通过模板类适配任意可比较的数据类型(如 intdouble)。

#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;
}
posted @ 2025-09-27 18:50  wz150432  阅读(7)  评论(0)    收藏  举报