Polish the chapter of stack_and_queue, tree

This commit is contained in:
krahets
2023-04-10 23:59:22 +08:00
parent 1bbfa85e08
commit 236b9cadb1
10 changed files with 162 additions and 180 deletions

View File

@@ -1,26 +1,24 @@
# AVL 树 *
二叉搜索树章节中提到,在进行多次插入删除操作后,二叉搜索树可能退化为链表。此时所有操作的时间复杂度都会由 $O(\log n)$ 劣化至 $O(n)$
在二叉搜索树章节中,我们提到了在多次插入删除操作后,二叉搜索树可能退化为链表。这种情况下,所有操作的时间复杂度将从 $O(\log n)$ 恶化为 $O(n)$。
如下图所示,执行两步删除节点后,该二叉搜索树会退化为链表。
如下图所示,经过两次删除节点操作,这个二叉搜索树便会退化为链表。
![AVL 树在删除节点后发生退化](avl_tree.assets/avltree_degradation_from_removing_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)$ 级别。
换言之在频繁增删查改的使用场景中AVL 树可始终保持很高的数据增删查改效率,具有很好的应用价值。
G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorithm for the organization of information" 中提出了「AVL 树」。论文中详细描述了一系列操作,确保在持续添加删除节点后AVL 树不会退化,从而使得各种操作的时间复杂度保持在 $O(\log n)$ 级别。换句话说在需要频繁进行增删查改操作的场景中AVL 树能始终保持高效的数据操作性能,具有很好的应用价值。
## AVL 树常见术语
「AVL 树」既是二叉搜索树」又是「平衡二叉树,同时满足这两二叉树的所有性质,因此被称为「平衡二叉搜索树」。
「AVL 树」既是二叉搜索树也是平衡二叉树,同时满足这两二叉树的所有性质,因此被称为「平衡二叉搜索树」。
### 节点高度
在 AVL 树的操作中,需要获取节点高度 Height」所以给 AVL 树的节点类添加 `height` 变量。
操作 AVL 树时,我们需要获取节点高度,因此需要为 AVL 树的节点类添加 `height` 变量。
=== "Java"
@@ -149,7 +147,7 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit
```
「节点高度」是最远叶节点到该节点的距离,即走过的「边」的数量。需要特别注意,**叶节点的高度为 0 ,空节点的高度为 -1**。我们封装两个工具函数,分别用于获取更新节点的高度。
「节点高度」是指从该节点到最远叶节点的距离,即所经过的“边”的数量。需要特别注意的是,叶节点的高度为 0 空节点的高度为 -1 。我们将创建两个工具函数,分别用于获取更新节点的高度。
=== "Java"
@@ -233,7 +231,7 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit
### 节点平衡因子
节点的「平衡因子 Balance Factor」是 **节点左子树高度减去右子树高度**,并定义空节点的平衡因子为 0 。同样地,我们将获取节点平衡因子封装成函数,便后续使用。
节点的「平衡因子 Balance Factor」定义为节点左子树高度减去右子树高度,同时规定空节点的平衡因子为 0 。我们同样将获取节点平衡因子的功能封装成函数,便后续使用。
=== "Java"
@@ -301,13 +299,13 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit
## 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 +319,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"
@@ -387,9 +385,9 @@ AVL 树的独特之处在于「旋转 Rotation」的操作其可 **在不影
[class]{AVLTree}-[func]{rightRotate}
```
### Case 2 - 左旋
### 左旋
类似地,如果将取上述失衡二叉树的“镜像”,那么则需要「左旋」操作。
相应的,如果考虑上述失衡二叉树的“镜像”,则需要执行「左旋」操作。
![左旋操作](avl_tree.assets/avltree_left_rotate.png)
@@ -397,7 +395,7 @@ AVL 树的独特之处在于「旋转 Rotation」的操作其可 **在不影
![有 grandChild 的左旋操作](avl_tree.assets/avltree_left_rotate_with_grandchild.png)
观察发现,**「左旋」和「右旋」操作是镜像对称的,两者对应解决的两种失衡情况也是对称的**。根据对称性,我们可以很方便地从右旋推导出左旋。具体地,只需将「右旋」代码中的把所有的 `left` 替换为 `right` 所有的 `right` 替换为 `left` ,即可得到「左旋」代码。
可以观察到,**右旋和左旋操作在逻辑上是镜像对称的,它们分别解决的两种失衡情况也是对称的**。基于对称性,我们可以轻松地从右旋的代码推导出左旋的代码。具体地,只需将「右旋」代码中的把所有的 `left` 替换为 `right` ,将所有的 `right` 替换为 `left` ,即可得到「左旋」代码。
=== "Java"
@@ -459,30 +457,30 @@ AVL 树的独特之处在于「旋转 Rotation」的操作其可 **在不影
[class]{AVLTree}-[func]{leftRotate}
```
### Case 3 - 先左后右
### 先左后右
对于下图的失衡节点 3 **单一使用左旋或右旋都无法使子树恢复平衡**此时需要先左旋后右旋,即先对 `child` 执行「左旋」,再对 `node` 执行「右旋」。
对于下图的失衡节点 3,仅使用左旋或右旋都无法使子树恢复平衡此时需要先左旋后右旋,即先对 `child` 执行「左旋」,再对 `node` 执行「右旋」。
![先左旋后右旋](avl_tree.assets/avltree_left_right_rotate.png)
### Case 4 - 先右后左
### 先右后左
同理,取以上失衡二叉树的镜像,需要先右旋后左旋,即先对 `child` 执行「右旋」,然后对 `node` 执行「左旋」。
同理,对于上述失衡二叉树的镜像情况,需要先右旋后左旋,即先对 `child` 执行「右旋」,然后对 `node` 执行「左旋」。
![先右旋后左旋](avl_tree.assets/avltree_right_left_rotate.png)
### 旋转的选择
下图描述的四种失衡情况与上述 Cases 逐个对应,分别需采用 **右旋、左旋、先右后左、先左后右** 的旋转操作。
下图展示的四种失衡情况与上述案例逐个对应,分别需采用右旋、左旋、先右后左、先左后右的旋转操作。
![AVL 树的四种旋转情况](avl_tree.assets/avltree_rotation_cases.png)
具体地,在代码中使用 **失衡节点的平衡因子较高一侧子节点的平衡因子** 来确定失衡节点属于上图中的哪种情况。
在代码中,我们通过判断失衡节点的平衡因子以及较高一侧子节点的平衡因子的正负号,来确定失衡节点属于上图中的哪种情况。
<div class="center-table" markdown>
| 失衡节点的平衡因子 | 子节点的平衡因子 | 应采用的旋转方法 |
| ------------------ | ---------------- | ---------------- |
| ---------------- | ---------------- | ---------------- |
| $>0$ (即左偏树) | $\geq 0$ | 右旋 |
| $>0$ (即左偏树) | $<0$ | 先左旋后右旋 |
| $<0$ (即右偏树) | $\leq 0$ | 左旋 |
@@ -490,7 +488,7 @@ AVL 树的独特之处在于「旋转 Rotation」的操作其可 **在不影
</div>
方便使用,我们将旋转操作封装成一个函数。至此,**我们可以使用此函数来旋转各种失衡情况,使失衡节点重新恢复平衡**。
了便于使用,我们将旋转操作封装成一个函数。**有了这个函数,我们就能对各种失衡情况进行旋转,使失衡节点重新恢复平衡**。
=== "Java"
@@ -556,7 +554,7 @@ AVL 树的独特之处在于「旋转 Rotation」的操作其可 **在不影
### 插入节点
「AVL 树」的节点插入操作与「二叉搜索树」主体类似。不同的是,在插入节点后,从该节点到根节点的路径上会出现一系列失衡节点」。所以**我们需要从节点开始,从底至顶地执行旋转操作,使所有失衡节点恢复平衡**。
「AVL 树」的节点插入操作与「二叉搜索树」主体类似。唯一的区别在于,在 AVL 树中插入节点后,从该节点到根节点的路径上可能会出现一系列失衡节点。因此**我们需要从这个节点开始,自底向上执行旋转操作,使所有失衡节点恢复平衡**。
=== "Java"
@@ -640,7 +638,7 @@ AVL 树的独特之处在于「旋转 Rotation」的操作其可 **在不影
### 删除节点
「AVL 树」删除节点操作与「二叉搜索树」删除节点操作总体相同。类似地,**在删除节点后,也需要从底至顶地执行旋转操作,使所有失衡节点恢复平衡**
类似地,在二叉搜索树的删除节点方法的基础上,需要从底至顶地执行旋转操作,使所有失衡节点恢复平衡。
=== "Java"
@@ -744,13 +742,13 @@ AVL 树的独特之处在于「旋转 Rotation」的操作其可 **在不影
### 查找节点
AVL 树的节点查找操作与二叉搜索树一致,在此不再赘述。
AVL 树的节点查找操作与二叉搜索树一致,在此不再赘述。
## AVL 树典型应用
- 组织存储大型数据,适用于高频查找、低频增删场景;
- 用于建数据库中的索引系统;
- 组织存储大型数据,适用于高频查找、低频增删场景;
- 用于建数据库中的索引系统;
!!! question "为什么红黑树比 AVL 树更受欢迎?"
红黑树的平衡条件相对宽松,因此在红黑树中插入与删除节点所需的旋转操作相对少,节点增删操作相比 AVL 树的效率更高
红黑树的平衡条件相对宽松,因此在红黑树中插入与删除节点所需的旋转操作相对少,节点增删操作上的平均效率高于 AVL 树。

View File

@@ -3,7 +3,7 @@
「二叉搜索树 Binary Search Tree」满足以下条件
1. 对于根节点,左子树中所有节点的值 $<$ 根节点的值 $<$ 右子树中所有节点的值;
2. 任意节点的左子树和右子树也是二叉搜索树,即满足条件 `1.`
2. 任意节点的左右子树也是二叉搜索树,即同样满足条件 `1.`
![二叉搜索树](binary_search_tree.assets/binary_search_tree.png)
@@ -15,10 +15,10 @@
-`cur.val < num` ,说明目标节点在 `cur` 的右子树中,因此执行 `cur = cur.right`
-`cur.val > num` ,说明目标节点在 `cur` 的左子树中,因此执行 `cur = cur.left`
-`cur.val = num` ,说明找到目标节点,跳出循环并返回该节点即可
-`cur.val = num` ,说明找到目标节点,跳出循环并返回该节点;
=== "<1>"
![查找节点步骤](binary_search_tree.assets/bst_search_step1.png)
![bst_search_step1](binary_search_tree.assets/bst_search_step1.png)
=== "<2>"
![bst_search_step2](binary_search_tree.assets/bst_search_step2.png)
@@ -29,7 +29,7 @@
=== "<4>"
![bst_search_step4](binary_search_tree.assets/bst_search_step4.png)
二叉搜索树的查找操作二分查找算法如出一辙,也是在每轮排除一半情况。循环次数最多为二叉树的高度,当二叉树平衡时,使用 $O(\log n)$ 时间。
二叉搜索树的查找操作二分查找算法的工作原理一致,都是每轮排除一半情况。循环次数最多为二叉树的高度,当二叉树平衡时,使用 $O(\log n)$ 时间。
=== "Java"
@@ -95,10 +95,10 @@
给定一个待插入元素 `num` ,为了保持二叉搜索树“左子树 < 根节点 < 右子树”的性质,插入操作分为两步:
1. **查找插入位置**:与查找操作似,我们从根节点出发,根据当前节点值和 `num` 的大小关系循环向下搜索,直到越过叶节点(遍历 $\text{null}$ )时跳出循环;
2. **在该位置插入节点**:初始化节点 `num` ,将该节点放到 $\text{null}$ 的位置
1. **查找插入位置**:与查找操作似,从根节点出发,根据当前节点值和 `num` 的大小关系循环向下搜索,直到越过叶节点(遍历 $\text{null}$ )时跳出循环;
2. **在该位置插入节点**:初始化节点 `num` ,将该节点置于 $\text{null}$ 的位置;
二叉搜索树不允许存在重复节点,否则将会违背其定义。因此若待插入节点在树中已存在,则不执行插入,直接返回即可
二叉搜索树不允许存在重复节点,否则将违反其定义。因此若待插入节点在树中已存在,则不执行插入,直接返回。
![在二叉搜索树中插入节点](binary_search_tree.assets/bst_insert.png)
@@ -162,30 +162,30 @@
[class]{BinarySearchTree}-[func]{insert}
```
为了插入节点,需要借助 **辅助节点 `pre`** 保存上一轮循环的节点,这样在遍历 $\text{null}$ 时,我们可以获取到其父节点,从而完成节点插入操作。
为了插入节点,我们需要利用辅助节点 `pre` 保存上一轮循环的节点,这样在遍历 $\text{null}$ 时,我们可以获取到其父节点,从而完成节点插入操作。
与查找节点相同,插入节点使用 $O(\log n)$ 时间。
### 删除节点
与插入节点一样,我们需要在删除操作后维持二叉搜索树的“左子树 < 根节点 < 右子树”的性质。首先,我们需要在二叉树中执行查找操作,获取待删除节点。接下来,根据待删除节点的子节点数量,删除操作需分为三种情况:
与插入节点类似,我们需要在删除操作后维持二叉搜索树的“左子树 < 根节点 < 右子树”的性质。首先,我们需要在二叉树中执行查找操作,获取待删除节点。接下来,根据待删除节点的子节点数量,删除操作需分为三种情况:
**当待删除节点的子节点数量 $= 0$ 时**,表待删除节点是叶节点,直接删除即可
当待删除节点的子节点数量 $= 0$ 时,表待删除节点是叶节点,可以直接删除。
![在二叉搜索树中删除节点(度为 0](binary_search_tree.assets/bst_remove_case1.png)
**当待删除节点的子节点数量 $= 1$ 时**,将待删除节点替换为其子节点即可。
当待删除节点的子节点数量 $= 1$ 时,将待删除节点替换为其子节点即可。
![在二叉搜索树中删除节点(度为 1](binary_search_tree.assets/bst_remove_case2.png)
**当待删除节点的子节点数量 $= 2$ 时**,删除操作分为三步:
当待删除节点的子节点数量 $= 2$ 时,删除操作分为三步:
1. 找到待删除节点在 **中序遍历序列** 中的下一个节点,记为 `nex`
1. 找到待删除节点在中序遍历序列中的下一个节点,记为 nex
2. 在树中递归删除节点 `nex`
3. 使用 `nex` 替换待删除节点;
=== "<1>"
![删除节点(度为 2步骤](binary_search_tree.assets/bst_remove_case3_step1.png)
![bst_remove_case3_step1](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,29 +280,29 @@
### 排序
我们知道,中序遍历遵循“左 $\rightarrow$ 根 $\rightarrow$ 右”的遍历优先级,而二叉搜索树遵循“左子节点 $<$ 根节点 $<$ 右子节点”的大小关系。因此,在二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小节点,从而得出一重要性质:**二叉搜索树的中序遍历序列是升序的**。
我们知道,二叉树的中序遍历遵循“左 $\rightarrow$ 根 $\rightarrow$ 右”的遍历顺序,而二叉搜索树满足“左子节点 $<$ 根节点 $<$ 右子节点”的大小关系。因此,在二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小节点,从而得出一重要性质:**二叉搜索树的中序遍历序列是升序的**。
借助中序遍历升序的性质,我们在二叉搜索树中获取有序数据仅需 $O(n)$ 时间,无需额外排序,非常高效。
利用中序遍历升序的性质,我们在二叉搜索树中获取有序数据仅需 $O(n)$ 时间,无需额外排序,非常高效。
![二叉搜索树的中序遍历序列](binary_search_tree.assets/bst_inorder_traversal.png)
## 二叉搜索树的效率
假设给定 $n$ 个数字,最常的存储方式是「数组」,那么对于这串乱序的数字,常见操作的效率
假设给定 $n$ 个数字,最常的存储方式是「数组」对于这串乱序的数字,常见操作的效率如下
- **查找元素**:由于数组是无序的,因此需要遍历数组来确定,使用 $O(n)$ 时间;
- **插入元素**:只需将元素添加至数组尾部即可,使用 $O(1)$ 时间;
- **删除元素**:先查找元素,使用 $O(n)$ 时间,再在数组中删除该元素,使用 $O(n)$ 时间;
- **获取最小 / 最大元素**:需要遍历数组来确定,使用 $O(n)$ 时间;
为了得先验信息,我们可以预先将数组元素进行排序,得到一个「排序数组」此时操作效率
为了得先验信息,我们可以预先将数组元素进行排序,得到一个「排序数组」此时操作效率如下
- **查找元素**:由于数组已排序,可以使用二分查找,平均使用 $O(\log n)$ 时间;
- **插入元素**:先查找插入位置,使用 $O(\log n)$ 时间,再插入到指定位置,使用 $O(n)$ 时间;
- **删除元素**:先查找元素,使用 $O(\log n)$ 时间,再在数组中删除该元素,使用 $O(n)$ 时间;
- **获取最小 / 最大元素**:数组头部和尾部元素即是最小和最大元素,使用 $O(1)$ 时间;
观察发现,无序数组和有序数组中的各项操作的时间复杂度“偏科”的,即有的快有的慢**二叉搜索树的各项操作的时间复杂度都是对数阶,在数据量 $n$ 大时有巨大优势**。
观察可知,无序数组和有序数组中的各项操作的时间复杂度呈现“偏科”的特点,即有的快有的慢**然而,二叉搜索树的各项操作的时间复杂度都是对数阶,在数据量 $n$ 大时具有显著优势**。
<div class="center-table" markdown>
@@ -317,18 +317,14 @@
## 二叉搜索树的退化
理想情况下,我们希望二叉搜索树是“左右平衡”的(详见「平衡二叉树」章节),此时可以在 $\log n$ 轮循环内查找任意节点。
理想情况下,我们希望二叉搜索树是“平衡”的,这样就可以在 $\log n$ 轮循环内查找任意节点。
如果我们动态地在二叉搜索树中插入删除节点,**则可能导致二叉树退化为链表**,此时各种操作的时间复杂度也退化 $O(n)$ 。
!!! note
在实际应用中,如何保持二叉搜索树的平衡,也是一个需要重要考虑的问题。
然而,如果我们在二叉搜索树中不断地插入删除节点,可能导致二叉树退化为链表,这时各种操作的时间复杂度也退化 $O(n)$ 。
![二叉搜索树的平衡与退化](binary_search_tree.assets/bst_degradation.png)
## 二叉搜索树常见应用
- 系统中的多级索引,高效查找、插入、删除操作。
- 各种搜索算法的底层数据结构。
- 存储数据流,保持其已排序
- 用作系统中的多级索引,实现高效查找、插入、删除操作。
- 作为某些搜索算法的底层数据结构。
- 用于存储数据流,保持其有序状态

View File

@@ -1,6 +1,6 @@
# 二叉树
「二叉树 Binary Tree」是一种非线性数据结构代表着祖先与后代之间的派生关系体现着“一分为二”的分治逻辑。类似于链表,二叉树也是以节点为单位存储的,节点包含「值」和两个「指针」。
「二叉树 Binary Tree」是一种非线性数据结构代表着祖先与后代之间的派生关系体现着“一分为二”的分治逻辑。与链表类似,二叉树的基本单元是节点,每个节点包含一个「值」和两个「指针」。
=== "Java"
@@ -123,34 +123,34 @@
```
节点的两个指针分别指向「左子节点」和「右子节点」,并且称该节点为两个子节点的「父节点」。给定二叉树节点,将“左子节点及其以下节点形成的树称为该节点的「左子树」,右子树同理
节点的两个指针分别指向「左子节点」和「右子节点」,同时该节点被称为这两个子节点的「父节点」。给定一个二叉树节点时,我们将该节点的左子节点及其以下节点形成的树称为该节点的「左子树」,同理可得「右子树
叶节点外,每个节点都子节点和子树。例如,若将下图的“节点 2”看作父节点,那么其左子节点和右子节点分别“节点 4”和“节点 5”左子树和右子树分别为“节点 4 及其以下节点形成的树”“节点 5 及其以下节点形成的树”。
**在二叉树中,除叶节点外,其他所有节点都包含子节点和非空子树**。例如,在以下示例中,若将“节点 2”视为父节点,其左子节点和右子节点分别“节点 4”和“节点 5”左子树“节点 4 及其以下节点形成的树”,右子树是“节点 5 及其以下节点形成的树”。
![父节点、子节点、子树](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"
@@ -298,7 +298,7 @@
```
**插入与删除节点**。与链表类似,插入与删除节点都可以通过修改指针实现。
**插入与删除节点**。与链表类似,通过修改指针实现插入与删除节点
![在二叉树中插入与删除节点](binary_tree.assets/binary_tree_add_remove.png)
@@ -410,17 +410,17 @@
!!! note
插入节点会改变二叉树的原有逻辑结构,删除节点往往意味着删除该节点所有子树。因此,二叉树中插入与删除一般都是由一套操作配合完成的,这样才能实现有意义的操作。
需要注意的是,插入节点可能会改变二叉树的原有逻辑结构,删除节点通常意味着删除该节点及其所有子树。因此,二叉树中插入与删除操作通常是由一套操作配合完成的,实现有实际意义的操作。
## 常见二叉树类型
### 完美二叉树
「完美二叉树 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)
@@ -428,8 +428,6 @@
「完全二叉树 Complete Binary Tree」只有最底层的节点未被填满且最底层节点尽量靠左填充。
**完全二叉树非常适合用数组来表示**。如果按照层序遍历序列的顺序来存储,那么空节点 `null` 一定全部出现在序列的尾部,因此我们就可以不用存储这些 null 了。
![完全二叉树](binary_tree.assets/complete_binary_tree.png)
### 完满二叉树
@@ -440,15 +438,15 @@
### 平衡二叉树
「平衡二叉树 Balanced Binary Tree」中任意节点的左子树和右子树的高度之差的绝对值 $\leq 1$
「平衡二叉树 Balanced Binary Tree」中任意节点的左子树和右子树的高度之差的绝对值不超过 1 。
![平衡二叉树](binary_tree.assets/balanced_binary_tree.png)
## 二叉树的退化
当二叉树的每层节点都被填满时,达到「完美二叉树」;而当所有节点都偏向一时,二叉树退化为「链表」。
当二叉树的每层节点都被填满时,达到「完美二叉树」;而当所有节点都偏向一时,二叉树退化为「链表」。
- 完美二叉树是一个二叉树的“最佳状态”,可以完全发挥二叉树“分治”的优势;
- 完美二叉树是理想情况,可以充分发挥二叉树“分治”的优势;
- 链表则是另一个极端,各项操作都变为线性操作,时间复杂度退化至 $O(n)$
![二叉树的最佳与最二叉树的最佳和最差结构差情况](binary_tree.assets/binary_tree_corner_cases.png)
@@ -468,19 +466,19 @@
## 二叉树表示方式 *
我们一般使用二叉树的「链表表示」,即存储单位为节点 `TreeNode` ,节点之间通过指针(引用)相连接。本文前述示例代码展示了二叉树在链表表示下的各项基本操作。
我们通常使用二叉树的「链表表示」,即存储单位为节点 `TreeNode` ,节点之间通过指针相连接。本文前述示例代码展示了二叉树在链表表示下的各项基本操作。
那能否可以用「数组表示二叉树呢?答案是肯定的。先来分析一个简单案例,给定一个「完美二叉树」,将节点按照层序遍历的顺序编号(从 0 开始),那么可以推导得出父节点索引与子节点索引之间的映射公式**节点的索引为 $i$ ,则该节点的左子节点索引为 $2i + 1$ 右子节点索引为 $2i + 2$** 。
么,能否用「数组」来表示二叉树呢?答案是肯定的。先来分析一个简单案例,给定一个「完美二叉树」,将节点按照层序遍历的顺序编号(从 0 开始),那么可以推导得出父节点索引与子节点索引之间的映射公式**节点的索引为 $i$ ,则该节点的左子节点索引为 $2i + 1$ 右子节点索引为 $2i + 2$** 。
**本质上,映射公式的作用就是链表中的指针**。对于层序遍历序列中的任意节点,我们都可以使用映射公式来访问子节点。因此,可以直接使用层序遍历序列(即数组)来表示完美二叉树。
**本质上,映射公式的作用相当于链表中的指针**。对于层序遍历序列中的任意节点,我们都可以使用映射公式来访问子节点。因此,我们可以将二叉树的层序遍历序列存储到数组中,利用以上映射公式来表示二叉树。
![完美二叉树的数组表示](binary_tree.assets/array_representation_mapping.png)
然而,完美二叉树只是个例,二叉树中间层往往存在许多空节点(即 `null` ),而层序遍历序列并不包含这些空节点,并且我们无法凭序列来测空节点的数量和分布位置,**理论上存在许多种二叉树都符合该层序遍历序列**。显然,这种情况无法使用数组来存储二叉树。
然而,完美二叉树只是一个特例。在二叉树中间层,通常存在许多 $\text{null}$ ,而层序遍历序列并不包含这些 $\text{null}$ 。我们无法凭序列来测空节点的数量和分布位置,**这意味着理论上存在许多种二叉树都符合该层序遍历序列**。显然,这种情况下,我们无法使用数组来存储二叉树。
![给定数组对应多种二叉树可能性](binary_tree.assets/array_representation_without_empty.png)
为了解决问题,考虑按照完美二叉树的形式来表示所有二叉树,**在序列中使用特殊符号来显式地表示“空位”**。如下图所示,这样处理后,序列(数组)就可以唯一表示二叉树了。
为了解决这个问题,我们可以考虑按照完美二叉树的形式来表示所有二叉树,**在序列中使用特殊符号来显式地表示 $\text{null}$**。如下图所示,这样处理后,层序遍历序列就可以唯一表示二叉树了。
=== "Java"
@@ -561,8 +559,8 @@
![任意类型二叉树的数组表示](binary_tree.assets/array_representation_with_empty.png)
回顾「完全二叉树」的定义,其只有最底层有空节点,并且最底层的节点尽量靠左,因而所有空节点一定出现在层序遍历序列的末尾**因为我们先验地确定了空位的位置,所以在使用数组表示完全二叉树时,可以省略存储“空位”**。因此,完全二叉树非常适合使用数组来表示
**完全二叉树非常适合使用数组来表示**。回顾「完全二叉树」的定义,$\text{null}$ 只出现在最底层,并且最底层的节点尽量靠左。这意味着,**所有空节点一定出现在层序遍历序列的末尾**。由于我们事先知道了所有 $\text{null}$ 的位置,因此在使用数组表示完全二叉树时,可以省略存储它们
![完全二叉树的数组表示](binary_tree.assets/array_representation_complete_binary_tree.png)
数组表示有两个优点: 一是不需要存储指针,节省空间;二是可以随机访问节点。然而,当二叉树中的“空位”很多时,数组中包含很少节点数据空间利用率低。
数组表示有两个显著优点:首先,它不需要存储指针,从而节省空间;其次,它允许随机访问节点。然而,当二叉树中存在大量 $\text{null}$ 时,数组中包含节点数据比重较低,导致有效空间利用率低。

View File

@@ -1,20 +1,20 @@
# 二叉树遍历
从物理结构角度看,树是一种基于链表的数据结构,因此遍历方式是通过指针(即引用)逐个遍历节点。同时,树是一种非线性数据结构,这导致遍历树比遍历链表更加复杂,需要使用搜索算法来实现。
从物理结构角度看,树是一种基于链表的数据结构,因此遍历方式是通过指针逐个访问节点。然而,树是一种非线性数据结构,这使得遍历树比遍历链表更加复杂,需要借助搜索算法来实现。
常见的二叉树遍历方式层序遍历、前序遍历、中序遍历后序遍历。
二叉树常见的遍历方式包括层序遍历、前序遍历、中序遍历后序遍历
## 层序遍历
「层序遍历 Level-Order Traversal」从顶至底、一层一层地遍历二叉树,并在每层按照从左到右的顺序访问节点。
「层序遍历 Level-Order Traversal」从顶部到底部逐层遍历二叉树,并在每层按照从左到右的顺序访问节点。
层序遍历本质上「广度优先搜索 Breadth-First Traversal」体现一种“一圈一圈向外”的层进遍历方式。
层序遍历本质上属于「广度优先搜索 Breadth-First Traversal」体现一种“一圈一圈向外扩展”的逐层搜索方式。
![二叉树的层序遍历](binary_tree_traversal.assets/binary_tree_bfs.png)
### 算法实现
广度优先遍历一般借助「队列」来实现。队列的规则是“先进先出”广度优先遍历的规则是“一层层平推”,两者背后的思想是一致的。
广度优先遍历通常借助「队列」来实现。队列遵循“先进先出”的规则,而广度优先遍历则遵循“逐层推进”的规则,两者背后的思想是一致的。
=== "Java"
@@ -80,13 +80,13 @@
**时间复杂度**:所有节点被访问一次,使用 $O(n)$ 时间,其中 $n$ 为节点数量。
**空间复杂度**当为满二叉树时达到最差情况,遍历到最底层前,队列中最多同时存在 $\frac{n + 1}{2}$ 个节点,使用 $O(n)$ 空间。
**空间复杂度**在最差情况下,即满二叉树时,遍历到最底层前,队列中最多同时存在 $\frac{n + 1}{2}$ 个节点,用 $O(n)$ 空间。
## 前序、中序、后序遍历
地,前、中后序遍历属于「深度优先遍历 Depth-First Traversal」体现一种“先走到尽头,再回继续”的回溯遍历方式。
地,前、中序和后序遍历属于「深度优先遍历 Depth-First Traversal」体现一种“先走到尽头,再回继续”的遍历方式。
如下图所示,左侧是深度优先遍历的示意图,右上方是对应的递归实现代码。深度优先遍历就像是绕着整个二叉树的外围“走”一圈,走的过程中,在每个节点都会遇到三个位置,分别对应前序遍历、中序遍历后序遍历。
如下图所示,左侧是深度优先遍历的示意图,右上方是对应的递归实现代码。深度优先遍历就像是绕着整个二叉树的外围“走”一圈,在这个过程中,在每个节点都会遇到三个位置,分别对应前序遍历、中序遍历后序遍历。
![二叉搜索树的前、中、后序遍历](binary_tree_traversal.assets/binary_tree_dfs.png)
@@ -210,4 +210,4 @@
**时间复杂度**:所有节点被访问一次,使用 $O(n)$ 时间,其中 $n$ 为节点数量。
**空间复杂度**树退化为链表时达到最差情况,递归深度达到 $n$ ,系统使用 $O(n)$ 栈帧空间。
**空间复杂度**在最差情况下,即树退化为链表时,递归深度达到 $n$ ,系统用 $O(n)$ 栈帧空间。

View File

@@ -1,21 +1,13 @@
# 小结
### 二叉树
- 二叉树是一种非线性数据结构,代表着“一分为二”的分治逻辑。二叉树的节点包含「值」和两个「指针」,分别指向左子节点和右子节点
- 选定二叉树中某节点,将其左(右)子节点以下形成的树称为左(右)子树
- 二叉树的术语较多,包括根节点、叶节点、层、度、边、高度、深度等
- 二叉树的初始化、节点插入、节点删除操作与链表的操作方法类似
- 常见的二叉树类型包括完美二叉树、完全二叉树、完满二叉树、平衡二叉树。完美二叉树是理想状态,链表则是退化后的最差状态
- 二叉树可以使用数组表示,具体做法是将节点值和空位按照层序遍历的顺序排列,并基于父节点和子节点之间的索引映射公式实现指针
### 二叉树遍历
- 二叉树层序遍历是一种广度优先搜索,体现着“一圈一圈向外”的层进式遍历方式,通常借助队列来实现。
- 前序、中序、后序遍历是深度优先搜索,体现着“走到头、再回头继续”的回溯遍历方式,通常使用递归实现。
### 二叉搜索树
- 二叉搜索树是一种高效的元素查找数据结构,查找、插入、删除操作的时间复杂度皆为 $O(\log n)$ 。二叉搜索树退化为链表后,各项时间复杂度劣化至 $O(n)$ ,因此如何避免退化是非常重要的课题。
- AVL 树又称平衡二叉搜索树,其通过旋转操作,使得在不断插入与删除节点后,仍然可以保持二叉树的平衡(不退化)。
- AVL 树的旋转操作分为右旋、左旋、先右旋后左旋、先左旋后右旋。在插入或删除节点后AVL 树会从底至顶地执行旋转操作,使树恢复平衡。
- 二叉树是一种非线性数据结构,体现“一分为二”的分治逻辑。每个二叉树节点包含一个值以及两个指针,分别指向其左子节点和右子节点。
- 对于二叉树中的某个节点,其左(右)子节点及其以下形成的树被称为该节点的左(右)子树。
- 二叉树的相关术语包括根节点、叶节点、层、度、边、高度和深度等
- 二叉树的初始化、节点插入和节点删除操作与链表操作方法类似
- 常见的二叉树类型有完美二叉树、完全二叉树、满二叉树和平衡二叉树。完美二叉树是最理想的状态,而链表是退化后的最差状态
- 二叉树可以用数组表示,方法是将节点值和空位按层序遍历顺序排列,并根据父节点与子节点之间的索引映射关系来实现指针
- 二叉树的层序遍历是一种广度优先搜索方法,它体现了“一圈一圈向外”的分层遍历方式,通常通过队列来实现
- 前序、中序、后序遍历皆属于深度优先搜索,它们体现了“走到尽头,再回头继续”的回溯遍历方式,通常使用递归来实现
- 二叉搜索树是一种高效的元素查找数据结构,其查找、插入和删除操作的时间复杂度均为 $O(\log n)$ 。当二叉搜索树退化为链表时,各项时间复杂度会劣化至 $O(n)$ 。
- AVL 树,也称为平衡二叉搜索树,它通过旋转操作,确保在不断插入和删除节点后,树仍然保持平衡。
- AVL 树的旋转操作包括右旋、左旋、先右旋再左旋、先左旋再右旋。在插入或删除节点后AVL 树会从底向顶执行旋转操作,使树重新恢复平衡。