堆详解
本文详细介绍堆(Heap)这种数据结构,包括堆的定义、性质、操作、应用场景以及堆排序算法。
什么是堆
堆(Heap)是一种特殊的完全二叉树,它满足堆序性质(Heap Property)。
堆的定义
堆是一个完全二叉树,并且满足以下性质之一:
- 最大堆(Max Heap):父节点的值总是大于或等于其子节点的值
- 最小堆(Min Heap):父节点的值总是小于或等于其子节点的值
堆的性质
- 完全二叉树:堆是一棵完全二叉树,除了最后一层,其他层都是满的,最后一层从左到右填充
- 堆序性质:
- 最大堆:
arr[parent(i)] >= arr[i] - 最小堆:
arr[parent(i)] <= arr[i]
- 最大堆:
- 数组表示:堆通常用数组表示,可以节省空间且访问高效
堆的数组表示
对于数组索引 i 的节点:
- 父节点索引:
parent(i) = (i - 1) / 2 - 左子节点索引:
left(i) = 2*i + 1 - 右子节点索引:
right(i) = 2*i + 2
flowchart TD
Array["数组表示
[10, 8, 9, 3, 5, 7, 6]"]
Tree["树形表示
10
/ \
8 9
/ \ / \
3 5 7 6"]
Array --> Tree
style Array fill:#e1f5ff
style Tree fill:#e8f5e9
堆的分类
flowchart TD
Root["堆"]
Max["最大堆
父节点 >= 子节点"]
Min["最小堆
父节点 <= 子节点"]
Root --> Max
Root --> Min
style Root fill:#e1f5ff
style Max fill:#e8f5e9
style Min fill:#fff3e0
堆的特点
- 堆顶元素:最大堆的堆顶是最大值,最小堆的堆顶是最小值
- 部分有序:堆只保证父节点和子节点的关系,不保证兄弟节点之间的关系
- 高效操作:插入、删除、查找最值的时间复杂度都是 O(log n)
创建堆
创建堆(建堆)是将一个无序数组转换为堆的过程。有两种主要的建堆方法:
方法 1:自下而上建堆(Heapify Down)
从最后一个非叶子节点开始,自下而上、从右到左进行堆化。
算法原理
- 找到最后一个非叶子节点:
n/2 - 1 - 从该节点开始,向前遍历到根节点
- 对每个节点执行下沉操作(Heapify Down)
算法流程
flowchart TD
Start["开始"] --> Input["输入数组 arr"]
Input --> Find["找到最后一个非叶子节点
i = n/2 - 1"]
Find --> Loop["循环: i >= 0"]
Loop -->|否| End["结束"]
Loop -->|是| Heapify["对节点 i 执行堆化
heapifyDown(arr, n, i)"]
Heapify --> Decrement["i--"]
Decrement --> Loop
style Start fill:#e1f5ff
style End fill:#e8f5e9
style Heapify fill:#fff3e0
实现代码
1 | package main |
时间复杂度: O(n) - 虽然看起来是 O(n log n),但通过数学分析可以证明是 O(n)
方法 2:自上而下建堆(Heapify Up)
逐个插入元素,每次插入后执行上浮操作。
算法原理
- 从空堆开始
- 逐个插入元素
- 每次插入后,对插入的元素执行上浮操作(Heapify Up)
实现代码
1 | // BuildMaxHeapTopDown 构建最大堆(自上而下) |
时间复杂度: O(n log n)
两种方法对比
| 特性 | 自下而上 | 自上而下 |
|---|---|---|
| 时间复杂度 | O(n) | O(n log n) |
| 实现复杂度 | 较简单 | 较复杂 |
| 适用场景 | 已知所有元素 | 动态插入 |
推荐使用自下而上建堆,因为时间复杂度更低。
调整堆
调整堆是维护堆性质的核心操作,包括两种基本操作:上浮(Heapify Up)和下沉(Heapify Down)。
下沉操作(Heapify Down)
下沉操作用于修复违反堆性质的节点,通常用于删除操作或建堆过程。
算法原理
- 比较当前节点与其子节点
- 如果不满足堆性质,与较大的子节点(最大堆)或较小的子节点(最小堆)交换
- 递归或迭代处理交换后的位置
算法流程
flowchart TD
Start["开始"] --> Input["输入: arr, n, i"]
Input --> Check{节点 i 有子节点?}
Check -->|否| End["结束"]
Check -->|是| Find["找到最大/最小子节点"]
Find --> Compare{子节点 > 父节点?
最大堆}
Compare -->|是| Swap["交换父子节点"]
Compare -->|否| End
Swap --> Recursive["递归处理交换后的位置"]
Recursive --> End
style Start fill:#e1f5ff
style End fill:#e8f5e9
style Swap fill:#fff3e0
实现代码
1 | // heapifyDown 下沉操作(最大堆) |
时间复杂度: O(log n)
空间复杂度: O(1)(迭代版本)或 O(log n)(递归版本)
上浮操作(Heapify Up)
上浮操作用于修复违反堆性质的节点,通常用于插入操作。
算法原理
- 比较当前节点与其父节点
- 如果不满足堆性质,与父节点交换
- 继续向上比较,直到满足堆性质或到达根节点
算法流程
flowchart TD
Start["开始"] --> Input["输入: arr, i"]
Input --> Check{i > 0?}
Check -->|否| End["结束"]
Check -->|是| Parent["parent = (i-1)/2"]
Parent --> Compare{"arr[i] > arr[parent]?
最大堆"}
Compare -->|是| Swap["交换父子节点"]
Compare -->|否| End
Swap --> Update["i = parent"]
Update --> Check
style Start fill:#e1f5ff
style End fill:#e8f5e9
style Swap fill:#fff3e0
实现代码
1 | // heapifyUp 上浮操作(最大堆) |
时间复杂度: O(log n)
空间复杂度: O(1)
堆的基本操作
插入元素
1 | // InsertMaxHeap 向最大堆插入元素 |
删除堆顶元素
1 | // ExtractMax 从最大堆删除并返回最大值 |
获取堆顶元素
1 | // Peek 获取堆顶元素(不删除) |
完整示例
1 | func main() { |
堆排序
堆排序(Heap Sort)是利用堆这种数据结构设计的一种排序算法。
算法原理
- 建堆:将待排序数组构建成最大堆
- 排序:重复以下步骤直到堆为空:
- 将堆顶元素(最大值)与数组末尾元素交换
- 减小堆的大小
- 对新的堆顶元素执行下沉操作
算法流程
flowchart TD
Start["开始"] --> Input["输入数组 arr"]
Input --> Build["构建最大堆"]
Build --> I["i = n - 1"]
I --> Loop["循环: i > 0"]
Loop -->|否| End["结束"]
Loop -->|是| Swap["交换 arr[0] 和 arr[i]"]
Swap --> Decrease["堆大小减1"]
Decrease --> Heapify["堆化 arr[0:i]"]
Heapify --> Decrement["i--"]
Decrement --> Loop
style Start fill:#e1f5ff
style End fill:#e8f5e9
style Build fill:#fff3e0
style Swap fill:#ffebee
实现代码
1 | // HeapSort 堆排序 |
复杂度分析
- 时间复杂度:
- 建堆: O(n)
- 排序: O(n log n)
- 总体: O(n log n)
- 空间复杂度: O(1) - 原地排序
- 稳定性: 不稳定排序
堆排序的特点
优点:
- 时间复杂度稳定为 O(n log n)
- 空间复杂度为 O(1)
- 不需要额外的辅助空间
缺点:
- 不稳定排序
- 对缓存不友好(访问模式不连续)
- 实际性能通常不如快速排序
优先队列
优先队列(Priority Queue)是堆的一个重要应用,它支持以下操作:
Insert(x): 插入元素ExtractMax()/ExtractMin(): 删除并返回最大/最小元素Peek(): 查看最大/最小元素(不删除)
实现优先队列
1 | // PriorityQueue 优先队列(最大堆实现) |
堆的应用场景
1. 优先队列
- 任务调度:按优先级调度任务
- 事件驱动系统:按时间顺序处理事件
- Dijkstra 算法:用于最短路径算法
2. Top K 问题
找出数组中前 K 个最大或最小的元素。
1 | // FindTopK 找出前K个最大元素 |
3. 堆排序
用于排序算法,时间复杂度 O(n log n)。
4. 中位数查找
使用两个堆(最大堆和最小堆)来维护中位数。
5. 合并K个有序链表
使用最小堆来高效合并多个有序链表。
1 | // ListNode 链表节点 |
复杂度总结
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 建堆 | O(n) | O(1) |
| 插入 | O(log n) | O(1) |
| 删除堆顶 | O(log n) | O(1) |
| 查找堆顶 | O(1) | O(1) |
| 堆排序 | O(n log n) | O(1) |
堆 vs 其他数据结构
堆 vs 二叉搜索树
| 特性 | 堆 | 二叉搜索树 |
|---|---|---|
| 查找最值 | O(1) | O(log n) |
| 查找任意值 | O(n) | O(log n) |
| 插入 | O(log n) | O(log n) |
| 删除 | O(log n) | O(log n) |
| 有序遍历 | 不支持 | 支持 |
堆 vs 排序数组
| 特性 | 堆 | 排序数组 |
|---|---|---|
| 查找最值 | O(1) | O(1) |
| 插入 | O(log n) | O(n) |
| 删除 | O(log n) | O(n) |
| 空间 | O(n) | O(n) |
总结
- 堆的定义:完全二叉树 + 堆序性质
- 两种堆:最大堆和最小堆
- 核心操作:上浮和下沉,时间复杂度都是 O(log n)
- 建堆方法:自下而上(O(n))和自上而下(O(n log n))
- 堆排序:时间复杂度 O(n log n),空间复杂度 O(1)
- 应用场景:优先队列、Top K 问题、任务调度等
堆是一种非常重要的数据结构,在算法和实际应用中都有广泛的使用。掌握堆的实现和应用对于解决很多问题都非常有帮助!