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树后,树是否为空:是
相关推荐
Jerry2 小时前
LeetCode 28. 找出字符串中第一个匹配项的下标
算法
Jerry4 小时前
LeetCode 459. 重复的子字符串
算法
海石7 小时前
1500分的题目,确实有实力,不过还是我略胜一筹
算法·leetcode
海石7 小时前
【记忆化搜索】条条大路通AC,走好适合你的那一条,走到后再考虑走得快
算法·leetcode
Jerry9 小时前
LeetCode 151. 反转字符串中的单词
算法
gugucoding9 小时前
31. 【C语言】堆栈与队列的实现
c语言·开发语言·数据结构·链表
ChaoZiLL11 小时前
我的数据结构3——链表(link list)
数据结构·链表
a11177611 小时前
LM 算法迭代过程动画演示(SLAM)
算法
头茬韭菜11 小时前
Context 的生死抉择:四层压缩、截断算法与 Session Memory
算法·ai
Jerry12 小时前
LeetCode 541. 反转字符串 II
算法