refactor: Replace 结点 with 节点 (#452)

* Replace 结点 with 节点
Update the footnotes in the figures

* Update mindmap

* Reduce the size of the mindmap.png
This commit is contained in:
Yudong Jin
2023-04-09 04:32:17 +08:00
committed by GitHub
parent 3f4e32b2b0
commit 1c8b7ef559
395 changed files with 2056 additions and 2056 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

+66 -66
View File
@@ -2,15 +2,15 @@
在「二叉搜索树」章节中提到,在进行多次插入与删除操作后,二叉搜索树可能会退化为链表。此时所有操作的时间复杂度都会由 $O(\log n)$ 劣化至 $O(n)$ 。
如下图所示,执行两步删除点后,该二叉搜索树就会退化为链表。
如下图所示,执行两步删除点后,该二叉搜索树就会退化为链表。
![AVL 树在删除点后发生退化](avl_tree.assets/avltree_degradation_from_removing_node.png)
![AVL 树在删除点后发生退化](avl_tree.assets/avltree_degradation_from_removing_node.png)
再比如,在以下完美二叉树中插入两个点后,树严重向左偏斜,查找操作的时间复杂度也随之发生劣化。
再比如,在以下完美二叉树中插入两个点后,树严重向左偏斜,查找操作的时间复杂度也随之发生劣化。
![AVL 树在插入点后发生退化](avl_tree.assets/avltree_degradation_from_inserting_node.png)
![AVL 树在插入点后发生退化](avl_tree.assets/avltree_degradation_from_inserting_node.png)
G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorithm for the organization of information" 中提出了「AVL 树」。**论文中描述了一系列操作,使得在不断添加与删除点后,AVL 树仍然不会发生退化**,进而使得各种操作的时间复杂度均能保持在 $O(\log n)$ 级别。
G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorithm for the organization of information" 中提出了「AVL 树」。**论文中描述了一系列操作,使得在不断添加与删除点后,AVL 树仍然不会发生退化**,进而使得各种操作的时间复杂度均能保持在 $O(\log n)$ 级别。
换言之,在频繁增删查改的使用场景中,AVL 树可始终保持很高的数据增删查改效率,具有很好的应用价值。
@@ -18,19 +18,19 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit
「AVL 树」既是「二叉搜索树」又是「平衡二叉树」,同时满足这两种二叉树的所有性质,因此又被称为「平衡二叉搜索树」。
### 点高度
### 点高度
在 AVL 树的操作中,需要获取点「高度 Height」,所以给 AVL 树的点类添加 `height` 变量。
在 AVL 树的操作中,需要获取点「高度 Height」,所以给 AVL 树的点类添加 `height` 变量。
=== "Java"
```java title=""
/* AVL 树点类 */
/* AVL 树点类 */
class TreeNode {
public int val; // 点值
public int height; // 点高度
public TreeNode left; // 左子
public TreeNode right; // 右子
public int val; // 点值
public int height; // 点高度
public TreeNode left; // 左子
public TreeNode right; // 右子
public TreeNode(int x) { val = x; }
}
```
@@ -38,12 +38,12 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit
=== "C++"
```cpp title=""
/* AVL 树点类 */
/* AVL 树点类 */
struct TreeNode {
int val{}; // 点值
int height = 0; // 点高度
TreeNode *left{}; // 左子
TreeNode *right{}; // 右子
int val{}; // 点值
int height = 0; // 点高度
TreeNode *left{}; // 左子
TreeNode *right{}; // 右子
TreeNode() = default;
explicit TreeNode(int x) : val(x){}
};
@@ -52,24 +52,24 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit
=== "Python"
```python title=""
""" AVL 树点类 """
""" AVL 树点类 """
class TreeNode:
def __init__(self, val: int):
self.val: int = val # 点值
self.height: int = 0 # 点高度
self.left: Optional[TreeNode] = None # 左子点引用
self.right: Optional[TreeNode] = None # 右子点引用
self.val: int = val # 点值
self.height: int = 0 # 点高度
self.left: Optional[TreeNode] = None # 左子点引用
self.right: Optional[TreeNode] = None # 右子点引用
```
=== "Go"
```go title=""
/* AVL 树点类 */
/* AVL 树点类 */
type TreeNode struct {
Val int // 点值
Height int // 点高度
Left *TreeNode // 左子点引用
Right *TreeNode // 右子点引用
Val int // 点值
Height int // 点高度
Left *TreeNode // 左子点引用
Right *TreeNode // 右子点引用
}
```
@@ -77,10 +77,10 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit
```javascript title=""
class TreeNode {
val; // 点值
height; //点高度
left; // 左子点指针
right; // 右子点指针
val; // 点值
height; //点高度
left; // 左子点指针
right; // 右子点指针
constructor(val, left, right, height) {
this.val = val === undefined ? 0 : val;
this.height = height === undefined ? 0 : height;
@@ -94,10 +94,10 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit
```typescript title=""
class TreeNode {
val: number; // 点值
height: number; // 点高度
left: TreeNode | null; // 左子点指针
right: TreeNode | null; // 右子点指针
val: number; // 点值
height: number; // 点高度
left: TreeNode | null; // 左子点指针
right: TreeNode | null; // 右子点指针
constructor(val?: number, height?: number, left?: TreeNode | null, right?: TreeNode | null) {
this.val = val === undefined ? 0 : val;
this.height = height === undefined ? 0 : height;
@@ -116,12 +116,12 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit
=== "C#"
```csharp title=""
/* AVL 树点类 */
/* AVL 树点类 */
class TreeNode {
public int val; // 点值
public int height; // 点高度
public TreeNode? left; // 左子
public TreeNode? right; // 右子
public int val; // 点值
public int height; // 点高度
public TreeNode? left; // 左子
public TreeNode? right; // 右子
public TreeNode(int x) { val = x; }
}
```
@@ -129,12 +129,12 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit
=== "Swift"
```swift title=""
/* AVL 树点类 */
/* AVL 树点类 */
class TreeNode {
var val: Int // 点值
var height: Int // 点高度
var left: TreeNode? // 左子
var right: TreeNode? // 右子
var val: Int // 点值
var height: Int // 点高度
var left: TreeNode? // 左子
var right: TreeNode? // 右子
init(x: Int) {
val = x
@@ -149,7 +149,7 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit
```
点高度」是最远叶点到该点的距离,即走过的「边」的数量。需要特别注意,**叶点的高度为 0 ,空点的高度为 -1**。我们封装两个工具函数,分别用于获取与更新点的高度。
点高度」是最远叶点到该点的距离,即走过的「边」的数量。需要特别注意,**叶点的高度为 0 ,空点的高度为 -1**。我们封装两个工具函数,分别用于获取与更新点的高度。
=== "Java"
@@ -231,9 +231,9 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit
[class]{AVLTree}-[func]{updateHeight}
```
### 点平衡因子
### 点平衡因子
点的「平衡因子 Balance Factor」是 **点的左子树高度减去右子树高度**,并定义空点的平衡因子为 0 。同样地,我们将获取点平衡因子封装成函数,以便后续使用。
点的「平衡因子 Balance Factor」是 **点的左子树高度减去右子树高度**,并定义空点的平衡因子为 0 。同样地,我们将获取点平衡因子封装成函数,以便后续使用。
=== "Java"
@@ -297,17 +297,17 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit
!!! note
设平衡因子为 $f$ ,则一棵 AVL 树的任意点的平衡因子皆满足 $-1 \le f \le 1$ 。
设平衡因子为 $f$ ,则一棵 AVL 树的任意点的平衡因子皆满足 $-1 \le f \le 1$ 。
## AVL 树旋转
AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影响二叉树中序遍历序列的前提下,使失衡点重新恢复平衡**。换言之,旋转操作既可以使树保持为「二叉搜索树」,也可以使树重新恢复为「平衡二叉树」。
AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影响二叉树中序遍历序列的前提下,使失衡点重新恢复平衡**。换言之,旋转操作既可以使树保持为「二叉搜索树」,也可以使树重新恢复为「平衡二叉树」。
我们将平衡因子的绝对值 $> 1$ 的点称为「失衡点」。根据点的失衡情况,旋转操作分为 **右旋、左旋、先右旋后左旋、先左旋后右旋**,接下来我们来一起来看看它们是如何操作的。
我们将平衡因子的绝对值 $> 1$ 的点称为「失衡点」。根据点的失衡情况,旋转操作分为 **右旋、左旋、先右旋后左旋、先左旋后右旋**,接下来我们来一起来看看它们是如何操作的。
### Case 1 - 右旋
如下图所示(点下方为「平衡因子」),从底至顶看,二叉树中首个失衡点是 **点 3**。我们聚焦在以该失衡点为根点的子树上,将该点记为 `node` ,将其左子点记为 `child` ,执行「右旋」操作。完成右旋后,该子树已经恢复平衡,并且仍然为二叉搜索树。
如下图所示(点下方为「平衡因子」),从底至顶看,二叉树中首个失衡点是 **点 3**。我们聚焦在以该失衡点为根点的子树上,将该点记为 `node` ,将其左子点记为 `child` ,执行「右旋」操作。完成右旋后,该子树已经恢复平衡,并且仍然为二叉搜索树。
=== "<1>"
![右旋操作步骤](avl_tree.assets/avltree_right_rotate_step1.png)
@@ -321,11 +321,11 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影
=== "<4>"
![avltree_right_rotate_step4](avl_tree.assets/avltree_right_rotate_step4.png)
进而,如果点 `child` 本身有右子点(记为 `grandChild` ),则需要在「右旋」中添加一步:将 `grandChild` 作为 `node` 的左子点。
进而,如果点 `child` 本身有右子点(记为 `grandChild` ),则需要在「右旋」中添加一步:将 `grandChild` 作为 `node` 的左子点。
![有 grandChild 的右旋操作](avl_tree.assets/avltree_right_rotate_with_grandchild.png)
“向右旋转”是一种形象化的说法,实际需要通过修改点指针实现,代码如下所示。
“向右旋转”是一种形象化的说法,实际需要通过修改点指针实现,代码如下所示。
=== "Java"
@@ -393,7 +393,7 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影
![左旋操作](avl_tree.assets/avltree_left_rotate.png)
同理,若点 `child` 本身有左子点(记为 `grandChild` ),则需要在「左旋」中添加一步:将 `grandChild` 作为 `node` 的右子点。
同理,若点 `child` 本身有左子点(记为 `grandChild` ),则需要在「左旋」中添加一步:将 `grandChild` 作为 `node` 的右子点。
![有 grandChild 的左旋操作](avl_tree.assets/avltree_left_rotate_with_grandchild.png)
@@ -461,7 +461,7 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影
### Case 3 - 先左后右
对于下图的失衡点 3 ,**单一使用左旋或右旋都无法使子树恢复平衡**,此时需要「先左旋后右旋」,即先对 `child` 执行「左旋」,再对 `node` 执行「右旋」。
对于下图的失衡点 3 ,**单一使用左旋或右旋都无法使子树恢复平衡**,此时需要「先左旋后右旋」,即先对 `child` 执行「左旋」,再对 `node` 执行「右旋」。
![先左旋后右旋](avl_tree.assets/avltree_left_right_rotate.png)
@@ -477,11 +477,11 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影
![AVL 树的四种旋转情况](avl_tree.assets/avltree_rotation_cases.png)
具体地,在代码中使用 **失衡点的平衡因子、较高一侧子点的平衡因子** 来确定失衡点属于上图中的哪种情况。
具体地,在代码中使用 **失衡点的平衡因子、较高一侧子点的平衡因子** 来确定失衡点属于上图中的哪种情况。
<div class="center-table" markdown>
| 失衡点的平衡因子 | 子点的平衡因子 | 应采用的旋转方法 |
| 失衡点的平衡因子 | 子点的平衡因子 | 应采用的旋转方法 |
| ------------------ | ---------------- | ---------------- |
| $>0$ (即左偏树) | $\geq 0$ | 右旋 |
| $>0$ (即左偏树) | $<0$ | 先左旋后右旋 |
@@ -490,7 +490,7 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影
</div>
为方便使用,我们将旋转操作封装成一个函数。至此,**我们可以使用此函数来旋转各种失衡情况,使失衡点重新恢复平衡**。
为方便使用,我们将旋转操作封装成一个函数。至此,**我们可以使用此函数来旋转各种失衡情况,使失衡点重新恢复平衡**。
=== "Java"
@@ -554,9 +554,9 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影
## AVL 树常用操作
### 插入
### 插入
「AVL 树」的点插入操作与「二叉搜索树」主体类似。不同的是,在插入点后,从该点到根点的路径上会出现一系列「失衡点」。所以,**我们需要从该点开始,从底至顶地执行旋转操作,使所有失衡点恢复平衡**。
「AVL 树」的点插入操作与「二叉搜索树」主体类似。不同的是,在插入点后,从该点到根点的路径上会出现一系列「失衡点」。所以,**我们需要从该点开始,从底至顶地执行旋转操作,使所有失衡点恢复平衡**。
=== "Java"
@@ -638,9 +638,9 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影
[class]{AVLTree}-[func]{insertHelper}
```
### 删除
### 删除
「AVL 树」删除点操作与「二叉搜索树」删除点操作总体相同。类似地,**在删除点后,也需要从底至顶地执行旋转操作,使所有失衡点恢复平衡**。
「AVL 树」删除点操作与「二叉搜索树」删除点操作总体相同。类似地,**在删除点后,也需要从底至顶地执行旋转操作,使所有失衡点恢复平衡**。
=== "Java"
@@ -742,9 +742,9 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影
[class]{AVLTree}-[func]{getInOrderNext}
```
### 查找
### 查找
「AVL 树」的点查找操作与「二叉搜索树」一致,在此不再赘述。
「AVL 树」的点查找操作与「二叉搜索树」一致,在此不再赘述。
## AVL 树典型应用
@@ -753,4 +753,4 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影
!!! question "为什么红黑树比 AVL 树更受欢迎?"
红黑树的平衡条件相对宽松,因此在红黑树中插入与删除点所需的旋转操作相对更少,点增删操作相比 AVL 树的效率更高。
红黑树的平衡条件相对宽松,因此在红黑树中插入与删除点所需的旋转操作相对更少,点增删操作相比 AVL 树的效率更高。
Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 58 KiB

+31 -31
View File
@@ -2,23 +2,23 @@
「二叉搜索树 Binary Search Tree」满足以下条件:
1. 对于根点,左子树中所有点的值 $<$ 根点的值 $<$ 右子树中所有点的值;
2. 任意点的左子树和右子树也是二叉搜索树,即也满足条件 `1.`
1. 对于根点,左子树中所有点的值 $<$ 根点的值 $<$ 右子树中所有点的值;
2. 任意点的左子树和右子树也是二叉搜索树,即也满足条件 `1.`
![二叉搜索树](binary_search_tree.assets/binary_search_tree.png)
## 二叉搜索树的操作
### 查找
### 查找
给定目标点值 `num` ,可以根据二叉搜索树的性质来查找。我们声明一个`cur` ,从二叉树的根`root` 出发,循环比较点值 `cur.val``num` 之间的大小关系
给定目标点值 `num` ,可以根据二叉搜索树的性质来查找。我们声明一个`cur` ,从二叉树的根`root` 出发,循环比较点值 `cur.val``num` 之间的大小关系
-`cur.val < num` ,说明目标点在 `cur` 的右子树中,因此执行 `cur = cur.right`
-`cur.val > num` ,说明目标点在 `cur` 的左子树中,因此执行 `cur = cur.left`
-`cur.val = num` ,说明找到目标点,跳出循环并返回该点即可;
-`cur.val < num` ,说明目标点在 `cur` 的右子树中,因此执行 `cur = cur.right`
-`cur.val > num` ,说明目标点在 `cur` 的左子树中,因此执行 `cur = cur.left`
-`cur.val = num` ,说明找到目标点,跳出循环并返回该点即可;
=== "<1>"
![查找点步骤](binary_search_tree.assets/bst_search_step1.png)
![查找点步骤](binary_search_tree.assets/bst_search_step1.png)
=== "<2>"
![bst_search_step2](binary_search_tree.assets/bst_search_step2.png)
@@ -91,16 +91,16 @@
[class]{BinarySearchTree}-[func]{search}
```
### 插入
### 插入
给定一个待插入元素 `num` ,为了保持二叉搜索树“左子树 < 根点 < 右子树”的性质,插入操作分为两步:
给定一个待插入元素 `num` ,为了保持二叉搜索树“左子树 < 根点 < 右子树”的性质,插入操作分为两步:
1. **查找插入位置**:与查找操作类似,我们从根点出发,根据当前点值和 `num` 的大小关系循环向下搜索,直到越过叶点(遍历到 $\text{null}$ )时跳出循环;
2. **在该位置插入点**:初始化点 `num` ,将该点放到 $\text{null}$ 的位置
1. **查找插入位置**:与查找操作类似,我们从根点出发,根据当前点值和 `num` 的大小关系循环向下搜索,直到越过叶点(遍历到 $\text{null}$ )时跳出循环;
2. **在该位置插入点**:初始化点 `num` ,将该点放到 $\text{null}$ 的位置
二叉搜索树不允许存在重复点,否则将会违背其定义。因此若待插入点在树中已经存在,则不执行插入,直接返回即可。
二叉搜索树不允许存在重复点,否则将会违背其定义。因此若待插入点在树中已经存在,则不执行插入,直接返回即可。
![在二叉搜索树中插入点](binary_search_tree.assets/bst_insert.png)
![在二叉搜索树中插入点](binary_search_tree.assets/bst_insert.png)
=== "Java"
@@ -162,30 +162,30 @@
[class]{BinarySearchTree}-[func]{insert}
```
为了插入点,需要借助 **辅助点 `pre`** 保存上一轮循环的点,这样在遍历到 $\text{null}$ 时,我们也可以获取到其父点,从而完成点插入操作。
为了插入点,需要借助 **辅助点 `pre`** 保存上一轮循环的点,这样在遍历到 $\text{null}$ 时,我们也可以获取到其父点,从而完成点插入操作。
与查找点相同,插入点使用 $O(\log n)$ 时间。
与查找点相同,插入点使用 $O(\log n)$ 时间。
### 删除
### 删除
与插入点一样,我们需要在删除操作后维持二叉搜索树的“左子树 < 根点 < 右子树”的性质。首先,我们需要在二叉树中执行查找操作,获取待删除点。接下来,根据待删除点的子点数量,删除操作需要分为三种情况:
与插入点一样,我们需要在删除操作后维持二叉搜索树的“左子树 < 根点 < 右子树”的性质。首先,我们需要在二叉树中执行查找操作,获取待删除点。接下来,根据待删除点的子点数量,删除操作需要分为三种情况:
**当待删除点的子点数量 $= 0$ 时**,表明待删除点是叶点,直接删除即可。
**当待删除点的子点数量 $= 0$ 时**,表明待删除点是叶点,直接删除即可。
![在二叉搜索树中删除点(度为 0](binary_search_tree.assets/bst_remove_case1.png)
![在二叉搜索树中删除点(度为 0](binary_search_tree.assets/bst_remove_case1.png)
**当待删除点的子点数量 $= 1$ 时**,将待删除点替换为其子点即可。
**当待删除点的子点数量 $= 1$ 时**,将待删除点替换为其子点即可。
![在二叉搜索树中删除点(度为 1](binary_search_tree.assets/bst_remove_case2.png)
![在二叉搜索树中删除点(度为 1](binary_search_tree.assets/bst_remove_case2.png)
**当待删除点的子点数量 $= 2$ 时**,删除操作分为三步:
**当待删除点的子点数量 $= 2$ 时**,删除操作分为三步:
1. 找到待删除点在 **中序遍历序列** 中的下一个点,记为 `nex`
2. 在树中递归删除点 `nex`
3. 使用 `nex` 替换待删除点;
1. 找到待删除点在 **中序遍历序列** 中的下一个点,记为 `nex`
2. 在树中递归删除点 `nex`
3. 使用 `nex` 替换待删除点;
=== "<1>"
![删除点(度为 2)步骤](binary_search_tree.assets/bst_remove_case3_step1.png)
![删除点(度为 2)步骤](binary_search_tree.assets/bst_remove_case3_step1.png)
=== "<2>"
![bst_remove_case3_step2](binary_search_tree.assets/bst_remove_case3_step2.png)
@@ -196,7 +196,7 @@
=== "<4>"
![bst_remove_case3_step4](binary_search_tree.assets/bst_remove_case3_step4.png)
删除点操作也使用 $O(\log n)$ 时间,其中查找待删除点 $O(\log n)$ ,获取中序遍历后继点 $O(\log n)$ 。
删除点操作也使用 $O(\log n)$ 时间,其中查找待删除点 $O(\log n)$ ,获取中序遍历后继点 $O(\log n)$ 。
=== "Java"
@@ -280,7 +280,7 @@
### 排序
我们知道,「中序遍历」遵循“左 $\rightarrow$ 根 $\rightarrow$ 右”的遍历优先级,而二叉搜索树遵循“左子点 $<$ 根点 $<$ 右子点”的大小关系。因此,在二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小点,从而得出一条重要性质:**二叉搜索树的中序遍历序列是升序的**。
我们知道,「中序遍历」遵循“左 $\rightarrow$ 根 $\rightarrow$ 右”的遍历优先级,而二叉搜索树遵循“左子点 $<$ 根点 $<$ 右子点”的大小关系。因此,在二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小点,从而得出一条重要性质:**二叉搜索树的中序遍历序列是升序的**。
借助中序遍历升序的性质,我们在二叉搜索树中获取有序数据仅需 $O(n)$ 时间,而无需额外排序,非常高效。
@@ -317,9 +317,9 @@
## 二叉搜索树的退化
理想情况下,我们希望二叉搜索树的是“左右平衡”的(详见「平衡二叉树」章节),此时可以在 $\log n$ 轮循环内查找任意点。
理想情况下,我们希望二叉搜索树的是“左右平衡”的(详见「平衡二叉树」章节),此时可以在 $\log n$ 轮循环内查找任意点。
如果我们动态地在二叉搜索树中插入与删除点,**则可能导致二叉树退化为链表**,此时各种操作的时间复杂度也退化之 $O(n)$ 。
如果我们动态地在二叉搜索树中插入与删除点,**则可能导致二叉树退化为链表**,此时各种操作的时间复杂度也退化之 $O(n)$ 。
!!! note
Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 56 KiB

+95 -95
View File
@@ -1,15 +1,15 @@
# 二叉树
「二叉树 Binary Tree」是一种非线性数据结构,代表着祖先与后代之间的派生关系,体现着“一分为二”的分治逻辑。类似于链表,二叉树也是以点为单位存储的,点包含「值」和两个「指针」。
「二叉树 Binary Tree」是一种非线性数据结构,代表着祖先与后代之间的派生关系,体现着“一分为二”的分治逻辑。类似于链表,二叉树也是以点为单位存储的,点包含「值」和两个「指针」。
=== "Java"
```java title=""
/* 二叉树点类 */
/* 二叉树点类 */
class TreeNode {
int val; // 点值
TreeNode left; // 左子点指针
TreeNode right; // 右子点指针
int val; // 点值
TreeNode left; // 左子点指针
TreeNode right; // 右子点指针
TreeNode(int x) { val = x; }
}
```
@@ -17,11 +17,11 @@
=== "C++"
```cpp title=""
/* 二叉树点结构体 */
/* 二叉树点结构体 */
struct TreeNode {
int val; // 点值
TreeNode *left; // 左子点指针
TreeNode *right; // 右子点指针
int val; // 点值
TreeNode *left; // 左子点指针
TreeNode *right; // 右子点指针
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
```
@@ -29,24 +29,24 @@
=== "Python"
```python title=""
""" 二叉树点类 """
""" 二叉树点类 """
class TreeNode:
def __init__(self, val: int):
self.val: int = val # 点值
self.left: Optional[TreeNode] = None # 左子点指针
self.right: Optional[TreeNode] = None # 右子点指针
self.val: int = val # 点值
self.left: Optional[TreeNode] = None # 左子点指针
self.right: Optional[TreeNode] = None # 右子点指针
```
=== "Go"
```go title=""
/* 二叉树点结构体 */
/* 二叉树点结构体 */
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
/* 点初始化方法 */
/* 点初始化方法 */
func NewTreeNode(v int) *TreeNode {
return &TreeNode{
Left: nil,
@@ -59,27 +59,27 @@
=== "JavaScript"
```javascript title=""
/* 二叉树点类 */
/* 二叉树点类 */
function TreeNode(val, left, right) {
this.val = (val === undefined ? 0 : val); // 点值
this.left = (left === undefined ? null : left); // 左子点指针
this.right = (right === undefined ? null : right); // 右子点指针
this.val = (val === undefined ? 0 : val); // 点值
this.left = (left === undefined ? null : left); // 左子点指针
this.right = (right === undefined ? null : right); // 右子点指针
}
```
=== "TypeScript"
```typescript title=""
/* 二叉树点类 */
/* 二叉树点类 */
class TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
this.val = val === undefined ? 0 : val; // 点值
this.left = left === undefined ? null : left; // 左子点指针
this.right = right === undefined ? null : right; // 右子点指针
this.val = val === undefined ? 0 : val; // 点值
this.left = left === undefined ? null : left; // 左子点指针
this.right = right === undefined ? null : right; // 右子点指针
}
}
```
@@ -93,11 +93,11 @@
=== "C#"
```csharp title=""
/* 二叉树点类 */
/* 二叉树点类 */
class TreeNode {
int val; // 点值
TreeNode? left; // 左子点指针
TreeNode? right; // 右子点指针
int val; // 点值
TreeNode? left; // 左子点指针
TreeNode? right; // 右子点指针
TreeNode(int x) { val = x; }
}
```
@@ -105,11 +105,11 @@
=== "Swift"
```swift title=""
/* 二叉树点类 */
/* 二叉树点类 */
class TreeNode {
var val: Int // 点值
var left: TreeNode? // 左子点指针
var right: TreeNode? // 右子点指针
var val: Int // 点值
var left: TreeNode? // 左子点指针
var right: TreeNode? // 右子点指针
init(x: Int) {
val = x
@@ -123,39 +123,39 @@
```
点的两个指针分别指向「左子点」和「右子点」,并且称该点为两个子点的「父点」。给定二叉树某点,将“左子点及其以下点形成的树”称为该点的「左子树」,右子树同理。
点的两个指针分别指向「左子点」和「右子点」,并且称该点为两个子点的「父点」。给定二叉树某点,将“左子点及其以下点形成的树”称为该点的「左子树」,右子树同理。
除了叶点外,每个点都有子点和子树。例如,若将下图的“点 2”看作父点,那么其左子点和右子点分别为“点 4”和“点 5”,左子树和右子树分别为“点 4 及其以下点形成的树”和“点 5 及其以下点形成的树”。
除了叶点外,每个点都有子点和子树。例如,若将下图的“点 2”看作父点,那么其左子点和右子点分别为“点 4”和“点 5”,左子树和右子树分别为“点 4 及其以下点形成的树”和“点 5 及其以下点形成的树”。
![父点、子点、子树](binary_tree.assets/binary_tree_definition.png)
![父点、子点、子树](binary_tree.assets/binary_tree_definition.png)
## 二叉树常见术语
二叉树的术语较多,建议尽量理解并记住。后续可能遗忘,可以在需要使用时回来查看确认。
- 「根点 Root Node」:二叉树最顶层的点,其没有父点;
- 「叶点 Leaf Node」:没有子点的点,其两个指针都指向 $\text{null}$
- 点所处「层 Level」:从顶至底依次增加,根点所处层为 1
- 点「度 Degree」:点的子点数量。二叉树中,度的范围是 0, 1, 2 ;
- 「边 Edge」:连接两个点的边,即点指针;
- 二叉树「高度」:二叉树中根点到最远叶点走过边的数量;
- 点「深度 Depth」 :根点到该点走过边的数量;
- 点「高度 Height」:最远叶点到该点走过边的数量;
- 「根点 Root Node」:二叉树最顶层的点,其没有父点;
- 「叶点 Leaf Node」:没有子点的点,其两个指针都指向 $\text{null}$
- 点所处「层 Level」:从顶至底依次增加,根点所处层为 1
- 点「度 Degree」:点的子点数量。二叉树中,度的范围是 0, 1, 2 ;
- 「边 Edge」:连接两个点的边,即点指针;
- 二叉树「高度」:二叉树中根点到最远叶点走过边的数量;
- 点「深度 Depth」 :根点到该点走过边的数量;
- 点「高度 Height」:最远叶点到该点走过边的数量;
![二叉树的常用术语](binary_tree.assets/binary_tree_terminology.png)
!!! tip "高度与深度的定义"
值得注意,我们通常将「高度」和「深度」定义为“走过边的数量”,而有些题目或教材会将其定义为“走过点的数量”,此时高度或深度都需要 + 1 。
值得注意,我们通常将「高度」和「深度」定义为“走过边的数量”,而有些题目或教材会将其定义为“走过点的数量”,此时高度或深度都需要 + 1 。
## 二叉树基本操作
**初始化二叉树**。与链表类似,先初始化点,再构建引用指向(即指针)。
**初始化二叉树**。与链表类似,先初始化点,再构建引用指向(即指针)。
=== "Java"
```java title="binary_tree.java"
// 初始化
// 初始化
TreeNode n1 = new TreeNode(1);
TreeNode n2 = new TreeNode(2);
TreeNode n3 = new TreeNode(3);
@@ -172,7 +172,7 @@
```cpp title="binary_tree.cpp"
/* 初始化二叉树 */
// 初始化
// 初始化
TreeNode* n1 = new TreeNode(1);
TreeNode* n2 = new TreeNode(2);
TreeNode* n3 = new TreeNode(3);
@@ -189,7 +189,7 @@
```python title="binary_tree.py"
""" 初始化二叉树 """
# 初始化
# 初始化
n1 = TreeNode(val=1)
n2 = TreeNode(val=2)
n3 = TreeNode(val=3)
@@ -206,7 +206,7 @@
```go title="binary_tree.go"
/* 初始化二叉树 */
// 初始化
// 初始化
n1 := NewTreeNode(1)
n2 := NewTreeNode(2)
n3 := NewTreeNode(3)
@@ -223,7 +223,7 @@
```javascript title="binary_tree.js"
/* 初始化二叉树 */
// 初始化
// 初始化
let n1 = new TreeNode(1),
n2 = new TreeNode(2),
n3 = new TreeNode(3),
@@ -240,7 +240,7 @@
```typescript title="binary_tree.ts"
/* 初始化二叉树 */
// 初始化
// 初始化
let n1 = new TreeNode(1),
n2 = new TreeNode(2),
n3 = new TreeNode(3),
@@ -263,7 +263,7 @@
```csharp title="binary_tree.cs"
/* 初始化二叉树 */
// 初始化
// 初始化
TreeNode n1 = new TreeNode(1);
TreeNode n2 = new TreeNode(2);
TreeNode n3 = new TreeNode(3);
@@ -279,7 +279,7 @@
=== "Swift"
```swift title="binary_tree.swift"
// 初始化
// 初始化
let n1 = TreeNode(x: 1)
let n2 = TreeNode(x: 2)
let n3 = TreeNode(x: 3)
@@ -298,78 +298,78 @@
```
**插入与删除点**。与链表类似,插入与删除点都可以通过修改指针实现。
**插入与删除点**。与链表类似,插入与删除点都可以通过修改指针实现。
![在二叉树中插入与删除点](binary_tree.assets/binary_tree_add_remove.png)
![在二叉树中插入与删除点](binary_tree.assets/binary_tree_add_remove.png)
=== "Java"
```java title="binary_tree.java"
TreeNode P = new TreeNode(0);
// 在 n1 -> n2 中间插入点 P
// 在 n1 -> n2 中间插入点 P
n1.left = P;
P.left = n2;
// 删除点 P
// 删除点 P
n1.left = n2;
```
=== "C++"
```cpp title="binary_tree.cpp"
/* 插入与删除点 */
/* 插入与删除点 */
TreeNode* P = new TreeNode(0);
// 在 n1 -> n2 中间插入点 P
// 在 n1 -> n2 中间插入点 P
n1->left = P;
P->left = n2;
// 删除点 P
// 删除点 P
n1->left = n2;
```
=== "Python"
```python title="binary_tree.py"
""" 插入与删除点 """
""" 插入与删除点 """
p = TreeNode(0)
# 在 n1 -> n2 中间插入点 P
# 在 n1 -> n2 中间插入点 P
n1.left = p
p.left = n2
# 删除点 P
# 删除点 P
n1.left = n2
```
=== "Go"
```go title="binary_tree.go"
/* 插入与删除点 */
// 在 n1 -> n2 中间插入点 P
/* 插入与删除点 */
// 在 n1 -> n2 中间插入点 P
p := NewTreeNode(0)
n1.Left = p
p.Left = n2
// 删除点 P
// 删除点 P
n1.Left = n2
```
=== "JavaScript"
```javascript title="binary_tree.js"
/* 插入与删除点 */
/* 插入与删除点 */
let P = new TreeNode(0);
// 在 n1 -> n2 中间插入点 P
// 在 n1 -> n2 中间插入点 P
n1.left = P;
P.left = n2;
// 删除点 P
// 删除点 P
n1.left = n2;
```
=== "TypeScript"
```typescript title="binary_tree.ts"
/* 插入与删除点 */
/* 插入与删除点 */
const P = new TreeNode(0);
// 在 n1 -> n2 中间插入点 P
// 在 n1 -> n2 中间插入点 P
n1.left = P;
P.left = n2;
// 删除点 P
// 删除点 P
n1.left = n2;
```
@@ -382,12 +382,12 @@
=== "C#"
```csharp title="binary_tree.cs"
/* 插入与删除点 */
/* 插入与删除点 */
TreeNode P = new TreeNode(0);
// 在 n1 -> n2 中间插入点 P
// 在 n1 -> n2 中间插入点 P
n1.left = P;
P.left = n2;
// 删除点 P
// 删除点 P
n1.left = n2;
```
@@ -395,10 +395,10 @@
```swift title="binary_tree.swift"
let P = TreeNode(x: 0)
// 在 n1 -> n2 中间插入点 P
// 在 n1 -> n2 中间插入点 P
n1.left = P
P.left = n2
// 删除点 P
// 删除点 P
n1.left = n2
```
@@ -410,13 +410,13 @@
!!! note
插入点会改变二叉树的原有逻辑结构,删除点往往意味着删除了该点的所有子树。因此,二叉树中的插入与删除一般都是由一套操作配合完成的,这样才能实现有意义的操作。
插入点会改变二叉树的原有逻辑结构,删除点往往意味着删除了该点的所有子树。因此,二叉树中的插入与删除一般都是由一套操作配合完成的,这样才能实现有意义的操作。
## 常见二叉树类型
### 完美二叉树
「完美二叉树 Perfect Binary Tree」的所有层的点都被完全填满。在完美二叉树中,叶点的度为 $0$ ,其余所有点的度都为 $2$ ;若树高度 $= h$ ,则点总数 $= 2^{h+1} - 1$ ,呈标准的指数级关系,反映着自然界中常见的细胞分裂。
「完美二叉树 Perfect Binary Tree」的所有层的点都被完全填满。在完美二叉树中,叶点的度为 $0$ ,其余所有点的度都为 $2$ ;若树高度 $= h$ ,则点总数 $= 2^{h+1} - 1$ ,呈标准的指数级关系,反映着自然界中常见的细胞分裂。
!!! tip
@@ -426,57 +426,57 @@
### 完全二叉树
「完全二叉树 Complete Binary Tree」只有最底层的点未被填满,且最底层点尽量靠左填充。
「完全二叉树 Complete Binary Tree」只有最底层的点未被填满,且最底层点尽量靠左填充。
**完全二叉树非常适合用数组来表示**。如果按照层序遍历序列的顺序来存储,那么空点 `null` 一定全部出现在序列的尾部,因此我们就可以不用存储这些 null 了。
**完全二叉树非常适合用数组来表示**。如果按照层序遍历序列的顺序来存储,那么空点 `null` 一定全部出现在序列的尾部,因此我们就可以不用存储这些 null 了。
![完全二叉树](binary_tree.assets/complete_binary_tree.png)
### 完满二叉树
「完满二叉树 Full Binary Tree」除了叶点之外,其余所有点都有两个子点。
「完满二叉树 Full Binary Tree」除了叶点之外,其余所有点都有两个子点。
![完满二叉树](binary_tree.assets/full_binary_tree.png)
### 平衡二叉树
「平衡二叉树 Balanced Binary Tree」中任意点的左子树和右子树的高度之差的绝对值 $\leq 1$ 。
「平衡二叉树 Balanced Binary Tree」中任意点的左子树和右子树的高度之差的绝对值 $\leq 1$ 。
![平衡二叉树](binary_tree.assets/balanced_binary_tree.png)
## 二叉树的退化
当二叉树的每层的点都被填满时,达到「完美二叉树」;而当所有点都偏向一边时,二叉树退化为「链表」。
当二叉树的每层的点都被填满时,达到「完美二叉树」;而当所有点都偏向一边时,二叉树退化为「链表」。
- 完美二叉树是一个二叉树的“最佳状态”,可以完全发挥出二叉树“分治”的优势;
- 链表则是另一个极端,各项操作都变为线性操作,时间复杂度退化至 $O(n)$ ;
![二叉树的最佳与最二叉树的最佳和最差结构差情况](binary_tree.assets/binary_tree_corner_cases.png)
如下表所示,在最佳和最差结构下,二叉树的叶点数量、点总数、高度等达到极大或极小值。
如下表所示,在最佳和最差结构下,二叉树的叶点数量、点总数、高度等达到极大或极小值。
<div class="center-table" markdown>
| | 完美二叉树 | 链表 |
| ----------------------------- | ---------- | ---------- |
| 第 $i$ 层的点数量 | $2^{i-1}$ | $1$ |
| 树的高度为 $h$ 时的叶点数量 | $2^h$ | $1$ |
| 树的高度为 $h$ 时的点总数 | $2^{h+1} - 1$ | $h + 1$ |
| 树的点总数为 $n$ 时的高度 | $\log_2 (n+1) - 1$ | $n - 1$ |
| 第 $i$ 层的点数量 | $2^{i-1}$ | $1$ |
| 树的高度为 $h$ 时的叶点数量 | $2^h$ | $1$ |
| 树的高度为 $h$ 时的点总数 | $2^{h+1} - 1$ | $h + 1$ |
| 树的点总数为 $n$ 时的高度 | $\log_2 (n+1) - 1$ | $n - 1$ |
</div>
## 二叉树表示方式 *
我们一般使用二叉树的「链表表示」,即存储单位为点 `TreeNode` 点之间通过指针(引用)相连接。本文前述示例代码展示了二叉树在链表表示下的各项基本操作。
我们一般使用二叉树的「链表表示」,即存储单位为点 `TreeNode` 点之间通过指针(引用)相连接。本文前述示例代码展示了二叉树在链表表示下的各项基本操作。
那能否可以用「数组表示」二叉树呢?答案是肯定的。先来分析一个简单案例,给定一个「完美二叉树」,将点按照层序遍历的顺序编号(从 0 开始),那么可以推导得出父点索引与子点索引之间的「映射公式」:**设点的索引为 $i$ ,则该点的左子点索引为 $2i + 1$ 、右子点索引为 $2i + 2$** 。
那能否可以用「数组表示」二叉树呢?答案是肯定的。先来分析一个简单案例,给定一个「完美二叉树」,将点按照层序遍历的顺序编号(从 0 开始),那么可以推导得出父点索引与子点索引之间的「映射公式」:**设点的索引为 $i$ ,则该点的左子点索引为 $2i + 1$ 、右子点索引为 $2i + 2$** 。
**本质上,映射公式的作用就是链表中的指针**。对于层序遍历序列中的任意点,我们都可以使用映射公式来访问子点。因此,可以直接使用层序遍历序列(即数组)来表示完美二叉树。
**本质上,映射公式的作用就是链表中的指针**。对于层序遍历序列中的任意点,我们都可以使用映射公式来访问子点。因此,可以直接使用层序遍历序列(即数组)来表示完美二叉树。
![完美二叉树的数组表示](binary_tree.assets/array_representation_mapping.png)
然而,完美二叉树只是个例,二叉树中间层往往存在许多空点(即 `null` ),而层序遍历序列并不包含这些空点,并且我们无法单凭序列来猜测空点的数量和分布位置,**即理论上存在许多种二叉树都符合该层序遍历序列**。显然,这种情况无法使用数组来存储二叉树。
然而,完美二叉树只是个例,二叉树中间层往往存在许多空点(即 `null` ),而层序遍历序列并不包含这些空点,并且我们无法单凭序列来猜测空点的数量和分布位置,**即理论上存在许多种二叉树都符合该层序遍历序列**。显然,这种情况无法使用数组来存储二叉树。
![给定数组对应多种二叉树可能性](binary_tree.assets/array_representation_without_empty.png)
@@ -495,7 +495,7 @@
```cpp title=""
/* 二叉树的数组表示 */
// 为了符合数据类型为 int ,使用 int 最大值标记空位
// 该方法的使用前提是没有点的值 = INT_MAX
// 该方法的使用前提是没有点的值 = INT_MAX
vector<int> tree = { 1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15 };
```
@@ -561,8 +561,8 @@
![任意类型二叉树的数组表示](binary_tree.assets/array_representation_with_empty.png)
回顾「完全二叉树」的定义,其只有最底层有空点,并且最底层的点尽量靠左,因而所有空点都一定出现在层序遍历序列的末尾。**因为我们先验地确定了空位的位置,所以在使用数组表示完全二叉树时,可以省略存储“空位”**。因此,完全二叉树非常适合使用数组来表示。
回顾「完全二叉树」的定义,其只有最底层有空点,并且最底层的点尽量靠左,因而所有空点都一定出现在层序遍历序列的末尾。**因为我们先验地确定了空位的位置,所以在使用数组表示完全二叉树时,可以省略存储“空位”**。因此,完全二叉树非常适合使用数组来表示。
![完全二叉树的数组表示](binary_tree.assets/array_representation_complete_binary_tree.png)
数组表示有两个优点: 一是不需要存储指针,节省空间;二是可以随机访问点。然而,当二叉树中的“空位”很多时,数组中只包含很少点的数据,空间利用率很低。
数组表示有两个优点: 一是不需要存储指针,节省空间;二是可以随机访问点。然而,当二叉树中的“空位”很多时,数组中只包含很少点的数据,空间利用率很低。
Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 167 KiB

+8 -8
View File
@@ -1,12 +1,12 @@
# 二叉树遍历
从物理结构角度看,树是一种基于链表的数据结构,因此遍历方式也是通过指针(即引用)逐个遍历点。同时,树还是一种非线性数据结构,这导致遍历树比遍历链表更加复杂,需要使用搜索算法来实现。
从物理结构角度看,树是一种基于链表的数据结构,因此遍历方式也是通过指针(即引用)逐个遍历点。同时,树还是一种非线性数据结构,这导致遍历树比遍历链表更加复杂,需要使用搜索算法来实现。
常见的二叉树遍历方式有层序遍历、前序遍历、中序遍历、后序遍历。
## 层序遍历
「层序遍历 Level-Order Traversal」从顶至底、一层一层地遍历二叉树,并在每层中按照从左到右的顺序访问点。
「层序遍历 Level-Order Traversal」从顶至底、一层一层地遍历二叉树,并在每层中按照从左到右的顺序访问点。
层序遍历本质上是「广度优先搜索 Breadth-First Traversal」,其体现着一种“一圈一圈向外”的层进遍历方式。
@@ -78,23 +78,23 @@
### 复杂度分析
**时间复杂度**:所有点被访问一次,使用 $O(n)$ 时间,其中 $n$ 为点数量。
**时间复杂度**:所有点被访问一次,使用 $O(n)$ 时间,其中 $n$ 为点数量。
**空间复杂度**:当为满二叉树时达到最差情况,遍历到最底层前,队列中最多同时存在 $\frac{n + 1}{2}$ 个点,使用 $O(n)$ 空间。
**空间复杂度**:当为满二叉树时达到最差情况,遍历到最底层前,队列中最多同时存在 $\frac{n + 1}{2}$ 个点,使用 $O(n)$ 空间。
## 前序、中序、后序遍历
相对地,前、中、后序遍历皆属于「深度优先遍历 Depth-First Traversal」,其体现着一种“先走到尽头,再回头继续”的回溯遍历方式。
如下图所示,左侧是深度优先遍历的的示意图,右上方是对应的递归实现代码。深度优先遍历就像是绕着整个二叉树的外围“走”一圈,走的过程中,在每个点都会遇到三个位置,分别对应前序遍历、中序遍历、后序遍历。
如下图所示,左侧是深度优先遍历的的示意图,右上方是对应的递归实现代码。深度优先遍历就像是绕着整个二叉树的外围“走”一圈,走的过程中,在每个点都会遇到三个位置,分别对应前序遍历、中序遍历、后序遍历。
![二叉搜索树的前、中、后序遍历](binary_tree_traversal.assets/binary_tree_dfs.png)
<div class="center-table" markdown>
| 位置 | 含义 | 此处访问点时对应 |
| 位置 | 含义 | 此处访问点时对应 |
| ---------- | ------------------------------------ | ----------------------------- |
| 橙色圆圈处 | 刚进入此点,即将访问该点的左子树 | 前序遍历 Pre-Order Traversal |
| 橙色圆圈处 | 刚进入此点,即将访问该点的左子树 | 前序遍历 Pre-Order Traversal |
| 蓝色圆圈处 | 已访问完左子树,即将访问右子树 | 中序遍历 In-Order Traversal |
| 紫色圆圈处 | 已访问完左子树和右子树,即将返回 | 后序遍历 Post-Order Traversal |
@@ -208,6 +208,6 @@
### 复杂度分析
**时间复杂度**:所有点被访问一次,使用 $O(n)$ 时间,其中 $n$ 为点数量。
**时间复杂度**:所有点被访问一次,使用 $O(n)$ 时间,其中 $n$ 为点数量。
**空间复杂度**:当树退化为链表时达到最差情况,递归深度达到 $n$ ,系统使用 $O(n)$ 栈帧空间。
+7 -7
View File
@@ -2,12 +2,12 @@
### 二叉树
- 二叉树是一种非线性数据结构,代表着“一分为二”的分治逻辑。二叉树的点包含「值」和两个「指针」,分别指向左子点和右子点。
- 选定二叉树中某点,将其左(右)子点以下形成的树称为左(右)子树。
- 二叉树的术语较多,包括根点、叶点、层、度、边、高度、深度等。
- 二叉树的初始化、点插入、点删除操作与链表的操作方法类似。
- 二叉树是一种非线性数据结构,代表着“一分为二”的分治逻辑。二叉树的点包含「值」和两个「指针」,分别指向左子点和右子点。
- 选定二叉树中某点,将其左(右)子点以下形成的树称为左(右)子树。
- 二叉树的术语较多,包括根点、叶点、层、度、边、高度、深度等。
- 二叉树的初始化、点插入、点删除操作与链表的操作方法类似。
- 常见的二叉树类型包括完美二叉树、完全二叉树、完满二叉树、平衡二叉树。完美二叉树是理想状态,链表则是退化后的最差状态。
- 二叉树可以使用数组表示,具体做法是将点值和空位按照层序遍历的顺序排列,并基于父点和子点之间的索引映射公式实现指针。
- 二叉树可以使用数组表示,具体做法是将点值和空位按照层序遍历的顺序排列,并基于父点和子点之间的索引映射公式实现指针。
### 二叉树遍历
@@ -17,5 +17,5 @@
### 二叉搜索树
- 二叉搜索树是一种高效的元素查找数据结构,查找、插入、删除操作的时间复杂度皆为 $O(\log n)$ 。二叉搜索树退化为链表后,各项时间复杂度劣化至 $O(n)$ ,因此如何避免退化是非常重要的课题。
- AVL 树又称平衡二叉搜索树,其通过旋转操作,使得在不断插入与删除点后,仍然可以保持二叉树的平衡(不退化)。
- AVL 树的旋转操作分为右旋、左旋、先右旋后左旋、先左旋后右旋。在插入或删除点后,AVL 树会从底至顶地执行旋转操作,使树恢复平衡。
- AVL 树又称平衡二叉搜索树,其通过旋转操作,使得在不断插入与删除点后,仍然可以保持二叉树的平衡(不退化)。
- AVL 树的旋转操作分为右旋、左旋、先右旋后左旋、先左旋后右旋。在插入或删除点后,AVL 树会从底至顶地执行旋转操作,使树恢复平衡。