feat: Revised the book (#978)

* Sync recent changes to the revised Word.

* Revised the preface chapter

* Revised the introduction chapter

* Revised the computation complexity chapter

* Revised the chapter data structure

* Revised the chapter array and linked list

* Revised the chapter stack and queue

* Revised the chapter hashing

* Revised the chapter tree

* Revised the chapter heap

* Revised the chapter graph

* Revised the chapter searching

* Reivised the sorting chapter

* Revised the divide and conquer chapter

* Revised the chapter backtacking

* Revised the DP chapter

* Revised the greedy chapter

* Revised the appendix chapter

* Revised the preface chapter doubly

* Revised the figures
This commit is contained in:
Yudong Jin
2023-12-02 06:21:34 +08:00
committed by GitHub
parent b824d149cb
commit e720aa2d24
404 changed files with 1537 additions and 1558 deletions

View File

@ -6,7 +6,7 @@
我们首先创建一个空堆,然后遍历列表,依次对每个元素执行“入堆操作”,即先将元素添加至堆的尾部,再对该元素执行“从底至顶”堆化。
每当一个元素入堆,堆的长度就加一。由于节点是从顶到底依次被添加进二叉树的,因此堆是“自上而下”构建的。
每当一个元素入堆,堆的长度就加一。由于节点是从顶到底依次被添加进二叉树的,因此堆是“自上而下”构建的。
设元素数量为 $n$ ,每个元素的入堆操作使用 $O(\log{n})$ 时间,因此该建堆方法的时间复杂度为 $O(n \log n)$ 。
@ -17,11 +17,11 @@
1. 将列表所有元素原封不动添加到堆中,此时堆的性质尚未得到满足。
2. 倒序遍历堆(即层序遍历的倒序),依次对每个非叶节点执行“从顶至底堆化”。
**每当堆化一个节点后,以该节点为根节点的子树就形成一个合法的子堆**。而由于是倒序遍历,因此堆是“自下而上”地被构建的。
**每当堆化一个节点后,以该节点为根节点的子树就形成一个合法的子堆**。而由于是倒序遍历,因此堆是“自下而上”构建的。
之所以选择倒序遍历,是因为这样能够保证当前节点之下的子树已经是合法的子堆,这样堆化当前节点才是有效的。
值得说明的是,**叶节点没有子节点,天然就是合法的子堆,因此无堆化**。如以下代码所示,最后一个非叶节点是最后一个节点的父节点,我们从它开始倒序遍历并执行堆化。
值得说明的是,**叶节点没有子节点,天然就是合法的子堆,因此无堆化**。如以下代码所示,最后一个非叶节点是最后一个节点的父节点,我们从它开始倒序遍历并执行堆化。
```src
[file]{my_heap}-[class]{max_heap}-[func]{__init__}
@ -36,11 +36,11 @@
将上述两者相乘,可得到建堆过程的时间复杂度为 $O(n \log n)$ 。**但这个估算结果并不准确,因为我们没有考虑到二叉树底层节点数量远多于顶层节点的性质**。
接下来我们来进行更为准确的计算。为了减小计算难度,假设给定一个节点数量为 $n$ 高度为 $h$ 的“完美二叉树”,该假设不会影响计算结果的正确性。
接下来我们来进行更为准确的计算。为了降低计算难度,假设给定一个节点数量为 $n$ 高度为 $h$ 的“完美二叉树”,该假设不会影响计算结果的正确性。
![完美二叉树的各层节点数量](build_heap.assets/heapify_operations_count.png)
如上图所示,节点“从顶至底堆化”的最大迭代次数等于该节点到叶节点的距离,而该距离正是“节点高度”。因此,我们可以各层的“节点数量 $\times$ 节点高度”求和,**从而得到所有节点的堆化迭代次数的总和**。
如上图所示,节点“从顶至底堆化”的最大迭代次数等于该节点到叶节点的距离,而该距离正是“节点高度”。因此,我们可以各层的“节点数量 $\times$ 节点高度”求和,**得到所有节点的堆化迭代次数的总和**。
$$
T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{(h-1)}\times1

View File

@ -1,6 +1,6 @@
# 堆
「堆 heap」是一种满足特定条件的完全二叉树主要可分为下图所示的两种类型。
「堆 heap」是一种满足特定条件的完全二叉树主要可分为两种类型,如下图所示
- 「大顶堆 max heap」任意节点的值 $\geq$ 其子节点的值。
- 「小顶堆 min heap」任意节点的值 $\leq$ 其子节点的值。
@ -11,13 +11,13 @@
- 最底层节点靠左填充,其他层的节点都被填满。
- 我们将二叉树的根节点称为“堆顶”,将底层最靠右的节点称为“堆底”。
- 对于大顶堆(小顶堆),堆顶元素(根节点)的值分别是最大(最小)的。
- 对于大顶堆(小顶堆),堆顶元素(根节点)的值分别是最大(最小)的。
## 堆常用操作
需要指出的是,许多编程语言提供的是「优先队列 priority queue」这是一种抽象数据结构定义为具有优先级排序的队列。
实际上,**堆通常用实现优先队列,大顶堆相当于元素按从大到小顺序出队的优先队列**。从使用角度来看,我们可以将“优先队列”和“堆”看作等价的数据结构。因此,本书对两者不做特别区分,统一使用“堆“来命名
实际上,**堆通常用实现优先队列,大顶堆相当于元素按从大到小顺序出队的优先队列**。从使用角度来看,我们可以将“优先队列”和“堆”看作等价的数据结构。因此,本书对两者不做特别区分,统一称作“堆”
堆的常用操作见下表,方法名需要根据编程语言来确定。
@ -33,9 +33,7 @@
在实际应用中,我们可以直接使用编程语言提供的堆类(或优先队列类)。
!!! tip
类似于排序算法中的“从小到大排列”和“从大到小排列”,我们可以通过修改 Comparator 来实现“小顶堆”与“大顶堆”之间的转换。
类似于排序算法中的“从小到大排列”和“从大到小排列”,我们可以通过设置一个 `flag` 或修改 `Comparator` 实现“小顶堆”与“大顶堆”之间的转换。代码如下所示:
=== "Python"
@ -351,15 +349,15 @@
### 堆的存储与表示
我们在二叉树章节中学习到,完全二叉树非常适合用数组来表示。由于堆正是一种完全二叉树,**我们将采用数组来存储堆**。
二叉树章节讲过,完全二叉树非常适合用数组来表示。由于堆正是一种完全二叉树,**因此我们将采用数组来存储堆**。
当使用数组表示二叉树时,元素代表节点值,索引代表节点在二叉树中的位置。**节点指针通过索引映射公式来实现**。
如下图所示,给定索引 $i$ ,其左子节点索引为 $2i + 1$ ,右子节点索引为 $2i + 2$ ,父节点索引为 $(i - 1) / 2$(向下整)。当索引越界时,表示空节点或节点不存在。
如下图所示,给定索引 $i$ ,其左子节点索引为 $2i + 1$ ,右子节点索引为 $2i + 2$ ,父节点索引为 $(i - 1) / 2$(向下整)。当索引越界时,表示空节点或节点不存在。
![堆的表示与存储](heap.assets/representation_of_heap.png)
我们可以将索引映射公式封装成函数,方便后续使用
我们可以将索引映射公式封装成函数,方便后续使用
```src
[file]{my_heap}-[class]{max_heap}-[func]{parent}
@ -367,7 +365,7 @@
### 访问堆顶元素
堆顶元素即为二叉树的根节点,也就是列表的首个元素
堆顶元素即为二叉树的根节点,也就是列表的首个元素
```src
[file]{my_heap}-[class]{max_heap}-[func]{peek}
@ -375,7 +373,7 @@
### 元素入堆
给定元素 `val` ,我们首先将其添加到堆底。添加之后,由于 val 可能大于堆中其他元素,堆的成立条件可能已被破坏。因此**需要修复从插入节点到根节点的路径上的各个节点**,这个操作被称为「堆化 heapify」。
给定元素 `val` ,我们首先将其添加到堆底。添加之后,由于 val 可能大于堆中其他元素,堆的成立条件可能已被破坏,**因此需要修复从插入节点到根节点的路径上的各个节点**,这个操作被称为「堆化 heapify」。
考虑从入堆节点开始,**从底至顶执行堆化**。如下图所示,我们比较插入节点与其父节点的值,如果插入节点更大,则将它们交换。然后继续执行此操作,从底至顶修复堆中的各个节点,直至越过根节点或遇到无须交换的节点时结束。
@ -406,7 +404,7 @@
=== "<9>"
![heap_push_step9](heap.assets/heap_push_step9.png)
设节点总数为 $n$ ,则树的高度为 $O(\log n)$ 。由此可知,堆化操作的循环轮数最多为 $O(\log n)$ **元素入堆操作的时间复杂度为 $O(\log n)$** 。
设节点总数为 $n$ ,则树的高度为 $O(\log n)$ 。由此可知,堆化操作的循环轮数最多为 $O(\log n)$ **元素入堆操作的时间复杂度为 $O(\log n)$** 。代码如下所示:
```src
[file]{my_heap}-[class]{max_heap}-[func]{sift_up}
@ -414,10 +412,10 @@
### 堆顶元素出堆
堆顶元素是二叉树的根节点,即列表首元素。如果我们直接从列表中删除首元素,那么二叉树中所有节点的索引都会发生变化,这将使得后续使用堆化修复变得困难。为了尽量减少元素索引的变动,我们采用以下操作步骤。
堆顶元素是二叉树的根节点,即列表首元素。如果我们直接从列表中删除首元素,那么二叉树中所有节点的索引都会发生变化,这将使得后续使用堆化进行修复变得困难。为了尽量减少元素索引的变动,我们采用以下操作步骤。
1. 交换堆顶元素与堆底元素(交换根节点与最右叶节点)。
2. 交换完成后,将堆底从列表中删除(注意,由于已经交换,实际上删除的是原来的堆顶元素)。
1. 交换堆顶元素与堆底元素(交换根节点与最右叶节点)。
2. 交换完成后,将堆底从列表中删除(注意,由于已经交换,因此实际上删除的是原来的堆顶元素)。
3. 从根节点开始,**从顶至底执行堆化**。
如下图所示,**“从顶至底堆化”的操作方向与“从底至顶堆化”相反**,我们将根节点的值与其两个子节点的值进行比较,将最大的子节点与根节点交换。然后循环执行此操作,直到越过叶节点或遇到无须交换的节点时结束。
@ -452,7 +450,7 @@
=== "<10>"
![heap_pop_step10](heap.assets/heap_pop_step10.png)
与元素入堆操作相似,堆顶元素出堆操作的时间复杂度也为 $O(\log n)$ 。
与元素入堆操作相似,堆顶元素出堆操作的时间复杂度也为 $O(\log n)$ 。代码如下所示:
```src
[file]{my_heap}-[class]{max_heap}-[func]{sift_down}
@ -461,5 +459,5 @@
## 堆常见应用
- **优先队列**:堆通常作为实现优先队列的首选数据结构,其入队和出队操作的时间复杂度均为 $O(\log n)$ ,而建队操作为 $O(n)$ ,这些操作都非常高效。
- **堆排序**:给定一组数据,我们可以用它们建立一个堆,然后不断地执行元素出堆操作,从而得到有序数据。然而,我们通常会使用一种更优雅的方式实现堆排序,详见后续的堆排序章节。
- **堆排序**:给定一组数据,我们可以用它们建立一个堆,然后不断地执行元素出堆操作,从而得到有序数据。然而,我们通常会使用一种更优雅的方式实现堆排序,详见堆排序章节。
- **获取最大的 $k$ 个元素**:这是一个经典的算法问题,同时也是一种典型应用,例如选择热度前 10 的新闻作为微博热搜,选取销量前 10 的商品等。

View File

@ -8,6 +8,6 @@
!!! abstract
堆就像是山川的峰峦,它们层叠起伏、形态各异。
堆就像是山峰峦,层叠起伏、形态各异。
每一座山峰都有其高低之分,而最高的山峰总是最先映入眼帘。
座山峰高低错落,而最高的山峰总是最先映入眼帘。

View File

@ -14,4 +14,4 @@
!!! question "数据结构的“堆”与内存管理的“堆”是同一个概念吗?"
两者不是同一个概念,只是碰巧都叫堆。计算机系统内存中的堆是动态内存分配的一部分,程序在运行时可以使用它来存储数据。程序可以请求一定量的堆内存,用于存储如对象和数组等复杂结构。当这些数据不再需要时,程序需要释放这些内存,以防止内存泄。相较于栈内存,堆内存的管理和使用需要更谨慎,不恰当的使用可能会导致内存泄和野指针等问题。
两者不是同一个概念,只是碰巧都叫堆。计算机系统内存中的堆是动态内存分配的一部分,程序在运行时可以使用它来存储数据。程序可以请求一定量的堆内存,用于存储如对象和数组等复杂结构。当这些数据不再需要时,程序需要释放这些内存,以防止内存泄。相较于栈内存,堆内存的管理和使用需要更谨慎,使用不当可能会导致内存泄和野指针等问题。

View File

@ -2,7 +2,7 @@
!!! question
给定一个长度为 $n$ 无序数组 `nums` ,请返回数组中前 $k$ 大的元素。
给定一个长度为 $n$ 无序数组 `nums` ,请返回数组中前 $k$ 大的元素。
对于该问题,我们先介绍两种思路比较直接的解法,再介绍效率更高的堆解法。
@ -22,7 +22,7 @@
如下图所示,我们可以先对数组 `nums` 进行排序,再返回最右边的 $k$ 个元素,时间复杂度为 $O(n \log n)$ 。
显然,该方法“超额”完成任务了,因为我们只需找出最大的 $k$ 个元素即可,而不需要排序其他元素。
显然,该方法“超额”完成任务了,因为我们只需找出最大的 $k$ 个元素即可,而不需要排序其他元素。
![排序寻找最大的 k 个元素](top_k.assets/top_k_sorting.png)
@ -62,10 +62,12 @@
=== "<9>"
![top_k_heap_step9](top_k.assets/top_k_heap_step9.png)
总共执行了 $n$ 轮入堆和出堆,堆的最大长度为 $k$ ,因此时间复杂度为 $O(n \log k)$ 。该方法的效率很高,当 $k$ 较小时,时间复杂度趋向 $O(n)$ ;当 $k$ 较大时,时间复杂度不会超过 $O(n \log n)$ 。
另外,该方法适用于动态数据流的使用场景。在不断加入数据时,我们可以持续维护堆内的元素,从而实现最大 $k$ 个元素的动态更新。
示例代码如下:
```src
[file]{top_k}-[class]{}-[func]{top_k_heap}
```
总共执行了 $n$ 轮入堆和出堆,堆的最大长度为 $k$ ,因此时间复杂度为 $O(n \log k)$ 。该方法的效率很高,当 $k$ 较小时,时间复杂度趋向 $O(n)$ ;当 $k$ 较大时,时间复杂度不会超过 $O(n \log n)$ 。
另外,该方法适用于动态数据流的使用场景。在不断加入数据时,我们可以持续维护堆内的元素,从而实现最大 $k$ 个元素的动态更新。