堆(Heap) 是一个非常重要的数据结构。
一、堆是什么?
堆是一种特殊的完全二叉树,它满足以下性质:
-
结构性 :它是一棵完全二叉树 。这意味着除了最后一层,其他层都是满的,并且最后一层的节点都尽可能靠左排列。这个特性使得堆可以非常高效地用数组来存储,而不需要像普通树一样使用链式结构。
-
有序性:堆中任意一个节点的值都必须满足与它的子节点的关系。根据这个关系,堆主要分为两种:
-
最大堆(大顶堆) :父节点的值 >= 其所有子节点 的值。因此,堆顶(根节点)是整个堆中的最大元素。
-
最小堆(小顶堆) :父节点的值 <= 其所有子节点 的值。因此,堆顶(根节点)是整个堆中的最小元素。
-
注意:堆不保证兄弟节点之间(比如左孩子和右孩子)有任何特定的大小关系,它只维护"父子"层级间的大小关系。
二、堆的存储:数组表示法
由于堆是一棵完全二叉树,我们可以使用一个一维数组来存储它,这样既节省空间,又可以利用索引快速定位父节点和子节点。
对于数组中索引为 i 的节点(通常从0开始索引):
-
父节点 的索引:
parent(i) = (i - 1) / 2(向下取整) -
左孩子 的索引:
left_child(i) = 2 * i + 1 -
右孩子 的索引:
right_child(i) = 2 * i + 2
示例(最大堆):
[90]
/ \
[80] [70]
/ \ /
[60] [50][40]
对应的数组为:[90, 80, 70, 60, 50, 40]
三、堆的核心操作
为了维护堆的性质,有两个最基础、最重要的操作:上浮(Sift Up / Swim) 和 下沉(Sift Down / Sink)。
1. 插入元素(Insert)
操作步骤:
-
将新元素追加到数组的末尾(即完全二叉树的最后一个位置)。
-
对这个新元素进行 "上浮" 操作,以恢复堆的性质。
-
上浮:将新节点与其父节点比较。
-
在最大堆 中,如果它大于其父节点,就交换它们的位置。
-
在最小堆 中,如果它小于其父节点,就交换它们的位置。
-
-
重复这个过程,直到它不再大于(或小于)其父节点,或者到达堆顶。
-
时间复杂度:O(log n),因为树的深度是 log n。
2. 删除堆顶元素(Extract Max / Extract Min)
这是堆最常用的操作,用于获取并移除最大(或最小)元素。
操作步骤:
-
取出堆顶元素(这就是我们要的结果)。
-
将堆中最后一个元素移到堆顶。
-
对新的堆顶元素进行 "下沉" 操作,以恢复堆的性质。
-
下沉 :将当前节点与其较大的那个子节点 (对于最大堆)或较小的那个子节点(对于最小堆)进行比较。
-
在最大堆 中,如果它小于其子节点中较大的那个,就交换它们的位置。
-
在最小堆 中,如果它大于其子节点中较小的那个,就交换它们的位置。
-
-
重复这个过程,直到它大于(或小于)其所有子节点,或者成为叶子节点。
-
时间复杂度:O(log n)。
四、堆的构建(Heapify)
如何将一个无序的数组构建成一个堆?
一个直观的方法是逐个插入,时间复杂度为 O(n log n)。但有一个更高效的方法,自底向上堆化。
算法步骤:
-
从最后一个非叶子节点 开始(索引为
n/2 - 1)。 -
向前遍历,对每一个遇到的节点都执行一次 "下沉" 操作。
时间复杂度:O(n)。这个结论有点反直觉,但数学上可以证明。直观理解是,大部分节点都很矮,不需要下沉很多层。
五、堆的应用
堆是一个功能强大且应用极其广泛的数据结构。
-
堆排序(Heap Sort)
-
思路:先将待排序数组构建成一个最大堆。此时,最大元素在堆顶。将其与堆的最后一个元素交换,然后堆的大小减一,并对新的堆顶执行"下沉"。重复此过程,直到堆中只剩一个元素。
-
时间复杂度 :O(n log n),并且是原地排序。
-
-
优先队列(Priority Queue)
-
这是堆最典型的应用。优先队列不再遵循"先入先出"的原则,而是按照优先级出队。优先级最高的元素先出队。
-
堆可以完美地实现优先队列的所有核心操作:
-
插入(enqueue)-> 堆的插入操作。 -
取出最高优先级元素(dequeue)-> 堆的删除堆顶操作。 -
查看最高优先级元素(peek)-> 查看堆顶元素。
-
-
-
求 Top K 问题
-
求最大的K个元素 :维护一个大小为 K 的最小堆。遍历数据,如果当前元素比堆顶大,就替换堆顶并下沉。最终堆里的就是最大的K个元素。
-
求最小的K个元素 :维护一个大小为 K 的最大堆。遍历数据,如果当前元素比堆顶小,就替换堆顶并下沉。最终堆里的就是最小的K个元素。
-
时间复杂度:O(n log K),比全排序 O(n log n) 更高效。
-
-
图算法
-
Dijkstra 算法:用于寻找带权图中的最短路径。它使用优先队列(最小堆)来高效地选择当前距离最短的节点。
-
Prim 算法:用于构建最小生成树。同样使用优先队列来选择当前连接生成树的最小权重的边。
-
六、总结:堆的优缺点
| 优点 | 缺点 |
|---|---|
| 获取最大/最小元素极快(O(1)) | 除了堆顶,查找其他元素很慢(O(n)),不支持快速查找 |
| 插入和删除堆顶元素高效(O(log n)) | 堆中元素没有完全的排序,只有偏序关系 |
| 可以高效地进行堆排序和构建 | |
| 是实现优先队列的最佳数据结构 |
总而言之,堆是一种在需要快速访问最大或最小元素 以及处理动态优先级的场景下无可替代的高效数据结构。