文章目录
- [1、 AVL树的定义](#1、 AVL树的定义)
- [2、 AVL树节点](#2、 AVL树节点)
- [3、 AVL树的插入:插入+平衡调整](#3、 AVL树的插入:插入+平衡调整)
-
- (1)插入
- (2)旋转操作修复平衡
-
- [1. 右单旋:新节点插入较高左子树的左侧](#1. 右单旋:新节点插入较高左子树的左侧)
- [2. 左单旋:新节点插入较高右子树的右侧](#2. 左单旋:新节点插入较高右子树的右侧)
- [3. 左右双旋:新节点插入较高左子树的右侧](#3. 左右双旋:新节点插入较高左子树的右侧)
- [4. 右左双旋:新节点插入较高右子树的左侧](#4. 右左双旋:新节点插入较高右子树的左侧)
- [4、 AVL树的验证:确保结构正确](#4、 AVL树的验证:确保结构正确)
- [5、 AVL树的删除(了解)](#5、 AVL树的删除(了解))
- [6、 AVL树的性能分析](#6、 AVL树的性能分析)
- [7. 二叉搜索树与AVL树的对比](#7. 二叉搜索树与AVL树的对比)
1、 AVL树的定义
AVL树本质上是一棵"高度平衡"的二叉搜索树,定义如下:
- 要么是空树;
- 左右子树都是AVL树;
- 左右子树高度之差(平衡因子)的绝对值不超过1,平衡因子的取值只能是-1、0、1。
平衡因子定义为:平衡因子 = 右子树高度 - 左子树高度 。通过这个约束,AVL树确保了树的高度始终保持在 O(log2 N) 级别,因此查找、插入、删除的时间复杂度都稳定在O(log2 N),解决了二叉搜索树退化为单支树的问题。
2、 AVL树节点
为了方便维护平衡因子,AVL树的节点在二叉搜索树节点的基础上,增加了平衡因子属性,同时保留了双亲指针.
java
class TreeNode {
public TreeNode left = null;
public TreeNode right = null;
public TreeNode parent = null;
public int val = 0;
public int bf = 0; // 平衡因子:右子树高度 - 左子树高度
public TreeNode(int val) {
this.val = val;
}
}
平衡因子只是维护AVL树平衡的一种实现方式,并非必须------也可以通过直接计算左右子树高度来判断平衡状态,但平衡因子能让判断更高效。
3、 AVL树的插入:插入+平衡调整
AVL树的插入过程分为两步:按二叉搜索树规则插入新节点 , 调整平衡因子并修复不平衡。
(1)插入
- 按二叉搜索树规则插入新节点,新节点作为叶子节点插入;
- 向上更新平衡因子:新节点插入后,其双亲节点的平衡因子一定会发生变化,需要从新节点的双亲开始,向上逐层更新平衡因子:
- 若新节点插入到双亲的左子树,双亲的平衡因子减1;
- 若新节点插入到双亲的右子树,双亲的平衡因子加1;
- 判断平衡状态:更新后,双亲节点的平衡因子会出现三种情况,对应不同的处理方式:
- 平衡因子为0:说明插入前双亲的平衡因子为±1,插入后被调整为0,树的高度未变化,无需继续向上更新,插入成功;
- 平衡因子为±1:说明插入前双亲的平衡因子为0,插入后高度增加1,需要继续向上更新其祖父节点的平衡因子;
- 平衡因子为±2:说明当前子树已经不平衡,需要通过旋转操作修复平衡,修复后无需继续向上更新(旋转后子树高度与插入前一致)。
(2)旋转操作修复平衡
当节点的平衡因子为±2时,必须通过旋转操作让子树重新平衡。根据新节点插入位置的不同,旋转分为四种情况,核心原则是:通过旋转降低较高子树的高度,提升较低子树的高度,同时保持二叉搜索树的性质。
1. 右单旋:新节点插入较高左子树的左侧
适用场景:当前不平衡节点(设为parent)的平衡因子为-2(左子树更高),且其左子树(设为subL)的平衡因子为-1(新节点插入subL的左侧)。
旋转逻辑:
- 将subL的右子树(subLR)作为parent的左子树(若subLR不为空,更新其双亲为parent);
- 将parent作为subL的右子树;
- 更新parent和subL的双亲指针(若parent是根节点,更新根为subL;否则将subL挂载到parent原双亲的对应位置);
- 旋转后,parent和subL的平衡因子均重置为0。
代码实现:
java
private void rotateRight(AVLTreeNode parent) {
AVLTreeNode subL = parent.left;
AVLTreeNode subLR = subL.right;
// 1. 处理subLR
parent.left = subLR;
if (subLR != null) {
subLR.parent = parent;
}
// 2. 处理subL和parent的关系
subL.right = parent;
AVLTreeNode parParent = parent.parent; // 记录parent的原双亲
parent.parent = subL;
// 3. 处理subL与原双亲的关系
if (parent == root) { // parent是根节点
root = subL;
root.parent = null;
} else { // parent是子树
if (parParent.left == parent) {
parParent.left = subL;
} else {
parParent.right = subL;
}
subL.parent = parParent;
}
// 4. 更新平衡因子
subL.bf = 0;
parent.bf = 0;
}
2. 左单旋:新节点插入较高右子树的右侧
适用场景:当前不平衡节点(parent)的平衡因子为2(右子树更高),且其右子树(subR)的平衡因子为1(新节点插入subR的右侧)。
旋转逻辑与右单旋对称:
- 将subR的左子树(subRL)作为parent的右子树;
- 将parent作为subR的左子树;
- 更新双亲指针;
- 平衡因子重置为0。
代码实现:
java
private void rotateLeft(AVLTreeNode parent) {
AVLTreeNode subR = parent.right;
AVLTreeNode subRL = subR.left;
// 1. 处理subRL
parent.right = subRL;
if (subRL != null) {
subRL.parent = parent;
}
// 2. 处理subR和parent的关系
subR.left = parent;
AVLTreeNode parParent = parent.parent;
parent.parent = subR;
// 3. 处理subR与原双亲的关系
if (parent == root) {
root = subR;
root.parent = null;
} else {
if (parParent.left == parent) {
parParent.left = subR;
} else {
parParent.right = subR;
}
subR.parent = parParent;
}
// 4. 更新平衡因子
subR.bf = 0;
parent.bf = 0;
}
3. 左右双旋:新节点插入较高左子树的右侧
适用场景:当前不平衡节点(parent)的平衡因子为-2(左子树更高),且其左子树(subL)的平衡因子为1(新节点插入subL的右侧)。
旋转逻辑:双旋本质是将复杂情况转化为单旋处理------先对subL进行左单旋,再对parent进行右单旋。
具体步骤:
- 对parent的左子树subL执行左单旋,此时新节点的插入位置转化为"较高左子树的左侧";
- 对parent执行右单旋,完成平衡修复;
- 根据原中间节点(subLR)的平衡因子,调整相关节点的平衡因子:
- 若subLR的平衡因子为-1:旋转后subL的平衡因子为1,parent的平衡因子为0;
- 若subLR的平衡因子为1:旋转后subL的平衡因子为0,parent的平衡因子为-1;
- 若subLR的平衡因子为0:旋转后subL和parent的平衡因子均为0。
代码实现:
java
private void rotateLR(AVLTreeNode parent) {
AVLTreeNode subL = parent.left;
AVLTreeNode subLR = subL.right;
int bf = subLR.bf; // 记录subLR的平衡因子,用于后续调整
// 第一步:对subL左单旋
rotateLeft(subL);
// 第二步:对parent右单旋
rotateRight(parent);
// 调整平衡因子
if (bf == -1) {
subL.bf = 1;
parent.bf = 0;
subLR.bf = 0;
} else if (bf == 1) {
subL.bf = 0;
parent.bf = -1;
subLR.bf = 0;
} else { // bf == 0
subL.bf = 0;
parent.bf = 0;
subLR.bf = 0;
}
}
4. 右左双旋:新节点插入较高右子树的左侧
适用场景:当前不平衡节点(parent)的平衡因子为2(右子树更高),且其右子树(subR)的平衡因子为-1(新节点插入subR的左侧)。
旋转逻辑与左右双旋对称:先对subR执行右单旋,再对parent执行左单旋,最后调整平衡因子。
总结旋转规律:
- 当不平衡节点的平衡因子与较高子树的平衡因子同号时(如parent.bf=-2且subL.bf=-1),执行单旋;
- 当不平衡节点的平衡因子与较高子树的平衡因子异号时(如parent.bf=-2且subL.bf=1),执行双旋。
4、 AVL树的验证:确保结构正确
AVL树的验证需要分两步,既要保证是二叉搜索树,也要保证是平衡树:
(1)验证二叉搜索树特性
通过中序遍历验证,若遍历结果为有序序列,则满足二叉搜索树的性质:
java
public void inorder(AVLTreeNode root) {
if (root == null) return;
inorder(root.left);
System.out.print(root.val + " ");
inorder(root.right);
}
(2)验证平衡特性
需要检查两个点:①每个节点的平衡因子是否等于"右子树高度 - 左子树高度";②每个节点的左右子树高度差的绝对值是否不超过1。
代码实现:
java
private boolean isBalance(AVLTreeNode root) {
if (root == null) return true;
// 计算左右子树高度
int leftH = getHeight(root.left);
int rightH = getHeight(root.right);
// 验证平衡因子是否正确
if (rightH - leftH != root.bf) {
System.out.println("节点" + root.val + "平衡因子异常!");
return false;
}
// 验证左右子树高度差不超过1,递归验证子树
return Math.abs(leftH - rightH) <= 1
&& isBalance(root.left)
&& isBalance(root.right);
}
// 辅助方法:计算树的高度
private int getHeight(AVLTreeNode node) {
if (node == null) return 0;
int leftH = getHeight(node.left);
int rightH = getHeight(node.right);
return Math.max(leftH, rightH) + 1;
}
5、 AVL树的删除(了解)
AVL树的删除流程与二叉搜索树一致,但删除后需要更新平衡因子,若出现不平衡则进行旋转调整。与插入不同的是,删除操作可能导致平衡问题一直向上传播(最差情况下需要调整到根节点),因此实现更为复杂。
大致步骤:
- 按二叉搜索树规则删除目标节点;
- 从删除节点的双亲开始,向上更新平衡因子;
- 若某节点的平衡因子变为±2,执行相应的旋转操作;
- 继续向上更新,直到根节点或平衡因子恢复为0。
6、 AVL树的性能分析
AVL树的优势在于绝对平衡,确保了查找操作的时间复杂度稳定在 (O(log_2 N)),适合静态数据(数据量不频繁变化)的场景。但它的缺点也很明显:
- 插入和删除操作需要频繁调整平衡因子和旋转,尤其是删除操作,可能需要多次旋转才能恢复平衡,性能开销较大;
- 维护平衡因子需要额外的空间开销。
因此,AVL树更适合"查询多、修改少"的场景,若数据需要频繁插入和删除,红黑树(一种"近似平衡"的二叉搜索树)会是更优选择。
7. 二叉搜索树与AVL树的对比
| 特性 | 二叉搜索树 | AVL树 |
|---|---|---|
| 结构约束 | 无平衡约束 | 左右子树高度差≤1(绝对平衡) |
| 时间复杂度(查找) | 最优(O(log_2 N)),最差(O(N)) | 稳定(O(log_2 N)) |
| 时间复杂度(插入/删除) | 最优(O(log_2 N)),最差(O(N)) | 稳定(O(log_2 N))(需旋转调整) |
| 额外开销 | 无 | 维护平衡因子,插入/删除需旋转 |
| 适用场景 | 数据无序插入、修改少 | 数据需频繁查询、静态或修改少 |