AVL树的讲解

一、AVL 树基础概念

1.1 为什么需要 AVL 树?

普通的二叉搜索树(BST)在理想情况下(完全平衡),查找、插入、删除的时间复杂度都是O(log n) 。但如果插入的数据是有序的(如 1,2,3,4,5),二叉搜索树会退化成链表 ,此时时间复杂度会恶化到O(n),这在数据量大时是不可接受的。

AVL 树 就是为了解决这个问题而诞生的。它是一种自平衡的二叉搜索树 ,通过在插入和删除操作后进行旋转调整,保证树的任意节点的左右子树高度差不超过 1,从而始终维持 O (log n) 的时间复杂度。

1.2 AVL 树的定义

AVL 树满足以下两个条件:

  1. 它首先是一棵二叉搜索树(左子树所有节点值 < 根节点值 < 右子树所有节点值)
  2. 任意节点的平衡因子(左子树高度 - 右子树高度)的绝对值不超过 1

1.3 核心术语

  • 节点高度:从该节点到其最远叶子节点的路径上的边数(空节点高度定义为 - 1,叶子节点高度为 0)
  • 平衡因子:左子树高度 - 右子树高度,取值范围为 {-1, 0, 1}
  • 失衡:当某个节点的平衡因子绝对值等于 2 时,该节点所在的子树失衡,需要进行旋转调整

二、AVL 树的四种失衡情况与旋转操作

AVL 树的失衡只有四种情况,对应四种旋转操作:

表格

失衡类型 平衡因子情况 旋转方式
LL 型 节点平衡因子 = 2,左孩子平衡因子≥0 单右旋
RR 型 节点平衡因子 =-2,右孩子平衡因子≤0 单左旋
LR 型 节点平衡因子 = 2,左孩子平衡因子 =-1 先左旋左孩子,再右旋当前节点
RL 型 节点平衡因子 =-2,右孩子平衡因子 = 1 先右旋右孩子,再左旋当前节点

三、完整代码实现与逐行讲解

3.1 头文件与结构体定义

cpp 复制代码
#include<stdio.h>
#include<iostream>
#include<cassert>
#include<string.h>
#include<stack>
using namespace std;

// AVL树节点结构体
typedef struct Avlnode
{
    int val;                // 节点存储的值
    int height;             // 节点的高度
    struct Avlnode* rightchild; // 右孩子指针
    struct Avlnode* leftchild;  // 左孩子指针
}Avlnode;

// AVL树结构体(封装根节点,方便操作)
typedef struct AvlTree
{
    struct Avlnode* root;   // 树的根节点
}AvlTree;

讲解

  • 每个节点除了存储值和左右孩子指针外,还额外存储了高度,避免每次计算平衡因子时都递归遍历子树
  • AvlTree结构体封装根节点,而不是直接用Avlnode*,这样对外接口更统一,也方便后续扩展

3.2 基础工具函数

3.2.1 购买节点
cpp 复制代码
// 1. 购买节点(创建新节点)
Avlnode* BuyNode(int val)
{
    // 动态分配内存
    Avlnode* node = (Avlnode*)malloc(sizeof(Avlnode));
    if (node == nullptr)
    {
        printf("内存分配失败!\n");
        return nullptr;
    }
    // 初始化节点属性
    node->height = 0;       // 新节点是叶子节点,高度为0
    node->leftchild = nullptr;
    node->rightchild = nullptr;
    node->val = val;
    return node;
}
3.2.2 获取节点高度
cpp 复制代码
// 2. 获取节点的高度
int Get_height(Avlnode* node)
{
    // 空节点高度定义为-1(这是AVL树的标准定义)
    if (node == nullptr)
    {
        return -1;
    }
    return node->height;
}
3.2.3 更新节点高度
cpp 复制代码
// 3. 更新当前节点的高度
int Updata_Height(Avlnode* node)
{
    if (node == nullptr)
    {
        return -1;
    }
    // 节点高度 = 左右子树高度的最大值 + 1
    int height_right = Get_height(node->rightchild);
    int height_left = Get_height(node->leftchild);
    
    node->height = (height_right > height_left ? height_right + 1 : height_left + 1);
    return node->height;
}

注意 :原代码中这里有个 bug------ 只计算了高度但没有赋值给node->height,导致后续平衡因子计算错误。

3.2.4 获取平衡因子
cpp 复制代码
// 4. 获取当前节点的平衡因子
int Get_Balance_Factor(Avlnode* node)
{
    if (node == nullptr)
    {
        return 0;
    }
    // 平衡因子 = 左子树高度 - 右子树高度
    return Get_height(node->leftchild) - Get_height(node->rightchild);
}

3.3 核心旋转操作

3.3.1 单左旋(处理 RR 型失衡)
cpp 复制代码
// 5. 单左旋(处理RR型失衡)
Avlnode* Left_Rotate(Avlnode* node)
{
    assert(node != nullptr);
    assert(node->rightchild != nullptr); // 左旋必须有右孩子
    
    Avlnode* child = node->rightchild;       // 右孩子成为新的根
    Avlnode* grandchild = child->leftchild;  // 右孩子的左孩子
    
    // 旋转操作
    node->rightchild = grandchild;  // 原根的右孩子指向孙子节点
    child->leftchild = node;        // 新根的左孩子指向原根
    
    // 更新高度(先更新原根,再更新新根)
    Updata_Height(node);
    Updata_Height(child);
    
    return child; // 返回新的根节点
}

左旋示意图

cpp 复制代码
    node                child
   /    \              /    \
  A     child   =>   node    C
       /    \       /    \
      B      C     A      B
3.3.2 单右旋(处理 LL 型失衡)
cpp 复制代码
// 6. 单右旋(处理LL型失衡)
Avlnode* right_Rotate(Avlnode* node)
{
    assert(node != nullptr);
    assert(node->leftchild != nullptr); // 右旋必须有左孩子
    
    Avlnode* child = node->leftchild;        // 左孩子成为新的根
    Avlnode* grandchild = child->rightchild; // 左孩子的右孩子
    
    // 旋转操作
    node->leftchild = grandchild;   // 原根的左孩子指向孙子节点
    child->rightchild = node;       // 新根的右孩子指向原根
    
    // 更新高度(先更新原根,再更新新根)
    Updata_Height(node);
    Updata_Height(child);
    
    return child; // 返回新的根节点
}

右旋示意图

cpp 复制代码
      node              child
     /    \            /    \
   child   C   =>    A     node
  /    \                  /    \
 A      B                B      C
3.3.3 通用旋转函数(自动判断旋转类型)
cpp 复制代码
// 7. 通用旋转函数(根据平衡因子自动判断旋转类型)
Avlnode* Rotate(Avlnode* node)
{
    assert(node != nullptr);
    int balance = Get_Balance_Factor(node);
    
    // 左子树更高(平衡因子=2)
    if (balance == 2)
    {
        if (Get_Balance_Factor(node->leftchild) >= 0)
        {
            // LL型:单右旋
            return right_Rotate(node);
        }
        else
        {
            // LR型:先左旋左孩子,再右旋当前节点
            node->leftchild = Left_Rotate(node->leftchild);
            return right_Rotate(node);
        }
    }
    // 右子树更高(平衡因子=-2)
    if (balance == -2)
    {
        if (Get_Balance_Factor(node->rightchild) <= 0)
        {
            // RR型:单左旋
            return Left_Rotate(node);
        }
        else
        {
            // RL型:先右旋右孩子,再左旋当前节点
            node->rightchild = right_Rotate(node->rightchild);
            return Left_Rotate(node);
        }
    }
    // 如果没有失衡,直接返回原节点
    return node;
}

注意:原代码中 RL 旋转有个严重 bug------ 错误地旋转了左孩子而不是右孩子,这会导致程序崩溃或树结构错误。

3.4 AVL 树初始化与销毁

cpp 复制代码
// 8. 初始化AVL树
void Init_AVLTree(AvlTree* pTree)
{
    assert(pTree != nullptr);
    pTree->root = nullptr;
}

// 9. 递归销毁节点
void Destory(Avlnode* root)
{
    if (root == NULL)
        return;
    // 后序遍历销毁(先销毁左右子树,再销毁根节点)
    Destory(root->leftchild);
    Destory(root->rightchild);
    free(root);
}

// 10. 销毁整个AVL树
void Destory_AVLTree(AvlTree* pTree)
{
    assert(pTree != nullptr);
    Destory(pTree->root);
    pTree->root = nullptr; // 根节点置空,防止野指针
}

3.5 插入操作

cpp 复制代码
// 11. 插入帮助函数(递归实现)
Avlnode* Insert_Helper(Avlnode* node, int val)
{
    // 1. 找到插入位置,创建新节点
    if (node == nullptr)
    {
        return BuyNode(val);
    }
    
    // 2. 二叉搜索树的插入逻辑
    if (val < node->val)
    {
        node->leftchild = Insert_Helper(node->leftchild, val);
    }
    else if (val > node->val)
    {
        node->rightchild = Insert_Helper(node->rightchild, val);
    }
    else
    {
        // 3. 值已存在,不插入
        printf("值%d已存在,无需插入!\n", val);
        return node;
    }
    
    // 4. 回溯时更新当前节点的高度
    Updata_Height(node);
    
    // 5. 检查并修复失衡
    return Rotate(node);
}

// 12. 插入节点(对外接口)
bool Insert(AvlTree* pTree, int val)
{
    assert(pTree != nullptr);
    pTree->root = Insert_Helper(pTree->root, val);
    return true;
}

插入操作流程

  1. 按照二叉搜索树的规则找到插入位置
  2. 创建新节点并插入
  3. 从插入位置向上回溯,更新每个节点的高度
  4. 检查每个节点是否失衡,如果失衡则进行相应的旋转调整

3.6 删除操作

cpp 复制代码
// 13. 删除帮助函数(递归实现)
Avlnode* Delete_help(Avlnode* node, int val)
{
    // 1. 节点不存在,返回空
    if (node == nullptr)
    {
        printf("值%d不存在,无法删除!\n", val);
        return nullptr;
    }
    
    // 2. 二叉搜索树的删除逻辑
    if (val < node->val)
    {
        node->leftchild = Delete_help(node->leftchild, val);
    }
    else if (val > node->val)
    {
        node->rightchild = Delete_help(node->rightchild, val);
    }
    else // 3. 找到要删除的节点
    {
        // 情况1:节点有两个孩子
        if (node->leftchild != nullptr && node->rightchild != nullptr)
        {
            // 找到右子树的最小节点(中序后继)
            Avlnode* successor = node->rightchild;
            while (successor->leftchild != nullptr)
            {
                successor = successor->leftchild;
            }
            // 用后继节点的值替换当前节点的值
            node->val = successor->val;
            // 删除后继节点
            node->rightchild = Delete_help(node->rightchild, successor->val);
        }
        // 情况2:节点有0个或1个孩子
        else
        {
            Avlnode* child = node->leftchild != nullptr ? node->leftchild : node->rightchild;
            free(node); // 释放当前节点内存
            return child; // 返回孩子节点,让父节点指向它
        }
    }
    
    // 4. 回溯时更新当前节点的高度
    Updata_Height(node);
    
    // 5. 检查并修复失衡
    return Rotate(node);
}

// 14. 删除节点(对外接口)
bool Delete(AvlTree* pTree, int val)
{
    assert(pTree != nullptr);
    pTree->root = Delete_help(pTree->root, val);
    return true;
}

删除操作注意点

  • 删除有两个孩子的节点时,我们选择用 ** 右子树的最小节点(中序后继)** 来替换它,这样可以保持二叉搜索树的性质
  • 删除操作比插入操作更复杂,因为删除可能导致祖先节点失衡,需要从删除位置向上回溯检查所有节点

3.7 查找、判空与遍历

cpp 复制代码
// 15. 判断AVL树是否为空
bool IsEmpty(AvlTree* pTree)
{
    assert(pTree != nullptr);
    return pTree->root == nullptr;
}

// 16. 查找节点
Avlnode* Search_AVLTree(AvlTree* pTree, int val)
{
    if (IsEmpty(pTree))
        return nullptr;
    
    Avlnode* p = pTree->root;
    while (p != nullptr)
    {
        if (p->val == val)
        {
            return p; // 找到节点
        }
        else if (val > p->val)
        {
            p = p->rightchild; // 去右子树查找
        }
        else
        {
            p = p->leftchild; // 去左子树查找
        }
    }
    return nullptr; // 未找到
}

// 17. 中序遍历打印AVL树(非递归实现)
void Print_AvlTree(AvlTree* pTree)
{
    if (IsEmpty(pTree))
    {
        printf("AVL树为空!\n");
        return;
    }
    
    stack<Avlnode*> s;
    Avlnode* cur = pTree->root;
    
    while (cur != nullptr || !s.empty())
    {
        // 一直向左走,将所有左节点入栈
        while (cur != nullptr)
        {
            s.push(cur);
            cur = cur->leftchild;
        }
        
        // 弹出栈顶节点并访问
        cur = s.top();
        s.pop();
        cout << cur->val << " ";
        
        // 处理右子树
        cur = cur->rightchild;
    }
    cout << endl;
}

注意:原代码中的中序遍历实现有严重 bug,会导致死循环和遍历不完整。这里重写了标准的非递归中序遍历实现。

四、测试代码与运行结果

cpp 复制代码
int main()
{
    AvlTree tree;
    Init_AVLTree(&tree);
    
    // 测试插入操作(插入有序序列,验证AVL树的自平衡能力)
    printf("插入10,20,30,40,50后,中序遍历结果:\n");
    Insert(&tree, 10);
    Insert(&tree, 20);
    Insert(&tree, 30);
    Insert(&tree, 40);
    Insert(&tree, 50);
    Print_AvlTree(&tree); // 预期输出:10 20 30 40 50
    
    // 测试删除操作
    printf("\n删除40后,中序遍历结果:\n");
    Delete(&tree, 40);
    Print_AvlTree(&tree); // 预期输出:10 20 30 50
    
    printf("\n删除20后,中序遍历结果:\n");
    Delete(&tree, 20);
    Print_AvlTree(&tree); // 预期输出:10 30 50
    
    // 测试查找操作
    Avlnode* node = Search_AVLTree(&tree, 30);
    if (node != nullptr)
    {
        printf("\n找到节点30,其高度为:%d\n", node->height);
    }
    else
    {
        printf("\n未找到节点30\n");
    }
    
    // 测试销毁操作
    Destory_AVLTree(&tree);
    printf("\n销毁AVL树后,树是否为空:%s\n", IsEmpty(&tree) ? "是" : "否");
    
    return 0;
}

运行结果

复制代码
插入10,20,30,40,50后,中序遍历结果:
10 20 30 40 50 

删除40后,中序遍历结果:
10 20 30 50 

删除20后,中序遍历结果:
10 30 50 

找到节点30,其高度为:1

销毁AVL树后,树是否为空:是
相关推荐
Trouvaille ~1 小时前
【Redis篇】Hash 哈希:字段级操作与对象存储的最佳实践
数据库·redis·后端·算法·缓存·哈希算法·键值对
辞忧九千七1 小时前
吃透Redis7核心数据结构:从基础用法到实战场景(Python版)
开发语言·数据结构·redis·python
悠仁さん1 小时前
数据结构 树 二叉树 堆 (链式二叉树模拟实现篇)
数据结构·算法
z200509302 小时前
今日算法(带回文问题的回溯)
算法·leetcode·回溯
洛水水2 小时前
【力扣100题】55.编辑距离
算法·leetcode·动态规划
better_liang2 小时前
每日Java面试场景题知识点之-MySQL底层数据结构B+树
java·数据结构·mysql·性能优化·面试题·b+树·数据库索引
洛水水2 小时前
【力扣100题】62.滑动窗口最大值
数据结构·算法·leetcode
IronMurphy2 小时前
算法五十一 64. 最小路径和
算法
醒醒该学习了!2 小时前
Prompt提示词——带有深度思考模型的提示方法(理论篇)
人工智能·算法·prompt