目录
搭建“创世”的舞台
注入序列,观察演化
注入 10
注入 20
注入 30
注入 40
注入 50
注入 25
再次审视
上一讲,我们已经从最根本的逻辑出发,推导出了 AVL 树失衡时所必需的修复操作——旋转 (Rotation)。
现在,我们将进入一个非常实践性的环节:如何从无到有地生成 (Generating) 一棵 AVL 树?
这个问题的本质,其实是一个思想实验:
如果我们拥有一台“AVL 插入机”(也就是我们上一讲实现的
insert
函数),我们把一堆数字一个一个地扔给它,这台机器内部会发生什么?最终会得到一个什么样的产品?
搭建“创世”的舞台
在开始之前,我们需要三样东西:
1. 一个起点 (The Starting Point): 一个空的世界,也就是一棵空树。在 C/C++ 代码中,我们用一个 NULL
指针来表示。
// 我们的宇宙之初,只有一个指向虚空的根指针
AVLTreeNode* root = NULL;
2. 一套基本法则 (The Rules): 这就是我们上一讲呕心沥血推导出的 insert
函数。它内部封装了 BST 的插入逻辑、高度 (Height) 的更新、平衡因子 (Balance Factor, BF) 的检查,以及四种旋转 (Rotation) 修复机制。
3. 一串生命序列 (The Sequence): 我们将要注入的数字。为了充分展示树的演化过程,我们需要一个能触发各种情况的序列。我们选用这个经典的序列:[10, 20, 30, 40, 50, 25]
。
我们的整个“生成”过程,在代码层面其实非常简洁,就是一个循环,不断地调用我们的基本法则 insert
函数。
数据结构:利用旋转在AVL树中维持平衡(Inserting in AVL with Rotation)-CSDN博客
// 引入头文件,以便我们可以打印输出观察每一步
#include <iostream>// ... 此处省略我们之前已经定义的 AVLTreeNode 结构体、
// createNode, getHeight, max, leftRotate, rightRotate, insert 等函数 ...// 主程序,也就是我们的“创世”过程
int main() {AVLTreeNode* root = NULL; // 1. 起点:空树int keys[] = {10, 20, 30, 40, 50, 25}; // 3. 生命序列int n = sizeof(keys) / sizeof(keys[0]);std::cout << "Starting to generate the AVL tree..." << std::endl;std::cout << "------------------------------------" << std::endl;for (int i = 0; i < n; ++i) {std::cout << "\n>>> Inserting " << keys[i] << "..." << std::endl;// 2. 反复调用我们的基本法则root = insert(root, keys[i]);// 为了便于观察,我们可以在这里加上一个打印树结构的函数(此处省略其实现)// printTree(root); std::cout << ">>> ... " << keys[i] << " inserted. Tree is now balanced." << std::endl;}std::cout << "\n------------------------------------" << std::endl;std::cout << "Generation complete." << std::endl;return 0;
}
代码框架已经搭好,现在,让我们进入核心环节,手动模拟上面这段代码的执行过程,观察树的每一步演化。
注入序列,观察演化
注入 10
-
演化过程: 树为空,
insert
函数直接创建一个新节点作为根。 -
最终形态:树诞生了,它是平衡的。
(10) {h=0, BF=0} // h 是 Height(高度), BF 是 Balance Factor(平衡因子)
注入 20
-
演化过程:
-
根据 BST 法则,
20 > 10
,插入到10
的右侧。 -
插入后回溯,更新路径上的节点。节点
10
的高度h
更新为 1,其平衡因子BF
变为height(left) - height(right)
=-1 - 0
=-1
。 -
|BF| <= 1
,树依然平衡。
-
-
最终形态:
(10) {h=1, BF=-1}\(20) {h=0, BF=0}
注入 30
-
演化过程:
-
根据 BST 法则,
30 > 10
(向右) ->30 > 20
(向右),插入到20
的右侧。 -
回溯更新:
-
20
节点:h
更新为 1,BF
更新为-1
。平衡。 -
10
节点:h
更新为 2,BF
更新为height(left) - height(right)
=-1 - 1
=-2
。
-
-
警报! 节点
10
的BF
绝对值变为2
,树失衡 (Unbalanced)!
-
-
诊断与修复:
-
失衡节点
A
是10
(BF=-2
)。 -
失衡是由于在
A
的右 (Right)子树 (B=20
) 的右 (Right)侧插入新节点导致的。 -
这是典型的 RR (右-右) 失衡。
-
根据我们的法则,需要对失衡节点
10
执行一次 左旋 (Left Rotation)。
-
-
修复后形态:
(20) {h=1, BF=0}/ \
(10) (30) {h=0, BF=0}
注入 40
-
演化过程:
40
根据 BST 法则插入为30
的右孩子。回溯检查,所有节点的BF
均在[-1, 1]
区间内。 -
最终形态: 无需旋转。
(20) {h=2, BF=-1}/ \
(10) (30) {h=1, BF=-1}\(40) {h=0, BF=0}
注入 50
-
演化过程:
-
50
插入为40
的右孩子。 -
回溯更新,当检查到
30
节点时,其h
变为 2,BF
变为-2
。失衡!
-
-
诊断与修复:
-
失衡节点
A
是30
(BF=-2
)。 -
失衡路径是 右-右 (RR)。
-
修复操作:对
30
执行 左旋 (Left Rotation)。40
会取代30
的位置,成为20
的右孩子。
-
-
修复后形态:
(20) {h=2, BF=-1}/ \(10) (40) {h=1, BF=0}/ \(30) (50)
系统再次自我修复,恢复平衡。
注入 25
-
演化过程:
-
根据 BST 法则:
25>20
(右) ->25<40
(左) ->25<30
(左)。25
插入为30
的左孩子。 -
回溯更新:
-
30
节点:h
变为 1,BF
变为1
。平衡。 -
40
节点:h
变为 2,BF
变为1
。平衡。 -
20
节点:h
变为 3,BF
变为height(left) - height(right)
=0 - 2
=-2
。 失衡!
-
-
-
诊断与修复:
-
失衡节点
A
是20
(BF=-2
)。 -
失衡发生在
A
的右 (Right)子树 (根为40
)。 -
新节点
25
是插入在该子树的左 (Left)侧。 -
这是一个 RL (右-左) 的“拐角”型失衡。
-
根据法则,需要两步操作:
-
“拉直”拐角: 对
A
的孩子节点40
,执行一次右旋 (Right Rotation)。 -
整体修复: 对
A
节点20
,执行一次左旋 (Left Rotation)。
-
-
-
修复后最终形态:
(30)/ \(20) (40)/ \ \
(10) (25) (50)
经历了一次复杂的重构后,系统演化出了一个极为稳定和平衡的最终形态。
再次审视
我们完成了整个生成过程。回过头看,我们得到了什么结论?
-
自组织性 (Self-Organization): 我们从未规划过最终那棵树长什么样。我们只定义了最基本的、局部的交互规则(插入和旋转)。一个高度平衡的、优美的全局结构,是自发涌现出来的。
-
动态平衡 (Dynamic Equilibrium): AVL 树不是静止的。每次有新的元素加入,都会暂时打破它的平衡,然后系统会通过一系列的调整,迅速回归到一个新的平衡状态。这是一种动态的、有弹性的稳定。
-
局部决定全局 (Local Actions, Global Consequences): 每次修复操作(旋转)都只涉及两到三个节点。然而,就是这样微小的局部调整,确保了整棵树的全局属性——对数级别的高度,从而保证了 O(logn) 的操作效率。
这就是从第一性原理理解“生成 AVL 树”——它不是一个按图索骥的建造过程,而是一个遵循简单规则、不断演化的创生过程。