This commit is contained in:
krahets
2023-08-20 23:27:50 +08:00
parent 68e11cfa34
commit 4d55cafd13
46 changed files with 174 additions and 162 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ comments: true
在链表表示下,二叉树的存储单元为节点 `TreeNode` ,节点之间通过指针相连接。在上节中,我们学习了在链表表示下的二叉树的各项基本操作。
那么,能否用数组来表示二叉树呢?答案是肯定的。
那么,我们能否用数组来表示二叉树呢?答案是肯定的。
## 7.3.1   表示完美二叉树
+14 -13
View File
@@ -22,7 +22,7 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit
## 7.5.1   AVL 树常见术语
AVL 树既是二叉搜索树也是平衡二叉树,同时满足这两类二叉树的所有性质,因此也被称为「平衡二叉搜索树」。
AVL 树既是二叉搜索树也是平衡二叉树,同时满足这两类二叉树的所有性质,因此也被称为「平衡二叉搜索树 balanced binary search tree」。
### 1.   节点高度
@@ -196,7 +196,7 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit
```
节点高度是指从该节点到最远叶节点的距离,即所经过的“边”的数量。需要特别注意的是,叶节点的高度为 0 ,而空节点的高度为 -1 。我们将创建两个工具函数,分别用于获取和更新节点的高度。
节点高度是指从该节点到最远叶节点的距离,即所经过的“边”的数量。需要特别注意的是,叶节点的高度为 0 ,而空节点的高度为 -1 。我们将创建两个工具函数,分别用于获取和更新节点的高度。
=== "Java"
@@ -420,7 +420,7 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit
### 2.   节点平衡因子
节点的「平衡因子 Balance Factor」定义为节点左子树的高度减去右子树的高度,同时规定空节点的平衡因子为 0 。我们同样将获取节点平衡因子的功能封装成函数,方便后续使用。
节点的「平衡因子 balance factor」定义为节点左子树的高度减去右子树的高度,同时规定空节点的平衡因子为 0 。我们同样将获取节点平衡因子的功能封装成函数,方便后续使用。
=== "Java"
@@ -582,13 +582,13 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit
## 7.5.2   AVL 树旋转
AVL 树的特点在于旋转 Rotation」操作,它能够在不影响二叉树的中序遍历序列的前提下,使失衡节点重新恢复平衡。换句话说,**旋转操作既能保持树的「二叉搜索树」属性,也能使树重新变为平衡二叉树**。
AVL 树的特点在于旋转操作,它能够在不影响二叉树的中序遍历序列的前提下,使失衡节点重新恢复平衡。换句话说,**旋转操作既能保持二叉搜索树”的性质,也能使树重新变为平衡二叉树**。
我们将平衡因子绝对值 $> 1$ 的节点称为失衡节点。根据节点失衡情况的不同,旋转操作分为四种:右旋、左旋、先右旋后左旋、先左旋后右旋。下面我们将详细介绍这些旋转操作。
我们将平衡因子绝对值 $> 1$ 的节点称为失衡节点。根据节点失衡情况的不同,旋转操作分为四种:右旋、左旋、先右旋后左旋、先左旋后右旋。下面我们将详细介绍这些旋转操作。
### 1.   右旋
如下图所示,节点下方为平衡因子。从底至顶看,二叉树中首个失衡节点是“节点 3”。我们关注以该失衡节点为根节点的子树,将该节点记为 `node` ,其左子节点记为 `child` ,执行右旋操作。完成右旋后,子树已经恢复平衡,并且仍然保持二叉搜索树的特性。
如下图所示,节点下方为平衡因子。从底至顶看,二叉树中首个失衡节点是“节点 3”。我们关注以该失衡节点为根节点的子树,将该节点记为 `node` ,其左子节点记为 `child` ,执行右旋操作。完成右旋后,子树已经恢复平衡,并且仍然保持二叉搜索树的特性。
=== "<1>"
![右旋操作步骤](avl_tree.assets/avltree_right_rotate_step1.png)
@@ -604,7 +604,7 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉
<p align="center"> 图:右旋操作步骤 </p>
此外,如果节点 `child` 本身有右子节点(记为 `grandChild` ),则需要在右旋中添加一步:将 `grandChild` 作为 `node` 的左子节点。
此外,如果节点 `child` 本身有右子节点(记为 `grandChild` ),则需要在右旋中添加一步:将 `grandChild` 作为 `node` 的左子节点。
![有 grandChild 的右旋操作](avl_tree.assets/avltree_right_rotate_with_grandchild.png)
@@ -835,19 +835,19 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉
### 2. &nbsp; 左旋
相应的,如果考虑上述失衡二叉树的“镜像”,则需要执行左旋操作。
相应的,如果考虑上述失衡二叉树的“镜像”,则需要执行左旋操作。
![左旋操作](avl_tree.assets/avltree_left_rotate.png)
<p align="center"> 图:左旋操作 </p>
同理,若节点 `child` 本身有左子节点(记为 `grandChild` ),则需要在左旋中添加一步:将 `grandChild` 作为 `node` 的右子节点。
同理,若节点 `child` 本身有左子节点(记为 `grandChild` ),则需要在左旋中添加一步:将 `grandChild` 作为 `node` 的右子节点。
![有 grandChild 的左旋操作](avl_tree.assets/avltree_left_rotate_with_grandchild.png)
<p align="center"> 图:有 grandChild 的左旋操作 </p>
可以观察到,**右旋和左旋操作在逻辑上是镜像对称的,它们分别解决的两种失衡情况也是对称的**。基于对称性,我们可以轻松地从右旋的代码推导出左旋的代码。具体地,只需将右旋代码中的所有的 `left` 替换为 `right` ,将所有的 `right` 替换为 `left` ,即可得到左旋代码。
可以观察到,**右旋和左旋操作在逻辑上是镜像对称的,它们分别解决的两种失衡情况也是对称的**。基于对称性,我们只需将右旋的实现代码中的所有的 `left` 替换为 `right` ,将所有的 `right` 替换为 `left` ,即可得到左旋的实现代码。
=== "Java"
@@ -1072,7 +1072,7 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉
### 3. &nbsp; 先左旋后右旋
对于下图中的失衡节点 3,仅使用左旋或右旋都无法使子树恢复平衡。此时需要先左旋后右旋,即先对 `child` 执行左旋,再对 `node` 执行右旋
对于下图中的失衡节点 3,仅使用左旋或右旋都无法使子树恢复平衡。此时需要先左旋后右旋,即先对 `child` 执行左旋,再对 `node` 执行右旋
![先左旋后右旋](avl_tree.assets/avltree_left_right_rotate.png)
@@ -1080,7 +1080,7 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉
### 4. &nbsp; 先右旋后左旋
同理,对于上述失衡二叉树的镜像情况,需要先右旋后左旋,即先对 `child` 执行右旋,然后对 `node` 执行左旋
同理,对于上述失衡二叉树的镜像情况,需要先右旋后左旋,即先对 `child` 执行右旋,然后对 `node` 执行左旋
![先右旋后左旋](avl_tree.assets/avltree_right_left_rotate.png)
@@ -1095,6 +1095,7 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉
<p align="center"> 图:AVL 树的四种旋转情况 </p>
在代码中,我们通过判断失衡节点的平衡因子以及较高一侧子节点的平衡因子的正负号,来确定失衡节点属于上图中的哪种情况。
<p align="center"> 表:四种旋转情况的选择条件 </p>
<div class="center-table" markdown>
@@ -1523,7 +1524,7 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉
### 1. &nbsp; 插入节点
AVL 树的节点插入操作与二叉搜索树在主体上类似。唯一的区别在于,在 AVL 树中插入节点后,从该节点到根节点的路径上可能会出现一系列失衡节点。因此,**我们需要从这个节点开始,自底向上执行旋转操作,使所有失衡节点恢复平衡**。
AVL 树的节点插入操作与二叉搜索树在主体上类似。唯一的区别在于,在 AVL 树中插入节点后,从该节点到根节点的路径上可能会出现一系列失衡节点。因此,**我们需要从这个节点开始,自底向上执行旋转操作,使所有失衡节点恢复平衡**。
=== "Java"
+2 -1
View File
@@ -4,7 +4,7 @@ comments: true
# 7.4 &nbsp; 二叉搜索树
「二叉搜索树 Binary Search Tree」满足以下条件:
「二叉搜索树 binary search tree」满足以下条件:
1. 对于根节点,左子树中所有节点的值 $<$ 根节点的值 $<$ 右子树中所有节点的值。
2. 任意节点的左、右子树也是二叉搜索树,即同样满足条件 `1.`
@@ -1489,6 +1489,7 @@ comments: true
给定一组数据,我们考虑使用数组或二叉搜索树存储。
观察可知,二叉搜索树的各项操作的时间复杂度都是对数阶,具有稳定且高效的性能表现。只有在高频添加、低频查找删除的数据适用场景下,数组比二叉搜索树的效率更高。
<p align="center"> 表:数组与搜索树的效率对比 </p>
<div class="center-table" markdown>
+19 -18
View File
@@ -4,7 +4,7 @@ comments: true
# 7.1 &nbsp; 二叉树
「二叉树 Binary Tree」是一种非线性数据结构,代表着祖先与后代之间的派生关系,体现着“一分为二”的分治逻辑。与链表类似,二叉树的基本单元是节点,每个节点包含:值、左子节点引用、右子节点引用。
「二叉树 binary tree」是一种非线性数据结构,代表着祖先与后代之间的派生关系,体现着“一分为二”的分治逻辑。与链表类似,二叉树的基本单元是节点,每个节点包含:值、左子节点引用、右子节点引用。
=== "Java"
@@ -165,9 +165,9 @@ comments: true
```
节点的两个指针分别指向「左子节点」和「右子节点」,同时该节点被称为这两个子节点的「父节点」。当给定一个二叉树的节点时,我们将该节点的左子节点及其以下节点形成的树称为该节点的「左子树」,同理可得「右子树」。
每个节点都有两个引用(指针),分别指向「左子节点 left-child node」和「右子节点 right-child node」,该节点被称为这两个子节点的「父节点 parent node」。当给定一个二叉树的节点时,我们将该节点的左子节点及其以下节点形成的树称为该节点的「左子树 left subtree」,同理可得「右子树 right subtree」。
**在二叉树中,除叶节点外,其他所有节点都包含子节点和非空子树**。例如,在以下示例中,若将“节点 2”视为父节点,则其左子节点和右子节点分别是“节点 4”和“节点 5”,左子树是“节点 4 及其以下节点形成的树”,右子树是“节点 5 及其以下节点形成的树”。
**在二叉树中,除叶节点外,其他所有节点都包含子节点和非空子树**。在以下示例中,若将“节点 2”视为父节点,则其左子节点和右子节点分别是“节点 4”和“节点 5”,左子树是“节点 4 及其以下节点形成的树”,右子树是“节点 5 及其以下节点形成的树”。
![父节点、子节点、子树](binary_tree.assets/binary_tree_definition.png)
@@ -177,14 +177,14 @@ comments: true
二叉树涉及的术语较多,建议尽量理解并记住。
- 「根节点 Root Node」:位于二叉树顶层的节点,没有父节点。
- 「叶节点 Leaf Node」:没有子节点的节点,其两个指针均指向 $\text{None}$ 。
- 节点的「层 Level」:从顶至底递增,根节点所在层为 1
- 节点的「度 Degree」:节点的子节点的数量。在二叉树中,度的范围是 0, 1, 2
- 「边 Edge」:连接两个节点的线段,即节点指针
- 二叉树的「高度」:从根节点到最远叶节点所经过的边的数量。
- 节点的「深度 Depth」 :从根节点到该节点所经过的边的数量。
- 节点的「高度 Height」:从最远叶节点到该节点所经过的边的数量。
- 「根节点 root node」:位于二叉树顶层的节点,没有父节点。
- 「叶节点 leaf node」:没有子节点的节点,其两个指针均指向 $\text{None}$ 。
- 「边 edge」:连接两个节点的线段,即节点引用(指针)
- 节点所在的「层 level」:从顶至底递增,根节点所在层为 1
- 节点的「度 degree」:节点的子节点的数量。在二叉树中,度的取值范围是 0, 1, 2
- 二叉树的「高度 height」:从根节点到最远叶节点所经过的边的数量。
- 节点的「深度 depth」 :从根节点到该节点所经过的边的数量。
- 节点的「高度 height」:从最远叶节点到该节点所经过的边的数量。
![二叉树的常用术语](binary_tree.assets/binary_tree_terminology.png)
@@ -192,7 +192,7 @@ comments: true
!!! tip "高度与深度的定义"
请注意,我们通常将高度」和「深度定义为“走过边的数量”,但有些题目或教材可能会将其定义为“走过节点的数量”。在这种情况下,高度和深度都需要加 1 。
请注意,我们通常将高度”和“深度定义为“走过边的数量”,但有些题目或教材可能会将其定义为“走过节点的数量”。在这种情况下,高度和深度都需要加 1 。
## 7.1.2 &nbsp; 二叉树基本操作
@@ -522,11 +522,11 @@ comments: true
### 1. &nbsp; 完美二叉树
「完美二叉树 Perfect Binary Tree」除了最底层外,其余所有层的节点都被完全填满。在完美二叉树中,叶节点的度为 $0$ ,其余所有节点的度都为 $2$ ;若树高度为 $h$ ,则节点总数为 $2^{h+1} - 1$ ,呈现标准的指数级关系,反映了自然界中常见的细胞分裂现象。
「完美二叉树 perfect binary tree」除了最底层外,其余所有层的节点都被完全填满。在完美二叉树中,叶节点的度为 $0$ ,其余所有节点的度都为 $2$ ;若树高度为 $h$ ,则节点总数为 $2^{h+1} - 1$ ,呈现标准的指数级关系,反映了自然界中常见的细胞分裂现象。
!!! tip
在中文社区中,完美二叉树常被称为「满二叉树」,请注意区分
请注意,在中文社区中,完美二叉树常被称为「满二叉树」。
![完美二叉树](binary_tree.assets/perfect_binary_tree.png)
@@ -534,7 +534,7 @@ comments: true
### 2. &nbsp; 完全二叉树
「完全二叉树 Complete Binary Tree」只有最底层的节点未被填满,且最底层节点尽量靠左填充。
「完全二叉树 complete binary tree」只有最底层的节点未被填满,且最底层节点尽量靠左填充。
![完全二叉树](binary_tree.assets/complete_binary_tree.png)
@@ -542,7 +542,7 @@ comments: true
### 3. &nbsp; 完满二叉树
「完满二叉树 Full Binary Tree」除了叶节点之外,其余所有节点都有两个子节点。
「完满二叉树 full binary tree」除了叶节点之外,其余所有节点都有两个子节点。
![完满二叉树](binary_tree.assets/full_binary_tree.png)
@@ -550,7 +550,7 @@ comments: true
### 4. &nbsp; 平衡二叉树
「平衡二叉树 Balanced Binary Tree」中任意节点的左子树和右子树的高度之差的绝对值不超过 1 。
「平衡二叉树 balanced binary tree」中任意节点的左子树和右子树的高度之差的绝对值不超过 1 。
![平衡二叉树](binary_tree.assets/balanced_binary_tree.png)
@@ -558,7 +558,7 @@ comments: true
## 7.1.4 &nbsp; 二叉树的退化
当二叉树的每层节点都被填满时,达到完美二叉树;而当所有节点都偏向一侧时,二叉树退化为链表
当二叉树的每层节点都被填满时,达到完美二叉树;而当所有节点都偏向一侧时,二叉树退化为链表
- 完美二叉树是理想情况,可以充分发挥二叉树“分治”的优势。
- 链表则是另一个极端,各项操作都变为线性操作,时间复杂度退化至 $O(n)$ 。
@@ -568,6 +568,7 @@ comments: true
<p align="center"> 图:二叉树的最佳与最差结构 </p>
如下表所示,在最佳和最差结构下,二叉树的叶节点数量、节点总数、高度等达到极大或极小值。
<p align="center"> 表:二叉树的最佳与最差情况 </p>
<div class="center-table" markdown>
+4 -4
View File
@@ -10,15 +10,15 @@ comments: true
## 7.2.1 &nbsp; 层序遍历
「层序遍历 Level-Order Traversal」从顶部到底部逐层遍历二叉树,并在每一层按照从左到右的顺序访问节点。
「层序遍历 level-order traversal」从顶部到底部逐层遍历二叉树,并在每一层按照从左到右的顺序访问节点。
层序遍历本质上属于「广度优先搜索 Breadth-First Traversal」,它体现了一种“一圈一圈向外扩展”的逐层搜索方式。
层序遍历本质上属于「广度优先遍历 breadth-first traversal」,它体现了一种“一圈一圈向外扩展”的逐层遍历方式。
![二叉树的层序遍历](binary_tree_traversal.assets/binary_tree_bfs.png)
<p align="center"> 图:二叉树的层序遍历 </p>
广度优先遍历通常借助队列来实现。队列遵循“先进先出”的规则,而广度优先遍历则遵循“逐层推进”的规则,两者背后的思想是一致的。
广度优先遍历通常借助队列来实现。队列遵循“先进先出”的规则,而广度优先遍历则遵循“逐层推进”的规则,两者背后的思想是一致的。
=== "Java"
@@ -328,7 +328,7 @@ comments: true
## 7.2.2 &nbsp; 前序、中序、后序遍历
相应地,前序、中序和后序遍历都属于「深度优先遍历 Depth-First Traversal」,它体现了一种“先走到尽头,再回溯继续”的遍历方式。
相应地,前序、中序和后序遍历都属于「深度优先遍历 depth-first traversal」,它体现了一种“先走到尽头,再回溯继续”的遍历方式。
如下图所示,左侧是深度优先遍历的示意图,右上方是对应的递归代码。深度优先遍历就像是绕着整个二叉树的外围“走”一圈,在这个过程中,在每个节点都会遇到三个位置,分别对应前序遍历、中序遍历和后序遍历。