This commit is contained in:
krahets
2023-08-22 13:50:12 +08:00
parent 0c9bf14e20
commit 92a0853ab8
64 changed files with 478 additions and 479 deletions
+2 -2
View File
@@ -206,9 +206,9 @@ comments: true
![完美二叉树的各层节点数量](build_heap.assets/heapify_operations_count.png)
<p align="center"> 图完美二叉树的各层节点数量 </p>
<p align="center"> 图 8-5 &nbsp; 完美二叉树的各层节点数量 </p>
图所示,**节点“从顶至底堆化”的最大迭代次数等于该节点到叶节点的距离,而该距离正是“节点高度”**。因此,我们可以将各层的“节点数量 $\times$ 节点高度”求和,**从而得到所有节点的堆化迭代次数的总和**。
如图 8-5 所示,**节点“从顶至底堆化”的最大迭代次数等于该节点到叶节点的距离,而该距离正是“节点高度”**。因此,我们可以将各层的“节点数量 $\times$ 节点高度”求和,**从而得到所有节点的堆化迭代次数的总和**。
$$
T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{(h-1)}\times1
+15 -15
View File
@@ -4,14 +4,14 @@ comments: true
# 8.1 &nbsp; 堆
「堆 heap」是一种满足特定条件的完全二叉树,主要可分为图所示的两种类型:
「堆 heap」是一种满足特定条件的完全二叉树,主要可分为图 8-1 所示的两种类型:
- 「大顶堆 max heap」:任意节点的值 $\geq$ 其子节点的值。
- 「小顶堆 min heap」:任意节点的值 $\leq$ 其子节点的值。
![小顶堆与大顶堆](heap.assets/min_heap_and_max_heap.png)
<p align="center"> 图小顶堆与大顶堆 </p>
<p align="center"> 图 8-1 &nbsp; 小顶堆与大顶堆 </p>
堆作为完全二叉树的一个特例,具有以下特性:
@@ -25,9 +25,9 @@ comments: true
实际上,**堆通常用作实现优先队列,大顶堆相当于元素按从大到小顺序出队的优先队列**。从使用角度来看,我们可以将“优先队列”和“堆”看作等价的数据结构。因此,本书对两者不做特别区分,统一使用“堆“来命名。
堆的常用操作见表,方法名需要根据编程语言来确定。
堆的常用操作见表 8-1 ,方法名需要根据编程语言来确定。
<p align="center"> 表堆的操作效率 </p>
<p align="center"> 表 8-1 &nbsp; 堆的操作效率 </p>
<div class="center-table" markdown>
@@ -68,11 +68,11 @@ comments: true
/* 堆顶元素出堆 */
// 出堆元素会形成一个从大到小的序列
peek = heap.poll(); // 5
peek = heap.poll(); // 4
peek = heap.poll(); // 3
peek = heap.poll(); // 2
peek = heap.poll(); // 1
peek = maxHeap.poll(); // 5
peek = maxHeap.poll(); // 4
peek = maxHeap.poll(); // 3
peek = maxHeap.poll(); // 2
peek = maxHeap.poll(); // 1
/* 获取堆大小 */
int size = maxHeap.size();
@@ -331,11 +331,11 @@ comments: true
当使用数组表示二叉树时,元素代表节点值,索引代表节点在二叉树中的位置。**节点指针通过索引映射公式来实现**。
图所示,给定索引 $i$ ,其左子节点索引为 $2i + 1$ ,右子节点索引为 $2i + 2$ ,父节点索引为 $(i - 1) / 2$(向下取整)。当索引越界时,表示空节点或节点不存在。
如图 8-2 所示,给定索引 $i$ ,其左子节点索引为 $2i + 1$ ,右子节点索引为 $2i + 2$ ,父节点索引为 $(i - 1) / 2$(向下取整)。当索引越界时,表示空节点或节点不存在。
![堆的表示与存储](heap.assets/representation_of_heap.png)
<p align="center"> 图堆的表示与存储 </p>
<p align="center"> 图 8-2 &nbsp; 堆的表示与存储 </p>
我们可以将索引映射公式封装成函数,方便后续使用。
@@ -681,7 +681,7 @@ comments: true
给定元素 `val` ,我们首先将其添加到堆底。添加之后,由于 val 可能大于堆中其他元素,堆的成立条件可能已被破坏。因此,**需要修复从插入节点到根节点的路径上的各个节点**,这个操作被称为「堆化 heapify」。
考虑从入堆节点开始,**从底至顶执行堆化**。如图所示,我们比较插入节点与其父节点的值,如果插入节点更大,则将它们交换。然后继续执行此操作,从底至顶修复堆中的各个节点,直至越过根节点或遇到无须交换的节点时结束。
考虑从入堆节点开始,**从底至顶执行堆化**。如图 8-3 所示,我们比较插入节点与其父节点的值,如果插入节点更大,则将它们交换。然后继续执行此操作,从底至顶修复堆中的各个节点,直至越过根节点或遇到无须交换的节点时结束。
=== "<1>"
![元素入堆步骤](heap.assets/heap_push_step1.png)
@@ -710,7 +710,7 @@ comments: true
=== "<9>"
![heap_push_step9](heap.assets/heap_push_step9.png)
<p align="center"> 图元素入堆步骤 </p>
<p align="center"> 图 8-3 &nbsp; 元素入堆步骤 </p>
设节点总数为 $n$ ,则树的高度为 $O(\log n)$ 。由此可知,堆化操作的循环轮数最多为 $O(\log n)$ ,**元素入堆操作的时间复杂度为 $O(\log n)$** 。
@@ -1058,7 +1058,7 @@ comments: true
2. 交换完成后,将堆底从列表中删除(注意,由于已经交换,实际上删除的是原来的堆顶元素)。
3. 从根节点开始,**从顶至底执行堆化**。
图所示,**“从顶至底堆化”的操作方向与“从底至顶堆化”相反**,我们将根节点的值与其两个子节点的值进行比较,将最大的子节点与根节点交换。然后循环执行此操作,直到越过叶节点或遇到无须交换的节点时结束。
如图 8-4 所示,**“从顶至底堆化”的操作方向与“从底至顶堆化”相反**,我们将根节点的值与其两个子节点的值进行比较,将最大的子节点与根节点交换。然后循环执行此操作,直到越过叶节点或遇到无须交换的节点时结束。
=== "<1>"
![堆顶元素出堆步骤](heap.assets/heap_pop_step1.png)
@@ -1090,7 +1090,7 @@ comments: true
=== "<10>"
![heap_pop_step10](heap.assets/heap_pop_step10.png)
<p align="center"> 图堆顶元素出堆步骤 </p>
<p align="center"> 图 8-4 &nbsp; 堆顶元素出堆步骤 </p>
与元素入堆操作相似,堆顶元素出堆操作的时间复杂度也为 $O(\log n)$ 。
+6 -6
View File
@@ -12,13 +12,13 @@ comments: true
## 8.3.1 &nbsp; 方法一:遍历选择
我们可以进行图所示的 $k$ 轮遍历,分别在每轮中提取第 $1$ , $2$ , $\dots$ , $k$ 大的元素,时间复杂度为 $O(nk)$ 。
我们可以进行图 8-6 所示的 $k$ 轮遍历,分别在每轮中提取第 $1$ , $2$ , $\dots$ , $k$ 大的元素,时间复杂度为 $O(nk)$ 。
此方法只适用于 $k \ll n$ 的情况,因为当 $k$ 与 $n$ 比较接近时,其时间复杂度趋向于 $O(n^2)$ ,非常耗时。
![遍历寻找最大的 k 个元素](top_k.assets/top_k_traversal.png)
<p align="center"> 图遍历寻找最大的 k 个元素 </p>
<p align="center"> 图 8-6 &nbsp; 遍历寻找最大的 k 个元素 </p>
!!! tip
@@ -26,17 +26,17 @@ comments: true
## 8.3.2 &nbsp; 方法二:排序
图所示,我们可以先对数组 `nums` 进行排序,再返回最右边的 $k$ 个元素,时间复杂度为 $O(n \log n)$ 。
如图 8-7 所示,我们可以先对数组 `nums` 进行排序,再返回最右边的 $k$ 个元素,时间复杂度为 $O(n \log n)$ 。
显然,该方法“超额”完成任务了,因为我们只需要找出最大的 $k$ 个元素即可,而不需要排序其他元素。
![排序寻找最大的 k 个元素](top_k.assets/top_k_sorting.png)
<p align="center"> 图排序寻找最大的 k 个元素 </p>
<p align="center"> 图 8-7 &nbsp; 排序寻找最大的 k 个元素 </p>
## 8.3.3 &nbsp; 方法三:堆
我们可以基于堆更加高效地解决 Top-K 问题,流程如图所示。
我们可以基于堆更加高效地解决 Top-K 问题,流程如图 8-8 所示。
1. 初始化一个小顶堆,其堆顶元素最小。
2. 先将数组的前 $k$ 个元素依次入堆。
@@ -70,7 +70,7 @@ comments: true
=== "<9>"
![top_k_heap_step9](top_k.assets/top_k_heap_step9.png)
<p align="center"> 图基于堆寻找最大的 k 个元素 </p>
<p align="center"> 图 8-8 &nbsp; 基于堆寻找最大的 k 个元素 </p>
总共执行了 $n$ 轮入堆和出堆,堆的最大长度为 $k$ ,因此时间复杂度为 $O(n \log k)$ 。该方法的效率很高,当 $k$ 较小时,时间复杂度趋向 $O(n)$ ;当 $k$ 较大时,时间复杂度不会超过 $O(n \log n)$ 。