【数据结构】AVL树解析

文章目录

  • [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. 按二叉搜索树规则插入新节点,新节点作为叶子节点插入;
  2. 向上更新平衡因子:新节点插入后,其双亲节点的平衡因子一定会发生变化,需要从新节点的双亲开始,向上逐层更新平衡因子:
    • 若新节点插入到双亲的左子树,双亲的平衡因子减1;
    • 若新节点插入到双亲的右子树,双亲的平衡因子加1;
  3. 判断平衡状态:更新后,双亲节点的平衡因子会出现三种情况,对应不同的处理方式:
    • 平衡因子为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进行右单旋。

具体步骤:

  1. 对parent的左子树subL执行左单旋,此时新节点的插入位置转化为"较高左子树的左侧";
  2. 对parent执行右单旋,完成平衡修复;
  3. 根据原中间节点(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树的删除流程与二叉搜索树一致,但删除后需要更新平衡因子,若出现不平衡则进行旋转调整。与插入不同的是,删除操作可能导致平衡问题一直向上传播(最差情况下需要调整到根节点),因此实现更为复杂。

大致步骤:

  1. 按二叉搜索树规则删除目标节点;
  2. 从删除节点的双亲开始,向上更新平衡因子;
  3. 若某节点的平衡因子变为±2,执行相应的旋转操作;
  4. 继续向上更新,直到根节点或平衡因子恢复为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))(需旋转调整)
额外开销 维护平衡因子,插入/删除需旋转
适用场景 数据无序插入、修改少 数据需频繁查询、静态或修改少
相关推荐
Lazionr2 小时前
数据结构入门:栈实现全解析
c语言·数据结构
小π军2 小时前
STL之multiset 常见API介绍
数据结构·c++·算法
Shan12052 小时前
浅谈:从经典算法到实战优化的案例分析
数据结构
研究点啥好呢2 小时前
Momenta算法工程师面试题精选:10道高频考题+答案解析
人工智能·算法·求职招聘·面试笔试
Resistance丶未来2 小时前
DeepSeek-V4 新手快速上手指南
数据结构·python·gpt·算法·机器学习·claude·claude 4.6
无限进步_2 小时前
【C++】寻找数组中出现次数超过一半的数字:三种解法深度剖析
开发语言·c++·git·算法·leetcode·github·visual studio
Lazionr2 小时前
数据结构队列详解:从概念到代码实现
c语言·数据结构
comli_cn2 小时前
HMM算法
线性代数·算法
Via_Neo2 小时前
Nim Game
算法