mirror of
https://github.com/krahets/hello-algo.git
synced 2026-04-13 18:00:18 +08:00
Update the book based on the revised second edition (#1014)
* Revised the book * Update the book with the second revised edition * Revise base on the manuscript of the first edition
This commit is contained in:
@@ -16,13 +16,13 @@
|
||||
|
||||
## 表示任意二叉树
|
||||
|
||||
完美二叉树是一个特例,在二叉树的中间层通常存在许多 $\text{None}$ 。由于层序遍历序列并不包含这些 $\text{None}$ ,因此我们无法仅凭该序列来推测 $\text{None}$ 的数量和分布位置。**这意味着存在多种二叉树结构都符合该层序遍历序列**。
|
||||
完美二叉树是一个特例,在二叉树的中间层通常存在许多 `None` 。由于层序遍历序列并不包含这些 `None` ,因此我们无法仅凭该序列来推测 `None` 的数量和分布位置。**这意味着存在多种二叉树结构都符合该层序遍历序列**。
|
||||
|
||||
如下图所示,给定一棵非完美二叉树,上述数组表示方法已经失效。
|
||||
|
||||

|
||||
|
||||
为了解决此问题,**我们可以考虑在层序遍历序列中显式地写出所有 $\text{None}$** 。如下图所示,这样处理后,层序遍历序列就可以唯一表示二叉树了。示例代码如下:
|
||||
为了解决此问题,**我们可以考虑在层序遍历序列中显式地写出所有 `None`** 。如下图所示,这样处理后,层序遍历序列就可以唯一表示二叉树了。示例代码如下:
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -120,9 +120,9 @@
|
||||
|
||||

|
||||
|
||||
值得说明的是,**完全二叉树非常适合使用数组来表示**。回顾完全二叉树的定义,$\text{None}$ 只出现在最底层且靠右的位置,**因此所有 $\text{None}$ 一定出现在层序遍历序列的末尾**。
|
||||
值得说明的是,**完全二叉树非常适合使用数组来表示**。回顾完全二叉树的定义,`None` 只出现在最底层且靠右的位置,**因此所有 `None` 一定出现在层序遍历序列的末尾**。
|
||||
|
||||
这意味着使用数组表示完全二叉树时,可以省略存储所有 $\text{None}$ ,非常方便。下图给出了一个例子。
|
||||
这意味着使用数组表示完全二叉树时,可以省略存储所有 `None` ,非常方便。下图给出了一个例子。
|
||||
|
||||

|
||||
|
||||
@@ -147,4 +147,4 @@
|
||||
|
||||
- 数组存储需要连续内存空间,因此不适合存储数据量过大的树。
|
||||
- 增删节点需要通过数组插入与删除操作实现,效率较低。
|
||||
- 当二叉树中存在大量 $\text{None}$ 时,数组中包含的节点数据比重较低,空间利用率较低。
|
||||
- 当二叉树中存在大量 `None` 时,数组中包含的节点数据比重较低,空间利用率较低。
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# AVL 树 *
|
||||
|
||||
在“二叉搜索树”章节中,我们提到,在多次插入和删除操作后,二叉搜索树可能退化为链表。在这种情况下,所有操作的时间复杂度将从 $O(\log n)$ 恶化为 $O(n)$ 。
|
||||
在“二叉搜索树”章节中我们提到,在多次插入和删除操作后,二叉搜索树可能退化为链表。在这种情况下,所有操作的时间复杂度将从 $O(\log n)$ 劣化为 $O(n)$ 。
|
||||
|
||||
如下图所示,经过两次删除节点操作,这棵二叉搜索树便会退化为链表。
|
||||
|
||||

|
||||
|
||||
再例如,在下图所示的完美二叉树中插入两个节点后,树将严重向左倾斜,查找操作的时间复杂度也随之恶化。
|
||||
再例如,在下图所示的完美二叉树中插入两个节点后,树将严重向左倾斜,查找操作的时间复杂度也随之劣化。
|
||||
|
||||

|
||||
|
||||
1962 年 G. M. Adelson-Velsky 和 E. M. Landis 在论文 "An algorithm for the organization of information" 中提出了「AVL 树」。论文中详细描述了一系列操作,确保在持续添加和删除节点后,AVL 树不会退化,从而使得各种操作的时间复杂度保持在 $O(\log n)$ 级别。换句话说,在需要频繁进行增删查改操作的场景中,AVL 树能始终保持高效的数据操作性能,具有很好的应用价值。
|
||||
1962 年 G. M. Adelson-Velsky 和 E. M. Landis 在论文“An algorithm for the organization of information”中提出了「AVL 树」。论文中详细描述了一系列操作,确保在持续添加和删除节点后,AVL 树不会退化,从而使得各种操作的时间复杂度保持在 $O(\log n)$ 级别。换句话说,在需要频繁进行增删查改操作的场景中,AVL 树能始终保持高效的数据操作性能,具有很好的应用价值。
|
||||
|
||||
## AVL 树常见术语
|
||||
|
||||
@@ -206,7 +206,7 @@ AVL 树既是二叉搜索树也是平衡二叉树,同时满足这两类二叉
|
||||
|
||||
```
|
||||
|
||||
“节点高度”是指从该节点到其最远叶节点的距离,即所经过的“边”的数量。需要特别注意的是,叶节点的高度为 $0$ ,而空节点的高度为 $-1$ 。我们将创建两个工具函数,分别用于获取和更新节点的高度:
|
||||
“节点高度”是指从该节点到它的最远叶节点的距离,即所经过的“边”的数量。需要特别注意的是,叶节点的高度为 $0$ ,而空节点的高度为 $-1$ 。我们将创建两个工具函数,分别用于获取和更新节点的高度:
|
||||
|
||||
```src
|
||||
[file]{avl_tree}-[class]{a_v_l_tree}-[func]{update_height}
|
||||
@@ -246,9 +246,9 @@ AVL 树的特点在于“旋转”操作,它能够在不影响二叉树的中
|
||||
=== "<4>"
|
||||

|
||||
|
||||
如下图所示,当节点 `child` 有右子节点(记为 `grandChild` )时,需要在右旋中添加一步:将 `grandChild` 作为 `node` 的左子节点。
|
||||
如下图所示,当节点 `child` 有右子节点(记为 `grand_child` )时,需要在右旋中添加一步:将 `grand_child` 作为 `node` 的左子节点。
|
||||
|
||||

|
||||

|
||||
|
||||
“向右旋转”是一种形象化的说法,实际上需要通过修改节点指针来实现,代码如下所示:
|
||||
|
||||
@@ -262,9 +262,9 @@ AVL 树的特点在于“旋转”操作,它能够在不影响二叉树的中
|
||||
|
||||

|
||||
|
||||
同理,如下图所示,当节点 `child` 有左子节点(记为 `grandChild` )时,需要在左旋中添加一步:将 `grandChild` 作为 `node` 的右子节点。
|
||||
同理,如下图所示,当节点 `child` 有左子节点(记为 `grand_child` )时,需要在左旋中添加一步:将 `grand_child` 作为 `node` 的右子节点。
|
||||
|
||||

|
||||

|
||||
|
||||
可以观察到,**右旋和左旋操作在逻辑上是镜像对称的,它们分别解决的两种失衡情况也是对称的**。基于对称性,我们只需将右旋的实现代码中的所有的 `left` 替换为 `right` ,将所有的 `right` 替换为 `left` ,即可得到左旋的实现代码:
|
||||
|
||||
|
||||
@@ -41,15 +41,15 @@
|
||||
|
||||
给定一个待插入元素 `num` ,为了保持二叉搜索树“左子树 < 根节点 < 右子树”的性质,插入操作流程如下图所示。
|
||||
|
||||
1. **查找插入位置**:与查找操作相似,从根节点出发,根据当前节点值和 `num` 的大小关系循环向下搜索,直到越过叶节点(遍历至 $\text{None}$ )时跳出循环。
|
||||
2. **在该位置插入节点**:初始化节点 `num` ,将该节点置于 $\text{None}$ 的位置。
|
||||
1. **查找插入位置**:与查找操作相似,从根节点出发,根据当前节点值和 `num` 的大小关系循环向下搜索,直到越过叶节点(遍历至 `None` )时跳出循环。
|
||||
2. **在该位置插入节点**:初始化节点 `num` ,将该节点置于 `None` 的位置。
|
||||
|
||||

|
||||
|
||||
在代码实现中,需要注意以下两点。
|
||||
|
||||
- 二叉搜索树不允许存在重复节点,否则将违反其定义。因此,若待插入节点在树中已存在,则不执行插入,直接返回。
|
||||
- 为了实现插入节点,我们需要借助节点 `pre` 保存上一轮循环的节点。这样在遍历至 $\text{None}$ 时,我们可以获取到其父节点,从而完成节点插入操作。
|
||||
- 为了实现插入节点,我们需要借助节点 `pre` 保存上一轮循环的节点。这样在遍历至 `None` 时,我们可以获取到其父节点,从而完成节点插入操作。
|
||||
|
||||
```src
|
||||
[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{insert}
|
||||
@@ -59,11 +59,7 @@
|
||||
|
||||
### 删除节点
|
||||
|
||||
先在二叉树中查找到目标节点,再将其删除。
|
||||
|
||||
与插入节点类似,我们需要保证在删除操作完成后,二叉搜索树的“左子树 < 根节点 < 右子树”的性质仍然满足。
|
||||
|
||||
因此,我们根据目标节点的子节点数量,分 0、1 和 2 三种情况,执行对应的删除节点操作。
|
||||
先在二叉树中查找到目标节点,再将其删除。与插入节点类似,我们需要保证在删除操作完成后,二叉搜索树的“左子树 < 根节点 < 右子树”的性质仍然满足。因此,我们根据目标节点的子节点数量,分 0、1 和 2 三种情况,执行对应的删除节点操作。
|
||||
|
||||
如下图所示,当待删除节点的度为 $0$ 时,表示该节点是叶节点,可以直接删除。
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@
|
||||
二叉树的常用术语如下图所示。
|
||||
|
||||
- 「根节点 root node」:位于二叉树顶层的节点,没有父节点。
|
||||
- 「叶节点 leaf node」:没有子节点的节点,其两个指针均指向 $\text{None}$ 。
|
||||
- 「叶节点 leaf node」:没有子节点的节点,其两个指针均指向 `None` 。
|
||||
- 「边 edge」:连接两个节点的线段,即节点引用(指针)。
|
||||
- 节点所在的「层 level」:从顶至底递增,根节点所在层为 1 。
|
||||
- 节点的「度 degree」:节点的子节点的数量。在二叉树中,度的取值范围是 0、1、2 。
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
如下图所示,「层序遍历 level-order traversal」从顶部到底部逐层遍历二叉树,并在每一层按照从左到右的顺序访问节点。
|
||||
|
||||
层序遍历本质上属于「广度优先遍历 breadth-first traversal, BFS」,它体现了一种“一圈一圈向外扩展”的逐层遍历方式。
|
||||
层序遍历本质上属于「广度优先遍历 breadth-first traversal」,也称「广度优先搜索 breadth-first search, BFS」,它体现了一种“一圈一圈向外扩展”的逐层遍历方式。
|
||||
|
||||

|
||||
|
||||
@@ -22,12 +22,12 @@
|
||||
|
||||
### 复杂度分析
|
||||
|
||||
- **时间复杂度 $O(n)$** :所有节点被访问一次,使用 $O(n)$ 时间,其中 $n$ 为节点数量。
|
||||
- **空间复杂度 $O(n)$** :在最差情况下,即满二叉树时,遍历到最底层之前,队列中最多同时存在 $(n + 1) / 2$ 个节点,占用 $O(n)$ 空间。
|
||||
- **时间复杂度为 $O(n)$** :所有节点被访问一次,使用 $O(n)$ 时间,其中 $n$ 为节点数量。
|
||||
- **空间复杂度为 $O(n)$** :在最差情况下,即满二叉树时,遍历到最底层之前,队列中最多同时存在 $(n + 1) / 2$ 个节点,占用 $O(n)$ 空间。
|
||||
|
||||
## 前序、中序、后序遍历
|
||||
|
||||
相应地,前序、中序和后序遍历都属于「深度优先遍历 depth-first traversal, DFS」,它体现了一种“先走到尽头,再回溯继续”的遍历方式。
|
||||
相应地,前序、中序和后序遍历都属于「深度优先遍历 depth-first traversal」,也称「深度优先搜索 depth-first search, DFS」,它体现了一种“先走到尽头,再回溯继续”的遍历方式。
|
||||
|
||||
下图展示了对二叉树进行深度优先遍历的工作原理。**深度优先遍历就像是绕着整棵二叉树的外围“走”一圈**,在每个节点都会遇到三个位置,分别对应前序遍历、中序遍历和后序遍历。
|
||||
|
||||
@@ -85,5 +85,5 @@
|
||||
|
||||
### 复杂度分析
|
||||
|
||||
- **时间复杂度 $O(n)$** :所有节点被访问一次,使用 $O(n)$ 时间。
|
||||
- **空间复杂度 $O(n)$** :在最差情况下,即树退化为链表时,递归深度达到 $n$ ,系统占用 $O(n)$ 栈帧空间。
|
||||
- **时间复杂度为 $O(n)$** :所有节点被访问一次,使用 $O(n)$ 时间。
|
||||
- **空间复杂度为 $O(n)$** :在最差情况下,即树退化为链表时,递归深度达到 $n$ ,系统占用 $O(n)$ 栈帧空间。
|
||||
|
||||
Reference in New Issue
Block a user