This commit is contained in:
krahets
2023-06-02 02:38:24 +08:00
parent 874e75d92d
commit 2a85d796e6
35 changed files with 2354 additions and 0 deletions
@@ -108,6 +108,12 @@ comments: true
```
=== "Dart"
```dart title=""
```
![任意类型二叉树的数组表示](binary_tree.assets/array_representation_with_empty.png)
<p align="center"> Fig. 任意类型二叉树的数组表示 </p>
+172
View File
@@ -177,6 +177,12 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit
```
=== "Dart"
```dart title=""
```
「节点高度」是指从该节点到最远叶节点的距离,即所经过的“边”的数量。需要特别注意的是,叶节点的高度为 0 ,而空节点的高度为 -1 。我们将创建两个工具函数,分别用于获取和更新节点的高度。
=== "Java"
@@ -360,6 +366,21 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit
}
```
=== "Dart"
```dart title="avl_tree.dart"
/* 获取节点高度 */
int height(TreeNode? node) {
return node == null ? -1 : node.height;
}
/* 更新节点高度 */
void updateHeight(TreeNode? node) {
// 节点高度等于最高子树高度 + 1
node!.height = max(height(node.left), height(node.right)) + 1;
}
```
### 节点平衡因子
节点的「平衡因子 Balance Factor」定义为节点左子树的高度减去右子树的高度,同时规定空节点的平衡因子为 0 。我们同样将获取节点平衡因子的功能封装成函数,方便后续使用。
@@ -490,6 +511,18 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit
}
```
=== "Dart"
```dart title="avl_tree.dart"
/* 获取平衡因子 */
int balanceFactor(TreeNode? node) {
// 空节点平衡因子为 0
if (node == null) return 0;
// 节点平衡因子 = 左子树高度 - 右子树高度
return height(node.left) - height(node.right);
}
```
!!! note
设平衡因子为 $f$ ,则一棵 AVL 树的任意节点的平衡因子皆满足 $-1 \le f \le 1$ 。
@@ -704,6 +737,24 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉
}
```
=== "Dart"
```dart title="avl_tree.dart"
/* 右旋操作 */
TreeNode? rightRotate(TreeNode? node) {
TreeNode? child = node!.left;
TreeNode? grandChild = child!.right;
// 以 child 为原点,将 node 向右旋转
child.right = node;
node.left = grandChild;
// 更新节点高度
updateHeight(node);
updateHeight(child);
// 返回旋转后子树的根节点
return child;
}
```
### 左旋
相应的,如果考虑上述失衡二叉树的“镜像”,则需要执行「左旋」操作。
@@ -900,6 +951,24 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉
}
```
=== "Dart"
```dart title="avl_tree.dart"
/* 左旋操作 */
TreeNode? leftRotate(TreeNode? node) {
TreeNode? child = node!.right;
TreeNode? grandChild = child!.left;
// 以 child 为原点,将 node 向左旋转
child.left = node;
node.right = grandChild;
// 更新节点高度
updateHeight(node);
updateHeight(child);
// 返回旋转后子树的根节点
return child;
}
```
### 先左旋后右旋
对于下图中的失衡节点 3,仅使用左旋或右旋都无法使子树恢复平衡。此时需要先左旋后右旋,即先对 `child` 执行「左旋」,再对 `node` 执行「右旋」。
@@ -1275,6 +1344,40 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉
}
```
=== "Dart"
```dart title="avl_tree.dart"
/* 执行旋转操作,使该子树重新恢复平衡 */
TreeNode? rotate(TreeNode? node) {
// 获取节点 node 的平衡因子
int factor = balanceFactor(node);
// 左偏树
if (factor > 1) {
if (balanceFactor(node!.left) >= 0) {
// 右旋
return rightRotate(node);
} else {
// 先左旋后右旋
node.left = leftRotate(node.left);
return rightRotate(node);
}
}
// 右偏树
if (factor < -1) {
if (balanceFactor(node!.right) <= 0) {
// 左旋
return leftRotate(node);
} else {
// 先右旋后左旋
node.right = rightRotate(node.right);
return leftRotate(node);
}
}
// 平衡树,无需旋转,直接返回
return node;
}
```
## 7.5.3. &nbsp; AVL 树常用操作
### 插入节点
@@ -1562,6 +1665,32 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉
}
```
=== "Dart"
```dart title="avl_tree.dart"
/* 插入节点 */
void insert(int val) {
root = insertHelper(root, val);
}
/* 递归插入节点(辅助方法) */
TreeNode? insertHelper(TreeNode? node, int val) {
if (node == null) return TreeNode(val);
/* 1. 查找插入位置,并插入节点 */
if (val < node.val)
node.left = insertHelper(node.left, val);
else if (val > node.val)
node.right = insertHelper(node.right, val);
else
return node; // 重复节点不插入,直接返回
updateHeight(node); // 更新节点高度
/* 2. 执行旋转操作,使该子树重新恢复平衡 */
node = rotate(node);
// 返回子树的根节点
return node;
}
```
### 删除节点
类似地,在二叉搜索树的删除节点方法的基础上,需要从底至顶地执行旋转操作,使所有失衡节点恢复平衡。
@@ -2021,6 +2150,49 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉
}
```
=== "Dart"
```dart title="avl_tree.dart"
/* 删除节点 */
void remove(int val) {
root = removeHelper(root, val);
}
/* 递归删除节点(辅助方法) */
TreeNode? removeHelper(TreeNode? node, int val) {
if (node == null) return null;
/* 1. 查找节点,并删除之 */
if (val < node.val)
node.left = removeHelper(node.left, val);
else if (val > node.val)
node.right = removeHelper(node.right, val);
else {
if (node.left == null || node.right == null) {
TreeNode? child = node.left ?? node.right;
// 子节点数量 = 0 ,直接删除 node 并返回
if (child == null)
return null;
// 子节点数量 = 1 ,直接删除 node
else
node = child;
} else {
// 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点
TreeNode? temp = node.right;
while (temp!.left != null) {
temp = temp.left;
}
node.right = removeHelper(node.right, temp.val);
node.val = temp.val;
}
}
updateHeight(node); // 更新节点高度
/* 2. 执行旋转操作,使该子树重新恢复平衡 */
node = rotate(node);
// 返回子树的根节点
return node;
}
```
### 查找节点
AVL 树的节点查找操作与二叉搜索树一致,在此不再赘述。
+18
View File
@@ -265,6 +265,12 @@ comments: true
}
```
=== "Dart"
```dart title="binary_search_tree.dart"
[class]{BinarySearchTree}-[func]{search}
```
### 插入节点
给定一个待插入元素 `num` ,为了保持二叉搜索树“左子树 < 根节点 < 右子树”的性质,插入操作分为两步:
@@ -602,6 +608,12 @@ comments: true
}
```
=== "Dart"
```dart title="binary_search_tree.dart"
[class]{BinarySearchTree}-[func]{insert}
```
为了插入节点,我们需要利用辅助节点 `pre` 保存上一轮循环的节点,这样在遍历至 $\text{null}$ 时,我们可以获取到其父节点,从而完成节点插入操作。
与查找节点相同,插入节点使用 $O(\log n)$ 时间。
@@ -1201,6 +1213,12 @@ comments: true
}
```
=== "Dart"
```dart title="binary_search_tree.dart"
[class]{BinarySearchTree}-[func]{remove}
```
### 排序
我们知道,二叉树的中序遍历遵循“左 $\rightarrow$ 根 $\rightarrow$ 右”的遍历顺序,而二叉搜索树满足“左子节点 $<$ 根节点 $<$ 右子节点”的大小关系。因此,在二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小节点,从而得出一个重要性质:**二叉搜索树的中序遍历序列是升序的**。
+18
View File
@@ -147,6 +147,12 @@ comments: true
```
=== "Dart"
```dart title=""
```
节点的两个指针分别指向「左子节点」和「右子节点」,同时该节点被称为这两个子节点的「父节点」。当给定一个二叉树的节点时,我们将该节点的左子节点及其以下节点形成的树称为该节点的「左子树」,同理可得「右子树」。
**在二叉树中,除叶节点外,其他所有节点都包含子节点和非空子树**。例如,在以下示例中,若将“节点 2”视为父节点,则其左子节点和右子节点分别是“节点 4”和“节点 5”,左子树是“节点 4 及其以下节点形成的树”,右子树是“节点 5 及其以下节点形成的树”。
@@ -337,6 +343,12 @@ comments: true
```
=== "Dart"
```dart title="binary_tree.dart"
```
**插入与删除节点**。与链表类似,通过修改指针来实现插入与删除节点。
![在二叉树中插入与删除节点](binary_tree.assets/binary_tree_add_remove.png)
@@ -455,6 +467,12 @@ comments: true
```
=== "Dart"
```dart title="binary_tree.dart"
```
!!! note
需要注意的是,插入节点可能会改变二叉树的原有逻辑结构,而删除节点通常意味着删除该节点及其所有子树。因此,在二叉树中,插入与删除操作通常是由一套操作配合完成的,以实现有实际意义的操作。
+51
View File
@@ -279,6 +279,26 @@ comments: true
}
```
=== "Dart"
```dart title="binary_tree_bfs.dart"
/* 层序遍历 */
List<int> levelOrder(TreeNode? root) {
// 初始化队列,加入根节点
Queue<TreeNode?> queue = Queue();
queue.add(root);
// 初始化一个列表,用于保存遍历序列
List<int> res = [];
while (queue.isNotEmpty) {
TreeNode? node = queue.removeFirst(); // 队列出队
res.add(node!.val); // 保存节点值
if (node.left != null) queue.add(node.left); // 左子节点入队
if (node.right != null) queue.add(node.right); // 右子节点入队
}
return res;
}
```
### 复杂度分析
**时间复杂度**:所有节点被访问一次,使用 $O(n)$ 时间,其中 $n$ 为节点数量。
@@ -644,6 +664,37 @@ comments: true
}
```
=== "Dart"
```dart title="binary_tree_dfs.dart"
/* 前序遍历 */
void preOrder(TreeNode? node) {
if (node == null) return;
// 访问优先级:根节点 -> 左子树 -> 右子树
list.add(node.val);
preOrder(node.left);
preOrder(node.right);
}
/* 中序遍历 */
void inOrder(TreeNode? node) {
if (node == null) return;
// 访问优先级:左子树 -> 根节点 -> 右子树
inOrder(node.left);
list.add(node.val);
inOrder(node.right);
}
/* 后序遍历 */
void postOrder(TreeNode? node) {
if (node == null) return;
// 访问优先级:左子树 -> 右子树 -> 根节点
postOrder(node.left);
postOrder(node.right);
list.add(node.val);
}
```
!!! note
我们也可以仅基于循环实现前、中、后序遍历,有兴趣的同学可以自行实现。