mirror of
https://github.com/krahets/hello-algo.git
synced 2025-12-19 07:17:54 +08:00
Update the captions of all the figures.
This commit is contained in:
@@ -4,11 +4,11 @@
|
||||
|
||||
如下图所示,执行两步删除结点后,该二叉搜索树就会退化为链表。
|
||||
|
||||

|
||||

|
||||
|
||||
再比如,在以下完美二叉树中插入两个结点后,树严重向左偏斜,查找操作的时间复杂度也随之发生劣化。
|
||||
|
||||

|
||||

|
||||
|
||||
G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorithm for the organization of information" 中提出了「AVL 树」。**论文中描述了一系列操作,使得在不断添加与删除结点后,AVL 树仍然不会发生退化**,进而使得各种操作的时间复杂度均能保持在 $O(\log n)$ 级别。
|
||||
|
||||
@@ -323,7 +323,7 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影
|
||||
|
||||
进而,如果结点 `child` 本身有右子结点(记为 `grandChild` ),则需要在「右旋」中添加一步:将 `grandChild` 作为 `node` 的左子结点。
|
||||
|
||||

|
||||

|
||||
|
||||
“向右旋转”是一种形象化的说法,实际需要通过修改结点指针实现,代码如下所示。
|
||||
|
||||
@@ -391,11 +391,11 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影
|
||||
|
||||
类似地,如果将取上述失衡二叉树的“镜像”,那么则需要「左旋」操作。
|
||||
|
||||

|
||||

|
||||
|
||||
同理,若结点 `child` 本身有左子结点(记为 `grandChild` ),则需要在「左旋」中添加一步:将 `grandChild` 作为 `node` 的右子结点。
|
||||
|
||||

|
||||

|
||||
|
||||
观察发现,**「左旋」和「右旋」操作是镜像对称的,两者对应解决的两种失衡情况也是对称的**。根据对称性,我们可以很方便地从「右旋」推导出「左旋」。具体地,只需将「右旋」代码中的把所有的 `left` 替换为 `right` 、所有的 `right` 替换为 `left` ,即可得到「左旋」代码。
|
||||
|
||||
@@ -463,19 +463,19 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影
|
||||
|
||||
对于下图的失衡结点 3 ,**单一使用左旋或右旋都无法使子树恢复平衡**,此时需要「先左旋后右旋」,即先对 `child` 执行「左旋」,再对 `node` 执行「右旋」。
|
||||
|
||||

|
||||

|
||||
|
||||
### Case 4 - 先右后左
|
||||
|
||||
同理,取以上失衡二叉树的镜像,则需要「先右旋后左旋」,即先对 `child` 执行「右旋」,然后对 `node` 执行「左旋」。
|
||||
|
||||

|
||||

|
||||
|
||||
### 旋转的选择
|
||||
|
||||
下图描述的四种失衡情况与上述 Cases 逐个对应,分别需采用 **右旋、左旋、先右后左、先左后右** 的旋转操作。
|
||||
|
||||

|
||||

|
||||
|
||||
具体地,在代码中使用 **失衡结点的平衡因子、较高一侧子结点的平衡因子** 来确定失衡结点属于上图中的哪种情况。
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
1. 对于根结点,左子树中所有结点的值 $<$ 根结点的值 $<$ 右子树中所有结点的值;
|
||||
2. 任意结点的左子树和右子树也是二叉搜索树,即也满足条件 `1.` ;
|
||||
|
||||

|
||||

|
||||
|
||||
## 二叉搜索树的操作
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
|
||||
二叉搜索树不允许存在重复结点,否则将会违背其定义。因此若待插入结点在树中已经存在,则不执行插入,直接返回即可。
|
||||
|
||||

|
||||

|
||||
|
||||
=== "Java"
|
||||
|
||||
@@ -172,11 +172,11 @@
|
||||
|
||||
**当待删除结点的子结点数量 $= 0$ 时**,表明待删除结点是叶结点,直接删除即可。
|
||||
|
||||

|
||||

|
||||
|
||||
**当待删除结点的子结点数量 $= 1$ 时**,将待删除结点替换为其子结点即可。
|
||||
|
||||

|
||||

|
||||
|
||||
**当待删除结点的子结点数量 $= 2$ 时**,删除操作分为三步:
|
||||
|
||||
@@ -284,7 +284,7 @@
|
||||
|
||||
借助中序遍历升序的性质,我们在二叉搜索树中获取有序数据仅需 $O(n)$ 时间,而无需额外排序,非常高效。
|
||||
|
||||

|
||||

|
||||
|
||||
## 二叉搜索树的效率
|
||||
|
||||
@@ -325,7 +325,7 @@
|
||||
|
||||
在实际应用中,如何保持二叉搜索树的平衡,也是一个需要重要考虑的问题。
|
||||
|
||||

|
||||

|
||||
|
||||
## 二叉搜索树常见应用
|
||||
|
||||
|
||||
@@ -127,9 +127,7 @@
|
||||
|
||||
除了叶结点外,每个结点都有子结点和子树。例如,若将下图的「结点 2」看作父结点,那么其左子结点和右子结点分别为「结点 4」和「结点 5」,左子树和右子树分别为「结点 4 及其以下结点形成的树」和「结点 5 及其以下结点形成的树」。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 子结点与子树 </p>
|
||||

|
||||
|
||||
## 二叉树常见术语
|
||||
|
||||
@@ -144,9 +142,7 @@
|
||||
- 结点「深度 Depth」 :根结点到该结点走过边的数量;
|
||||
- 结点「高度 Height」:最远叶结点到该结点走过边的数量;
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 二叉树的常见术语 </p>
|
||||

|
||||
|
||||
!!! tip "高度与深度的定义"
|
||||
|
||||
@@ -304,9 +300,7 @@
|
||||
|
||||
**插入与删除结点**。与链表类似,插入与删除结点都可以通过修改指针实现。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 在二叉树中插入与删除结点 </p>
|
||||

|
||||
|
||||
=== "Java"
|
||||
|
||||
@@ -428,7 +422,7 @@
|
||||
|
||||
在中文社区中,完美二叉树常被称为「满二叉树」,请注意与完满二叉树区分。
|
||||
|
||||

|
||||

|
||||
|
||||
### 完全二叉树
|
||||
|
||||
@@ -436,19 +430,19 @@
|
||||
|
||||
**完全二叉树非常适合用数组来表示**。如果按照层序遍历序列的顺序来存储,那么空结点 `null` 一定全部出现在序列的尾部,因此我们就可以不用存储这些 null 了。
|
||||
|
||||

|
||||

|
||||
|
||||
### 完满二叉树
|
||||
|
||||
「完满二叉树 Full Binary Tree」除了叶结点之外,其余所有结点都有两个子结点。
|
||||
|
||||

|
||||

|
||||
|
||||
### 平衡二叉树
|
||||
|
||||
「平衡二叉树 Balanced Binary Tree」中任意结点的左子树和右子树的高度之差的绝对值 $\leq 1$ 。
|
||||
|
||||

|
||||

|
||||
|
||||
## 二叉树的退化
|
||||
|
||||
@@ -457,9 +451,7 @@
|
||||
- 完美二叉树是一个二叉树的“最佳状态”,可以完全发挥出二叉树“分治”的优势;
|
||||
- 链表则是另一个极端,各项操作都变为线性操作,时间复杂度退化至 $O(n)$ ;
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 二叉树的最佳和最差结构 </p>
|
||||

|
||||
|
||||
如下表所示,在最佳和最差结构下,二叉树的叶结点数量、结点总数、高度等达到极大或极小值。
|
||||
|
||||
@@ -482,11 +474,11 @@
|
||||
|
||||
**本质上,映射公式的作用就是链表中的指针**。对于层序遍历序列中的任意结点,我们都可以使用映射公式来访问子结点。因此,可以直接使用层序遍历序列(即数组)来表示完美二叉树。
|
||||
|
||||

|
||||

|
||||
|
||||
然而,完美二叉树只是个例,二叉树中间层往往存在许多空结点(即 `null` ),而层序遍历序列并不包含这些空结点,并且我们无法单凭序列来猜测空结点的数量和分布位置,**即理论上存在许多种二叉树都符合该层序遍历序列**。显然,这种情况无法使用数组来存储二叉树。
|
||||
|
||||

|
||||

|
||||
|
||||
为了解决此问题,考虑按照完美二叉树的形式来表示所有二叉树,**即在序列中使用特殊符号来显式地表示“空位”**。如下图所示,这样处理后,序列(数组)就可以唯一表示二叉树了。
|
||||
|
||||
@@ -565,10 +557,10 @@
|
||||
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
回顾「完全二叉树」的定义,其只有最底层有空结点,并且最底层的结点尽量靠左,因而所有空结点都一定出现在层序遍历序列的末尾。**因为我们先验地确定了空位的位置,所以在使用数组表示完全二叉树时,可以省略存储“空位”**。因此,完全二叉树非常适合使用数组来表示。
|
||||
|
||||

|
||||

|
||||
|
||||
数组表示有两个优点: 一是不需要存储指针,节省空间;二是可以随机访问结点。然而,当二叉树中的“空位”很多时,数组中只包含很少结点的数据,空间利用率很低。
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
层序遍历本质上是「广度优先搜索 Breadth-First Traversal」,其体现着一种“一圈一圈向外”的层进遍历方式。
|
||||
|
||||

|
||||

|
||||
|
||||
<p align="center"> Fig. 二叉树的层序遍历 </p>
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
|
||||
如下图所示,左侧是深度优先遍历的的示意图,右上方是对应的递归实现代码。深度优先遍历就像是绕着整个二叉树的外围“走”一圈,走的过程中,在每个结点都会遇到三个位置,分别对应前序遍历、中序遍历、后序遍历。
|
||||
|
||||

|
||||

|
||||
|
||||
<p align="center"> Fig. 二叉树的前 / 中 / 后序遍历 </p>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user