算法入门:数据结构-堆

堆(Heap):优先队列的高效实现

在数据结构的世界里,堆(Heap)是一种非常实用的结构。它专门用来解决一类问题:快速找到一堆元素中的最大值或最小值。比如任务调度系统需要优先处理紧急任务,游戏排行榜需要快速找到最高分玩家,这些场景都离不开堆。


一、什么是堆?

1.1 堆的定义

堆(Heap) 是一种特殊的树形数据结构,它支持以下核心操作:

操作 功能 时间复杂度
insert(x) 插入元素 O(log n)
getMax() / getMin() 查看最大/最小元素 O(1)
extractMax() / extractMin() 取出并删除最大/最小元素 O(log n)

根据堆顶元素的特性,堆分为两类:

  • 大根堆(Max Heap) :根节点的值 大于等于 其左右子树中所有节点的值
  • 小根堆(Min Heap) :根节点的值 小于等于 其左右子树中所有节点的值

1.2 二叉堆:最常见的堆实现

我们通常使用 二叉堆(Binary Heap) 来实现堆,它具有以下特点:

  1. 结构性质 :必须是 完全二叉树
  2. 堆序性质:满足大根堆或小根堆的定义

完全二叉树(Complete Binary Tree) 是一种特殊的二叉树:

  • 除了最后一层,其他层的节点都是满的
  • 最后一层的节点必须从左到右连续排列
text 复制代码
        完全二叉树 ✅               非完全二叉树 ❌
           16                        16
         /    \                    /    \
       14      10                14      10
      /  \    /  \              /          \
     8    7  9    3            8            3
    / \                       / \
   2   4                     2   4

二、数组表示法:堆的精妙之处

2.1 为什么用数组?

完全二叉树的特性使得我们可以用 数组 来高效存储堆,无需使用指针!

存储规则(从索引 1 开始):

text 复制代码
数组:[-, 16, 14, 10, 8, 7, 9, 3, 2, 4, 1]
索引: 0   1   2   3  4  5  6  7  8  9  10

对应的树结构:
              16 (1)
            /        \
         14 (2)      10 (3)
        /     \      /     \
      8 (4)  7 (5) 9 (6)  3 (7)
     /   \   /
   2 (8) 4 (9) 1 (10)

2.2 索引计算公式

对于索引为 i 的节点:

关系 公式 时间复杂度
父节点 ⌊i / 2⌋ O(1)
左子节点 2 * i O(1)
右子节点 2 * i + 1 O(1)

示例验证

text 复制代码
节点 14 (索引 2):
  - 父节点:⌊2/2⌋ = 1 → 16 ✅
  - 左子节点:2*2 = 4 → 8 ✅
  - 右子节点:2*2+1 = 5 → 7 ✅

这种表示法的优势:

  • 空间效率高:无需存储指针
  • 访问速度快:通过简单计算即可定位父子节点
  • 缓存友好:数组连续存储,CPU 缓存命中率高

三、插入操作:乒乓球队的挑战赛

3.1 形象比喻

想象一个乒乓球队,教练想找出队里最强的球员。他设计了一套 挑战赛机制

  1. 初始状态:随机选一个球员作为"擂主"(不一定是最强的)
  2. 新人入队:新球员不能直接挑战擂主,必须从最底层开始
  3. 逐层挑战
    • 新球员先站在最后一个空位(叶节点)
    • 如果比父节点强,就和父节点交换位置
    • 继续向上挑战,直到遇到更强的对手或到达顶端

这个过程保证了:

  • 擂主永远是最强的(堆顶元素最大)
  • 每个父节点都比子节点强(堆序性质)
  • 左右子节点之间无需比较(只需维护父子关系)

3.2 插入算法:上浮(Percolate Up)

核心思想:新元素先放在数组末尾,然后不断与父节点比较,如果比父节点大就交换,直到找到合适位置。

cpp 复制代码
class MaxHeap {
private:
    vector<int> heap;  // heap[0] 不使用,从 heap[1] 开始
    int size;
    
    // 上浮操作
    void percolateUp(int index) {
        int value = heap[index];
        
        // 当不是根节点 且 比父节点大时,继续上浮
        while (index > 1 && value > heap[index / 2]) {
            heap[index] = heap[index / 2];  // 父节点下移
            index = index / 2;              // 继续向上
        }
        
        heap[index] = value;  // 找到最终位置
    }
    
public:
    MaxHeap() {
        heap.push_back(-1);  // 占位,使索引从 1 开始
        size = 0;
    }
    
    // 插入元素
    void insert(int value) {
        heap.push_back(value);  // 先放在末尾
        size++;
        percolateUp(size);      // 上浮到正确位置
    }
    
    // 查看最大元素
    int getMax() {
        if (size == 0) throw runtime_error("Heap is empty");
        return heap[1];
    }
};

3.3 插入过程图解

插入元素 15 到现有堆中:

text 复制代码
初始堆:
              16
            /    \
          14      10
         /  \    /  \
        8    7  9    3
       / \
      2   4

数组:[-, 16, 14, 10, 8, 7, 9, 3, 2, 4]

步骤 1:将 15 放在末尾
              16
            /    \
          14      10
         /  \    /  \
        8    7  9    3
       / \  /
      2  4 15

数组:[-, 16, 14, 10, 8, 7, 9, 3, 2, 4, 15]
索引 10,父节点索引 5 (值为 7)

步骤 2:15 > 7,交换
              16
            /    \
          14      10
         /  \    /  \
        8   15  9    3
       / \  /
      2  4 7

数组:[-, 16, 14, 10, 8, 15, 9, 3, 2, 4, 7]
索引 5,父节点索引 2 (值为 14)

步骤 3:15 > 14,交换
              16
            /    \
          15      10
         /  \    /  \
        8   14  9    3
       / \  /
      2  4 7

数组:[-, 16, 15, 10, 8, 14, 9, 3, 2, 4, 7]
索引 2,父节点索引 1 (值为 16)

步骤 4:15 < 16,停止
最终堆构建完成!

时间复杂度分析

  • 最坏情况:新元素从叶节点上浮到根节点
  • 上浮次数 = 树的高度 = log₂(n)
  • 时间复杂度:O(log n)

四、删除操作:擂主退役后的重组

4.1 形象比喻

有一天,最强的球员(堆顶)退役了,队伍需要选出新的擂主。但问题来了:

  • 左右两边都有高手:左子树和右子树都可能藏着第二强的球员
  • 不能直接提拔子节点:如果把左子节点提上来,它原来的子节点怎么办?结构会乱

解决方案

  1. 最后一个球员(数组末尾元素)临时放到擂主位置
  2. 让他 逐层向下挑战
    • 比较左右两个子节点,选出更强的那个
    • 如果临时擂主打不过,就和更强的子节点交换
    • 继续向下,直到找到合适位置

这个过程叫做 下沉(Percolate Down)

4.2 删除算法:下沉(Percolate Down)

cpp 复制代码
class MaxHeap {
private:
    // 下沉操作
    void percolateDown(int index) {
        int value = heap[index];
        int child;
        
        // 当存在左子节点时
        while (index * 2 <= size) {
            child = index * 2;  // 左子节点
            
            // 如果右子节点存在且更大,选择右子节点
            if (child + 1 <= size && heap[child + 1] > heap[child]) {
                child++;
            }
            
            // 如果当前值比最大的子节点大,停止下沉
            if (value >= heap[child]) {
                break;
            }
            
            // 否则,子节点上移
            heap[index] = heap[child];
            index = child;
        }
        
        heap[index] = value;  // 找到最终位置
    }
    
public:
    // 删除并返回最大元素
    int extractMax() {
        if (size == 0) throw runtime_error("Heap is empty");
        
        int maxValue = heap[1];     // 保存最大值
        heap[1] = heap[size];       // 用最后一个元素替换根节点
        heap.pop_back();            // 删除最后一个元素
        size--;
        
        if (size > 0) {
            percolateDown(1);       // 从根节点开始下沉
        }
        
        return maxValue;
    }
};

4.3 删除过程图解

从堆中删除最大元素 16

text 复制代码
初始堆:
              16
            /    \
          14      10
         /  \    /  \
        8    7  9    3
       / \
      2   4

数组:[-, 16, 14, 10, 8, 7, 9, 3, 2, 4]

步骤 1:用最后一个元素 4 替换根节点
              4
            /    \
          14      10
         /  \    /  \
        8    7  9    3
       /
      2

数组:[-, 4, 14, 10, 8, 7, 9, 3, 2]
索引 1,左子节点 2 (值 14),右子节点 3 (值 10)

步骤 2:4 < max(14, 10),与 14 交换
              14
            /    \
           4      10
         /  \    /  \
        8    7  9    3
       /
      2

数组:[-, 14, 4, 10, 8, 7, 9, 3, 2]
索引 2,左子节点 4 (值 8),右子节点 5 (值 7)

步骤 3:4 < max(8, 7),与 8 交换
              14
            /    \
           8      10
         /  \    /  \
        4    7  9    3
       /
      2

数组:[-, 14, 8, 10, 4, 7, 9, 3, 2]
索引 4,左子节点 8 (值 2),右子节点不存在

步骤 4:4 > 2,停止下沉
最终堆重组完成!

时间复杂度:O(log n)


五、完整实现与测试

5.1 完整代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <stdexcept>
using namespace std;

class MaxHeap {
private:
    vector<int> heap;
    int size;
    
    void percolateUp(int index) {
        int value = heap[index];
        while (index > 1 && value > heap[index / 2]) {
            heap[index] = heap[index / 2];
            index = index / 2;
        }
        heap[index] = value;
    }
    
    void percolateDown(int index) {
        int value = heap[index];
        int child;
        
        while (index * 2 <= size) {
            child = index * 2;
            if (child + 1 <= size && heap[child + 1] > heap[child]) {
                child++;
            }
            if (value >= heap[child]) break;
            
            heap[index] = heap[child];
            index = child;
        }
        heap[index] = value;
    }
    
public:
    MaxHeap() {
        heap.push_back(-1);  // 占位
        size = 0;
    }
    
    void insert(int value) {
        heap.push_back(value);
        size++;
        percolateUp(size);
    }
    
    int extractMax() {
        if (size == 0) throw runtime_error("Heap is empty");
        
        int maxValue = heap[1];
        heap[1] = heap[size];
        heap.pop_back();
        size--;
        
        if (size > 0) percolateDown(1);
        return maxValue;
    }
    
    int getMax() {
        if (size == 0) throw runtime_error("Heap is empty");
        return heap[1];
    }
    
    bool isEmpty() { return size == 0; }
    int getSize() { return size; }
    
    // 打印堆(用于调试)
    void print() {
        cout << "Heap: ";
        for (int i = 1; i <= size; i++) {
            cout << heap[i] << " ";
        }
        cout << endl;
    }
};

int main() {
    MaxHeap heap;
    
    // 插入元素
    cout << "=== 插入操作 ===" << endl;
    int values[] = {16, 14, 10, 8, 7, 9, 3, 2, 4, 1};
    for (int val : values) {
        heap.insert(val);
        cout << "插入 " << val << " 后: ";
        heap.print();
    }
    
    // 查看最大元素
    cout << "\n当前最大元素: " << heap.getMax() << endl;
    
    // 删除操作
    cout << "\n=== 删除操作 ===" << endl;
    while (!heap.isEmpty()) {
        int max = heap.extractMax();
        cout << "删除 " << max << " 后: ";
        if (!heap.isEmpty()) heap.print();
        else cout << "堆已空" << endl;
    }
    
    return 0;
}

5.2 运行结果

text 复制代码
=== 插入操作 ===
插入 16 后: Heap: 16 
插入 14 后: Heap: 16 14 
插入 10 后: Heap: 16 14 10 
插入 8 后: Heap: 16 14 10 8 
插入 7 后: Heap: 16 14 10 8 7 
插入 9 后: Heap: 16 14 10 8 7 9 
插入 3 后: Heap: 16 14 10 8 7 9 3 
插入 2 后: Heap: 16 14 10 8 7 9 3 2 
插入 4 后: Heap: 16 14 10 8 7 9 3 2 4 
插入 1 后: Heap: 16 14 10 8 7 9 3 2 4 1 

当前最大元素: 16

=== 删除操作 ===
删除 16 后: Heap: 14 8 10 4 7 9 3 2 1 
删除 14 后: Heap: 10 8 9 4 7 1 3 2 
删除 10 后: Heap: 9 8 3 4 7 1 2 
删除 9 后: Heap: 8 7 3 4 2 1 
删除 8 后: Heap: 7 4 3 1 2 
删除 7 后: Heap: 4 2 3 1 
删除 4 后: Heap: 3 2 1 
删除 3 后: Heap: 2 1 
删除 2 后: Heap: 1 
删除 1 后: 堆已空

六、性能分析与应用

6.1 时间复杂度总结

操作 时间复杂度 说明
insert O(log n) 最多上浮 log n 层
extractMax O(log n) 最多下沉 log n 层
getMax O(1) 直接访问 heap1
buildHeap O(n) 批量建堆(后续讲解)

6.2 实际应用场景

  1. 优先队列:操作系统任务调度、网络数据包处理
  2. Top K 问题:找出数据流中最大的 K 个元素
  3. 堆排序:时间复杂度 O(n log n) 的排序算法
  4. 图算法:Dijkstra 最短路径、Prim 最小生成树
  5. 中位数维护:使用大根堆和小根堆配合

七、核心总结

7.1 堆的本质

堆是一种 用数组实现的完全二叉树 ,通过维护 堆序性质 来快速访问极值元素。

7.2 关键操作

  • 插入 :放在末尾,然后 上浮(与父节点比较)
  • 删除 :用末尾元素替换根节点,然后 下沉(与子节点比较)

7.3 设计精髓

  1. 数组表示:利用完全二叉树的特性,通过索引计算父子关系
  2. 局部调整:每次只需调整一条路径,无需重建整个堆
  3. 时间保证:所有核心操作都在 O(log n) 时间内完成

7.4 形象记忆

  • 插入 = 新人挑战赛:从底层逐级向上挑战
  • 删除 = 擂主退役重组:临时擂主逐级向下找位置
  • 堆顶 = 当前擂主:永远是最强的(或最弱的)

掌握堆不仅能解决优先队列问题,更是理解高级算法的基础。

相关推荐
8Qi86 小时前
LeetCode 75:颜色分类(荷兰国旗问题)—— Java 题解 ✅
java·算法·leetcode·指针·排序
888CC++7 小时前
如何在 C 语言中进行程序调试?
前端·javascript·算法
pluviophile_s8 小时前
数据结构:第2讲:线性表
数据结构·笔记
(●—●)橘子……9 小时前
力扣第503场周赛练习理解
python·学习·算法·leetcode·职场和发展·周赛
明志数科10 小时前
4D时序标注技术详解:让机器人理解连续动作的数据基础
java·算法·机器人
KaMeidebaby11 小时前
卡梅德生物技术快报|原核表达系统工艺优化:包涵体重折叠 + 分子筛纯化实现功能 RBD 高效制备,附全参数配置
前端·人工智能·算法·数据挖掘·数据分析
无限码力11 小时前
携程0510笔试真题【单数组交换】
算法·携程笔试·携程笔试真题·携程0510笔试真题
Love_云宝儿11 小时前
WKT数据示例并与GeoJSON数据对比
数据结构·gis
BlockWay12 小时前
WEEX Labs 周度观察:微软-OpenAI 合作调整与AI 多云趋势
大数据·人工智能·算法·安全·microsoft