This commit is contained in:
krahets
2023-06-25 21:11:35 +08:00
parent 5bc8df6d5d
commit e4e6cd6bae
19 changed files with 836 additions and 105 deletions

View File

@@ -1478,7 +1478,7 @@ AVL 树的特点在于「旋转 Rotation」操作它能够在不影响二叉
t.root = t.insertHelper(t.root, val)
}
/* 递归插入节点(辅助方法 */
/* 递归插入节点(辅助函数 */
func (t *aVLTree) insertHelper(node *TreeNode, val int) *TreeNode {
if node == nil {
return NewTreeNode(val)
@@ -1560,7 +1560,7 @@ AVL 树的特点在于「旋转 Rotation」操作它能够在不影响二叉
tree->root = insertHelper(tree->root, val);
}
/* 递归插入节点(辅助方法 */
/* 递归插入节点(辅助函数 */
TreeNode *insertHelper(TreeNode *node, int val) {
if (node == NULL) {
return newTreeNode(val);
@@ -1644,8 +1644,8 @@ AVL 树的特点在于「旋转 Rotation」操作它能够在不影响二叉
```zig title="avl_tree.zig"
// 插入节点
fn insert(self: *Self, val: T) void {
self.root = try self.insertHelper(self.root, val);
fn insert(self: *Self, val: T) !void {
self.root = (try self.insertHelper(self.root, val)).?;
}
// 递归插入节点(辅助方法)
@@ -1841,7 +1841,7 @@ AVL 树的特点在于「旋转 Rotation」操作它能够在不影响二叉
t.root = t.removeHelper(t.root, val)
}
/* 递归删除节点(辅助方法 */
/* 递归删除节点(辅助函数 */
func (t *aVLTree) removeHelper(node *TreeNode, val int) *TreeNode {
if node == nil {
return nil
@@ -1976,7 +1976,7 @@ AVL 树的特点在于「旋转 Rotation」操作它能够在不影响二叉
TreeNode *root = removeHelper(tree->root, val);
}
/* 递归删除节点(辅助方法 */
/* 递归删除节点(辅助函数 */
TreeNode *removeHelper(TreeNode *node, int val) {
TreeNode *child, *grandChild;
if (node == NULL) {
@@ -2117,7 +2117,7 @@ AVL 树的特点在于「旋转 Rotation」操作它能够在不影响二叉
```zig title="avl_tree.zig"
// 删除节点
fn remove(self: *Self, val: T) void {
self.root = self.removeHelper(self.root, val);
self.root = self.removeHelper(self.root, val).?;
}
// 递归删除节点(辅助方法)

View File

@@ -622,23 +622,22 @@ comments: true
与插入节点类似,我们需要在删除操作后维持二叉搜索树的“左子树 < 根节点 < 右子树”的性质。首先,我们需要在二叉树中执行查找操作,获取待删除节点。接下来,根据待删除节点的子节点数量,删除操作需分为三种情况:
当待删除节点的子节点数量 $= 0$ 时,表示待删除节点是叶节点,可以直接删除。
当待删除节点的度为 $0$ 时,表示待删除节点是叶节点,可以直接删除。
![在二叉搜索树中删除节点(度为 0](binary_search_tree.assets/bst_remove_case1.png)
<p align="center"> Fig. 在二叉搜索树中删除节点(度为 0 </p>
当待删除节点的子节点数量 $= 1$ 时,将待删除节点替换为其子节点即可。
当待删除节点的度为 $1$ 时,将待删除节点替换为其子节点即可。
![在二叉搜索树中删除节点(度为 1](binary_search_tree.assets/bst_remove_case2.png)
<p align="center"> Fig. 在二叉搜索树中删除节点(度为 1 </p>
当待删除节点的子节点数量 $= 2$ 时,删除操作分为三步
当待删除节点的度为 $2$ 时,我们无法直接删除它,而需要使用一个节点替换该节点。由于要保持二叉搜索树“左 $<$ 根 $<$ 右”的性质,因此这个节点可以是右子树的最小节点或左子树的最大节点。假设我们选择右子树的最小节点(或者称为中序遍历的下个节点),则删除操作为
1. 找到待删除节点在“中序遍历序列”中的下一个节点,记为 `tmp`
2. 在树中递归删除节点 `tmp`
3. 用 `tmp` 的值覆盖待删除节点的值;
2. 将 `tmp` 的值覆盖待删除节点的值,并在树中递归删除节点 `tmp`
=== "<1>"
![二叉搜索树删除节点示例](binary_search_tree.assets/bst_remove_case3_step1.png)
@@ -1167,7 +1166,7 @@ comments: true
```zig title="binary_search_tree.zig"
// 删除节点
fn remove(self: *Self, num: T) !void {
fn remove(self: *Self, num: T) void {
// 若树为空,直接提前返回
if (self.root == null) return;
var cur = self.root;
@@ -1204,11 +1203,11 @@ comments: true
while (tmp.?.left != null) {
tmp = tmp.?.left;
}
var tmpVal = tmp.?.val;
var tmp_val = tmp.?.val;
// 递归删除节点 tmp
_ = self.remove(tmp.?.val);
self.remove(tmp.?.val);
// 用 tmp 覆盖 cur
cur.?.val = tmpVal;
cur.?.val = tmp_val;
}
}
```

View File

@@ -15,3 +15,25 @@ comments: true
- 二叉搜索树是一种高效的元素查找数据结构,其查找、插入和删除操作的时间复杂度均为 $O(\log n)$ 。当二叉搜索树退化为链表时,各项时间复杂度会劣化至 $O(n)$ 。
- AVL 树,也称为平衡二叉搜索树,它通过旋转操作,确保在不断插入和删除节点后,树仍然保持平衡。
- AVL 树的旋转操作包括右旋、左旋、先右旋再左旋、先左旋再右旋。在插入或删除节点后AVL 树会从底向顶执行旋转操作,使树重新恢复平衡。
## 7.6.1. &nbsp; Q & A
!!! question "对于只有一个节点的二叉树,树的高度和根节点的深度都是 $0$ 吗?"
是的,因为高度和深度通常定义为“走过边的数量”。
!!! question "二叉树中的插入与删除一般都是由一套操作配合完成的,这里的“一套操作”指什么呢?可以理解为资源的子节点的资源释放吗?"
拿二叉搜索树来举例,删除节点操作要分为三种情况处理,其中每种情况都需要进行多个步骤的节点操作。
!!! question "为什么 DFS 遍历二叉树有前、中、后三种顺序,分别有什么用呢?"
DFS 的前、中、后序遍历和访问数组的顺序类似,是遍历二叉树的基本方法,利用这三种遍历方法,我们可以得到一个特定顺序的遍历结果。例如在二叉搜索树中,由于结点大小满足 `左子结点值 < 根结点值 < 右子结点值` ,因此我们只要按照 `左->根->右` 的优先级遍历树,就可以获得有序的节点序列。
!!! question "右旋操作是处理失衡节点 `node` , `child` , `grand_child` 之间的关系,那 `node` 的父节点和 `node` 原来的连接不需要维护吗?右旋操作后岂不是断掉了?"
我们需要从递归的视角来看这个问题。右旋操作 `right_rotate(root)` 传入的是子树的根节点,最终 `return child` 返回旋转之后的子树的根节点。子树的根节点和其父节点的连接是在该函数返回后完成的,不属于右旋操作的维护范围。
!!! question "在 C++ 中,函数被划分到 `private``public` 中,这方面有什么考量吗?为什么要将 `height()` 函数和 `updateHeight()` 函数分别放在 `public``private` 中呢?"
主要看方法的使用范围,如果方法只在类内部使用,那么就设计为 `private` 。例如,用户单独调用 `updateHeight()` 是没有意义的,它只是插入、删除操作中的一步。而 `height()` 是访问结点高度,类似于 `vector.size()` ,因此设置成 `public` 以便使用。