This commit is contained in:
krahets
2023-08-17 05:12:05 +08:00
parent f0826da7f6
commit 97c532b228
67 changed files with 1481 additions and 1066 deletions
+94 -5
View File
@@ -16,7 +16,7 @@ comments: true
![完美二叉树的数组表示](array_representation_of_tree.assets/array_representation_binary_tree.png)
<p align="center"> Fig. 完美二叉树的数组表示 </p>
<p align="center"> 图:完美二叉树的数组表示 </p>
**映射公式的角色相当于链表中的指针**。给定数组中的任意一个节点,我们都可以通过映射公式来访问它的左(右)子节点。
@@ -26,7 +26,7 @@ comments: true
![层序遍历序列对应多种二叉树可能性](array_representation_of_tree.assets/array_representation_without_empty.png)
<p align="center"> Fig. 层序遍历序列对应多种二叉树可能性 </p>
<p align="center"> 图:层序遍历序列对应多种二叉树可能性 </p>
为了解决此问题,**我们可以考虑在层序遍历序列中显式地写出所有 $\text{None}$** 。如下图所示,这样处理后,层序遍历序列就可以唯一表示二叉树了。
@@ -124,13 +124,13 @@ comments: true
![任意类型二叉树的数组表示](array_representation_of_tree.assets/array_representation_with_empty.png)
<p align="center"> Fig. 任意类型二叉树的数组表示 </p>
<p align="center"> 图:任意类型二叉树的数组表示 </p>
值得说明的是,**完全二叉树非常适合使用数组来表示**。回顾完全二叉树的定义,$\text{None}$ 只出现在最底层且靠右的位置,**因此所有 $\text{None}$ 一定出现在层序遍历序列的末尾**。这意味着使用数组表示完全二叉树时,可以省略存储所有 $\text{None}$ ,非常方便。
![完全二叉树的数组表示](array_representation_of_tree.assets/array_representation_complete_binary_tree.png)
<p align="center"> Fig. 完全二叉树的数组表示 </p>
<p align="center"> 图:完全二叉树的数组表示 </p>
如下代码给出了数组表示下的二叉树的简单实现,包括以下操作:
@@ -960,7 +960,96 @@ comments: true
=== "Dart"
```dart title="array_binary_tree.dart"
[class]{ArrayBinaryTree}-[func]{}
/* 数组表示下的二叉树类 */
class ArrayBinaryTree {
late List<int?> _tree;
/* 构造方法 */
ArrayBinaryTree(this._tree);
/* 节点数量 */
int size() {
return _tree.length;
}
/* 获取索引为 i 节点的值 */
int? val(int i) {
// 若索引越界,则返回 null ,代表空位
if (i < 0 || i >= size()) {
return null;
}
return _tree[i];
}
/* 获取索引为 i 节点的左子节点的索引 */
int? left(int i) {
return 2 * i + 1;
}
/* 获取索引为 i 节点的右子节点的索引 */
int? right(int i) {
return 2 * i + 2;
}
/* 获取索引为 i 节点的父节点的索引 */
int? parent(int i) {
return (i - 1) ~/ 2;
}
/* 层序遍历 */
List<int> levelOrder() {
List<int> res = [];
for (int i = 0; i < size(); i++) {
if (val(i) != null) {
res.add(val(i)!);
}
}
return res;
}
/* 深度优先遍历 */
void dfs(int i, String order, List<int?> res) {
// 若为空位,则返回
if (val(i) == null) {
return;
}
// 前序遍历
if (order == 'pre') {
res.add(val(i));
}
dfs(left(i)!, order, res);
// 中序遍历
if (order == 'in') {
res.add(val(i));
}
dfs(right(i)!, order, res);
// 后序遍历
if (order == 'post') {
res.add(val(i));
}
}
/* 前序遍历 */
List<int?> preOrder() {
List<int?> res = [];
dfs(0, 'pre', res);
return res;
}
/* 中序遍历 */
List<int?> inOrder() {
List<int?> res = [];
dfs(0, 'in', res);
return res;
}
/* 后序遍历 */
List<int?> postOrder() {
List<int?> res = [];
dfs(0, 'post', res);
return res;
}
}
```
=== "Rust"
+11 -8
View File
@@ -10,13 +10,13 @@ comments: true
![AVL 树在删除节点后发生退化](avl_tree.assets/avltree_degradation_from_removing_node.png)
<p align="center"> Fig. AVL 树在删除节点后发生退化 </p>
<p align="center"> 图:AVL 树在删除节点后发生退化 </p>
再例如,在以下完美二叉树中插入两个节点后,树将严重向左倾斜,查找操作的时间复杂度也随之恶化。
![AVL 树在插入节点后发生退化](avl_tree.assets/avltree_degradation_from_inserting_node.png)
<p align="center"> Fig. AVL 树在插入节点后发生退化 </p>
<p align="center"> 图:AVL 树在插入节点后发生退化 </p>
G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorithm for the organization of information" 中提出了「AVL 树」。论文中详细描述了一系列操作,确保在持续添加和删除节点后,AVL 树不会退化,从而使得各种操作的时间复杂度保持在 $O(\log n)$ 级别。换句话说,在需要频繁进行增删查改操作的场景中,AVL 树能始终保持高效的数据操作性能,具有很好的应用价值。
@@ -384,6 +384,7 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit
```dart title="avl_tree.dart"
/* 获取节点高度 */
int height(TreeNode? node) {
// 空节点高度为 -1 ,叶节点高度为 0
return node == null ? -1 : node.height;
}
@@ -601,11 +602,13 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉
=== "<4>"
![avltree_right_rotate_step4](avl_tree.assets/avltree_right_rotate_step4.png)
<p align="center"> 图:右旋操作步骤 </p>
此外,如果节点 `child` 本身有右子节点(记为 `grandChild` ),则需要在「右旋」中添加一步:将 `grandChild` 作为 `node` 的左子节点。
![有 grandChild 的右旋操作](avl_tree.assets/avltree_right_rotate_with_grandchild.png)
<p align="center"> Fig. 有 grandChild 的右旋操作 </p>
<p align="center"> 图:有 grandChild 的右旋操作 </p>
“向右旋转”是一种形象化的说法,实际上需要通过修改节点指针来实现,代码如下所示。
@@ -836,13 +839,13 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉
![左旋操作](avl_tree.assets/avltree_left_rotate.png)
<p align="center"> Fig. 左旋操作 </p>
<p align="center"> 图:左旋操作 </p>
同理,若节点 `child` 本身有左子节点(记为 `grandChild` ),则需要在「左旋」中添加一步:将 `grandChild` 作为 `node` 的右子节点。
![有 grandChild 的左旋操作](avl_tree.assets/avltree_left_rotate_with_grandchild.png)
<p align="center"> Fig. 有 grandChild 的左旋操作 </p>
<p align="center"> 图:有 grandChild 的左旋操作 </p>
可以观察到,**右旋和左旋操作在逻辑上是镜像对称的,它们分别解决的两种失衡情况也是对称的**。基于对称性,我们可以轻松地从右旋的代码推导出左旋的代码。具体地,只需将「右旋」代码中的把所有的 `left` 替换为 `right` ,将所有的 `right` 替换为 `left` ,即可得到「左旋」代码。
@@ -1073,7 +1076,7 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉
![先左旋后右旋](avl_tree.assets/avltree_left_right_rotate.png)
<p align="center"> Fig. 先左旋后右旋 </p>
<p align="center"> 图:先左旋后右旋 </p>
### 先右旋后左旋
@@ -1081,7 +1084,7 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉
![先右旋后左旋](avl_tree.assets/avltree_right_left_rotate.png)
<p align="center"> Fig. 先右旋后左旋 </p>
<p align="center"> 图:先右旋后左旋 </p>
### 旋转的选择
@@ -1089,7 +1092,7 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉
![AVL 树的四种旋转情况](avl_tree.assets/avltree_rotation_cases.png)
<p align="center"> Fig. AVL 树的四种旋转情况 </p>
<p align="center"> 图:AVL 树的四种旋转情况 </p>
在代码中,我们通过判断失衡节点的平衡因子以及较高一侧子节点的平衡因子的正负号,来确定失衡节点属于上图中的哪种情况。
+127 -9
View File
@@ -11,7 +11,7 @@ comments: true
![二叉搜索树](binary_search_tree.assets/binary_search_tree.png)
<p align="center"> Fig. 二叉搜索树 </p>
<p align="center"> 图:二叉搜索树 </p>
## 7.4.1. &nbsp; 二叉搜索树的操作
@@ -37,6 +37,8 @@ comments: true
=== "<4>"
![bst_search_step4](binary_search_tree.assets/bst_search_step4.png)
<p align="center"> 图:二叉搜索树查找节点示例 </p>
二叉搜索树的查找操作与二分查找算法的工作原理一致,都是每轮排除一半情况。循环次数最多为二叉树的高度,当二叉树平衡时,使用 $O(\log n)$ 时间。
=== "Java"
@@ -270,7 +272,24 @@ comments: true
=== "Dart"
```dart title="binary_search_tree.dart"
[class]{BinarySearchTree}-[func]{search}
/* 查找节点 */
TreeNode? search(int num) {
TreeNode? cur = _root;
// 循环查找,越过叶节点后跳出
while (cur != null) {
// 目标节点在 cur 的右子树中
if (cur.val < num)
cur = cur.right;
// 目标节点在 cur 的左子树中
else if (cur.val > num)
cur = cur.left;
// 找到目标节点,跳出循环
else
break;
}
// 返回目标节点
return cur;
}
```
=== "Rust"
@@ -311,7 +330,7 @@ comments: true
![在二叉搜索树中插入节点](binary_search_tree.assets/bst_insert.png)
<p align="center"> Fig. 在二叉搜索树中插入节点 </p>
<p align="center"> 图:在二叉搜索树中插入节点 </p>
=== "Java"
@@ -640,7 +659,31 @@ comments: true
=== "Dart"
```dart title="binary_search_tree.dart"
[class]{BinarySearchTree}-[func]{insert}
/* 插入节点 */
void insert(int num) {
// 若树为空,直接提前返回
if (_root == null) return;
TreeNode? cur = _root;
TreeNode? pre = null;
// 循环查找,越过叶节点后跳出
while (cur != null) {
// 找到重复节点,直接返回
if (cur.val == num) return;
pre = cur;
// 插入位置在 cur 的右子树中
if (cur.val < num)
cur = cur.right;
// 插入位置在 cur 的左子树中
else
cur = cur.left;
}
// 插入节点
TreeNode? node = TreeNode(num);
if (pre!.val < num)
pre.right = node;
else
pre.left = node;
}
```
=== "Rust"
@@ -693,13 +736,13 @@ comments: true
![在二叉搜索树中删除节点(度为 0](binary_search_tree.assets/bst_remove_case1.png)
<p align="center"> Fig. 在二叉搜索树中删除节点(度为 0) </p>
<p align="center"> 图:在二叉搜索树中删除节点(度为 0) </p>
当待删除节点的度为 $1$ 时,将待删除节点替换为其子节点即可。
![在二叉搜索树中删除节点(度为 1](binary_search_tree.assets/bst_remove_case2.png)
<p align="center"> Fig. 在二叉搜索树中删除节点(度为 1) </p>
<p align="center"> 图:在二叉搜索树中删除节点(度为 1) </p>
当待删除节点的度为 $2$ 时,我们无法直接删除它,而需要使用一个节点替换该节点。由于要保持二叉搜索树“左 $<$ 根 $<$ 右”的性质,因此这个节点可以是右子树的最小节点或左子树的最大节点。
@@ -720,6 +763,8 @@ comments: true
=== "<4>"
![bst_remove_case3_step4](binary_search_tree.assets/bst_remove_case3_step4.png)
<p align="center"> 图:二叉搜索树删除节点示例 </p>
删除节点操作同样使用 $O(\log n)$ 时间,其中查找待删除节点需要 $O(\log n)$ 时间,获取中序遍历后继节点需要 $O(\log n)$ 时间。
=== "Java"
@@ -1283,7 +1328,80 @@ comments: true
=== "Dart"
```dart title="binary_search_tree.dart"
[class]{BinarySearchTree}-[func]{remove}
/* 插入节点 */
void insert(int num) {
// 若树为空,直接提前返回
if (_root == null) return;
TreeNode? cur = _root;
TreeNode? pre = null;
// 循环查找,越过叶节点后跳出
while (cur != null) {
// 找到重复节点,直接返回
if (cur.val == num) return;
pre = cur;
// 插入位置在 cur 的右子树中
if (cur.val < num)
cur = cur.right;
// 插入位置在 cur 的左子树中
else
cur = cur.left;
}
// 插入节点
TreeNode? node = TreeNode(num);
if (pre!.val < num)
pre.right = node;
else
pre.left = node;
}
/* 删除节点 */
void remove(int num) {
// 若树为空,直接提前返回
if (_root == null) return;
TreeNode? cur = _root;
TreeNode? pre = null;
// 循环查找,越过叶节点后跳出
while (cur != null) {
// 找到待删除节点,跳出循环
if (cur.val == num) break;
pre = cur;
// 待删除节点在 cur 的右子树中
if (cur.val < num)
cur = cur.right;
// 待删除节点在 cur 的左子树中
else
cur = cur.left;
}
// 若无待删除节点,直接返回
if (cur == null) return;
// 子节点数量 = 0 or 1
if (cur.left == null || cur.right == null) {
// 当子节点数量 = 0 / 1 时, child = null / 该子节点
TreeNode? child = cur.left ?? cur.right;
// 删除节点 cur
if (cur != _root) {
if (pre!.left == cur)
pre.left = child;
else
pre.right = child;
} else {
// 若删除节点为根节点,则重新指定根节点
_root = child;
}
} else {
// 子节点数量 = 2
// 获取中序遍历中 cur 的下一个节点
TreeNode? tmp = cur.right;
while (tmp!.left != null) {
tmp = tmp.left;
}
// 递归删除节点 tmp
remove(tmp.val);
// 用 tmp 覆盖 cur
cur.val = tmp.val;
}
}
```
=== "Rust"
@@ -1364,7 +1482,7 @@ comments: true
![二叉搜索树的中序遍历序列](binary_search_tree.assets/bst_inorder_traversal.png)
<p align="center"> Fig. 二叉搜索树的中序遍历序列 </p>
<p align="center"> 图:二叉搜索树的中序遍历序列 </p>
## 7.4.2. &nbsp; 二叉搜索树的效率
@@ -1388,7 +1506,7 @@ comments: true
![二叉搜索树的平衡与退化](binary_search_tree.assets/bst_degradation.png)
<p align="center"> Fig. 二叉搜索树的平衡与退化 </p>
<p align="center"> 图:二叉搜索树的平衡与退化 </p>
## 7.4.3. &nbsp; 二叉搜索树常见应用
+26 -26
View File
@@ -4,7 +4,7 @@ comments: true
# 7.1. &nbsp; 二叉树
「二叉树 Binary Tree」是一种非线性数据结构,代表着祖先与后代之间的派生关系,体现着“一分为二”的分治逻辑。与链表类似,二叉树的基本单元是节点,每个节点包含一个「值」和两个「指针」
「二叉树 Binary Tree」是一种非线性数据结构,代表着祖先与后代之间的派生关系,体现着“一分为二”的分治逻辑。与链表类似,二叉树的基本单元是节点,每个节点包含:值、左子节点引用、右子节点引用
=== "Java"
@@ -12,8 +12,8 @@ comments: true
/* 二叉树节点类 */
class TreeNode {
int val; // 节点值
TreeNode left; // 左子节点指针
TreeNode right; // 右子节点指针
TreeNode left; // 左子节点引用
TreeNode right; // 右子节点引用
TreeNode(int x) { val = x; }
}
```
@@ -37,8 +37,8 @@ comments: true
"""二叉树节点类"""
def __init__(self, val: int):
self.val: int = val # 节点值
self.left: Optional[TreeNode] = None # 左子节点指针
self.right: Optional[TreeNode] = None # 右子节点指针
self.left: Optional[TreeNode] = None # 左子节点引用
self.right: Optional[TreeNode] = None # 右子节点引用
```
=== "Go"
@@ -53,9 +53,9 @@ comments: true
/* 节点初始化方法 */
func NewTreeNode(v int) *TreeNode {
return &TreeNode{
Left: nil,
Right: nil,
Val: v,
Left: nil, // 左子节点指针
Right: nil, // 右子节点指针
Val: v, // 节点值
}
}
```
@@ -66,8 +66,8 @@ comments: true
/* 二叉树节点类 */
function TreeNode(val, left, right) {
this.val = (val === undefined ? 0 : val); // 节点值
this.left = (left === undefined ? null : left); // 左子节点指针
this.right = (right === undefined ? null : right); // 右子节点指针
this.left = (left === undefined ? null : left); // 左子节点引用
this.right = (right === undefined ? null : right); // 右子节点引用
}
```
@@ -82,8 +82,8 @@ comments: true
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.left = left === undefined ? null : left; // 左子节点引用
this.right = right === undefined ? null : right; // 右子节点引用
}
}
```
@@ -120,8 +120,8 @@ comments: true
/* 二叉树节点类 */
class TreeNode {
int val; // 节点值
TreeNode? left; // 左子节点指针
TreeNode? right; // 右子节点指针
TreeNode? left; // 左子节点引用
TreeNode? right; // 右子节点引用
TreeNode(int x) { val = x; }
}
```
@@ -132,8 +132,8 @@ comments: true
/* 二叉树节点类 */
class TreeNode {
var val: Int // 节点值
var left: TreeNode? // 左子节点指针
var right: TreeNode? // 右子节点指针
var left: TreeNode? // 左子节点引用
var right: TreeNode? // 右子节点引用
init(x: Int) {
val = x
@@ -153,8 +153,8 @@ comments: true
/* 二叉树节点类 */
class TreeNode {
int val; // 节点值
TreeNode? left; // 左子节点指针
TreeNode? right; // 右子节点指针
TreeNode? left; // 左子节点引用
TreeNode? right; // 右子节点引用
TreeNode(this.val, [this.left, this.right]);
}
```
@@ -171,7 +171,7 @@ comments: true
![父节点、子节点、子树](binary_tree.assets/binary_tree_definition.png)
<p align="center"> Fig. 父节点、子节点、子树 </p>
<p align="center"> 图:父节点、子节点、子树 </p>
## 7.1.1. &nbsp; 二叉树常见术语
@@ -188,7 +188,7 @@ comments: true
![二叉树的常用术语](binary_tree.assets/binary_tree_terminology.png)
<p align="center"> Fig. 二叉树的常用术语 </p>
<p align="center"> 图:二叉树的常用术语 </p>
!!! tip "高度与深度的定义"
@@ -382,7 +382,7 @@ comments: true
![在二叉树中插入与删除节点](binary_tree.assets/binary_tree_add_remove.png)
<p align="center"> Fig. 在二叉树中插入与删除节点 </p>
<p align="center"> 图:在二叉树中插入与删除节点 </p>
=== "Java"
@@ -530,7 +530,7 @@ comments: true
![完美二叉树](binary_tree.assets/perfect_binary_tree.png)
<p align="center"> Fig. 完美二叉树 </p>
<p align="center"> 图:完美二叉树 </p>
### 完全二叉树
@@ -538,7 +538,7 @@ comments: true
![完全二叉树](binary_tree.assets/complete_binary_tree.png)
<p align="center"> Fig. 完全二叉树 </p>
<p align="center"> 图:完全二叉树 </p>
### 完满二叉树
@@ -546,7 +546,7 @@ comments: true
![完满二叉树](binary_tree.assets/full_binary_tree.png)
<p align="center"> Fig. 完满二叉树 </p>
<p align="center"> 图:完满二叉树 </p>
### 平衡二叉树
@@ -554,7 +554,7 @@ comments: true
![平衡二叉树](binary_tree.assets/balanced_binary_tree.png)
<p align="center"> Fig. 平衡二叉树 </p>
<p align="center"> 图:平衡二叉树 </p>
## 7.1.4. &nbsp; 二叉树的退化
@@ -565,7 +565,7 @@ comments: true
![二叉树的最佳与最差结构](binary_tree.assets/binary_tree_best_worst_cases.png)
<p align="center"> Fig. 二叉树的最佳与最差结构 </p>
<p align="center"> 图:二叉树的最佳与最差结构 </p>
如下表所示,在最佳和最差结构下,二叉树的叶节点数量、节点总数、高度等达到极大或极小值。
+4 -2
View File
@@ -16,7 +16,7 @@ comments: true
![二叉树的层序遍历](binary_tree_traversal.assets/binary_tree_bfs.png)
<p align="center"> Fig. 二叉树的层序遍历 </p>
<p align="center"> 图:二叉树的层序遍历 </p>
广度优先遍历通常借助「队列」来实现。队列遵循“先进先出”的规则,而广度优先遍历则遵循“逐层推进”的规则,两者背后的思想是一致的。
@@ -334,7 +334,7 @@ comments: true
![二叉搜索树的前、中、后序遍历](binary_tree_traversal.assets/binary_tree_dfs.png)
<p align="center"> Fig. 二叉搜索树的前、中、后序遍历 </p>
<p align="center"> 图:二叉搜索树的前、中、后序遍历 </p>
以下给出了实现代码,请配合上图理解深度优先遍历的递归过程。
@@ -794,3 +794,5 @@ comments: true
=== "<11>"
![preorder_step11](binary_tree_traversal.assets/preorder_step11.png)
<p align="center"> 图:前序遍历的递归过程 </p>