【数据结构】AVL树详解:从原理到C++实现

前言

🔥个人主页:不会c嘎嘎

📚专栏传送门:【数据结构】【C++】【Linux】【算法】【MySQL】

🐶学习方向:C++方向学习爱好者

⭐人生格言:谨言慎行,戒骄戒躁

每日一鸡汤:

"别怕此刻的黑暗,它只是在为闪光蓄能。把每一次想放弃的瞬间,都当成命运偷偷给你加油的暗号。记住:你比自己想象的更强大,只要再坚持一下,世界就会听见你的声音。"

目录

1.AVL树的概念

2.AVL树节点定义

3.AVL树的插入

5.AVL树的验证

6.AVL树的性能

结语


1.AVL树的概念

二叉搜索树(BST)虽然搜索效率高,但在极端情况下(例如插入有序序列),树会退化成单链表,导致时间复杂度从 O(log N) 退化为 O(N)。

为了解决这个问题,两位苏联数学家 G.M. Adelson-VelskyE.M. Landis 在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1 ,即可降低树的高度,从而减少平均搜索长度。这种树被命名为 AVL树

AVL树的特性:

  1. 它本身是一棵二叉搜索树。

  2. 任意节点的左右子树高度差(平衡因子)的绝对值不超过1。

  3. 如果一棵二叉搜索树是高度平衡的,它就是AVL树。有 N个结点的AVL树,高度可保持在 O(log N),搜索时间复杂度为 O(\log N)。

平衡因子 (Balance Factor, BF):

本文规定:平衡因子 = 右子树高度 - 左子树高度。

因此,AVL树中所有节点的平衡因子只可能是 -1, 0, 1。如果计算出其他值,说明树已经失衡,需要旋转。

如图就是一颗AVL树:

2.AVL树节点定义

AVL树的节点通常采用三叉链结构(包含父指针),这样在旋转和向上更新平衡因子时会更加方便。

cpp 复制代码
template <class K, class V>
struct AVLTreeNode
{
    typedef AVLTreeNode<K, V> Node;
    
    std::pair<K, V> _kv;
    Node* _left;
    Node* _right;
    Node* _parent;
    int _bf; // 平衡因子 = 右子树高度 - 左子树高度

    AVLTreeNode(const std::pair<K, V>& kv)
        : _kv(kv)
        , _left(nullptr)
        , _right(nullptr)
        , _parent(nullptr)
        , _bf(0)
    {}
};

3.AVL树的插入

AVL树的插入过程分为两步:

  1. 插入新节点:按照二叉搜索树的规则找到位置并插入。

  2. 更新平衡因子:从新插入节点的父节点开始,向上更新平衡因子,并检测是否需要旋转。

cpp 复制代码
bool Insert(const std::pair<K, V>& kv)
{
    if (_root == nullptr)
    {
        _root = new Node(kv);
        return true;
    }

    Node* parent = nullptr;
    Node* cur = _root;
    
    // 1. 寻找插入位置
    while (cur)
    {
        if (cur->_kv.first < kv.first) // 往右走
        {
            parent = cur;
            cur = cur->_right;
        }
        else if (cur->_kv.first > kv.first) // 往左走
        {
            parent = cur;
            cur = cur->_left;
        }
        else
        {
            return false; // key已存在
        }
    }

    // 2. 插入节点
    cur = new Node(kv);
    if (parent->_kv.first < kv.first)
    {
        parent->_right = cur;
    }
    else
    {
        parent->_left = cur;
    }
    cur->_parent = parent;

    // 3. 更新平衡因子
    while (parent)
    {
        // 根据定义:右 - 左
        if (cur == parent->_left)
            parent->_bf--; 
        else
            parent->_bf++;

        // 情况A:更新后为0,说明原来是1或-1,现在平衡了,无需向上更新
        if (parent->_bf == 0)
        {
            break;
        }
        // 情况B:更新后为1或-1,说明原来是0,现在变高了,需要继续向上更新
        else if (parent->_bf == 1 || parent->_bf == -1)
        {
            cur = parent;
            parent = parent->_parent;
        }
        // 情况C:更新后为2或-2,说明失衡了,需要旋转
        else if (parent->_bf == 2 || parent->_bf == -2)
        {
            // 旋转处理(详见下文)
            // 旋转完成后,该子树高度恢复,无需继续向上更新
            if (parent->_bf == 2 && cur->_bf == 1) // 右右 -> 左单旋
            {
                _RotateL(parent);
            }
            else if (parent->_bf == -2 && cur->_bf == -1) // 左左 -> 右单旋
            {
                _RotateR(parent);
            }
            else if (parent->_bf == 2 && cur->_bf == -1) // 右左 -> 右左双旋
            {
                _RotateRL(parent);
            }
            else if (parent->_bf == -2 && cur->_bf == 1) // 左右 -> 左右双旋
            {
                _RotateLR(parent);
            }
            break; // 旋转后子树高度降低回原样,停止更新
        }
        else
        {
            // 理论上不会出现 >2 或 <-2 的情况
            assert(false);
        }
    }
    return true;
}

4. AVL树的旋转

旋转是AVL树的核心。旋转的目的是在保持二叉搜索树性质(左<根<右)的前提下,降低树的高度。

4.1 左单旋 (Left Rotation)

触发场景 :新节点插入在较高右子树的右侧 (RR型)。 操作 :将右孩子 subR 提起来作为根,parent 变成 subR 的左孩子。

cpp 复制代码
void _RotateL(Node* parent)
{
    Node* subR = parent->_right;
    Node* subRL = subR->_left;
    Node* parentParent = parent->_parent;

    // 1. 处理 subRL
    parent->_right = subRL;
    if (subRL) subRL->_parent = parent;

    // 2. 处理 parent 和 subR 的关系
    subR->_left = parent;
    parent->_parent = subR;

    // 3. 处理 subR 和 上层节点 的关系
    if (parentParent == nullptr)
    {
        _root = subR;
        subR->_parent = nullptr;
    }
    else
    {
        if (parentParent->_left == parent)
            parentParent->_left = subR;
        else
            parentParent->_right = subR;
        subR->_parent = parentParent;
    }

    // 4. 更新平衡因子
    parent->_bf = subR->_bf = 0;
}

4.2 右单旋 (Right Rotation)

触发场景 :新节点插入在较高左子树的左侧 (LL型)。 操作 :将左孩子 subL 提起来作为根,parent 变成 subL 的右孩子。

cpp 复制代码
void _RotateR(Node* parent)
{
    Node* subL = parent->_left;
    Node* subLR = subL->_right;
    Node* parentParent = parent->_parent;

    parent->_left = subLR;
    if (subLR) subLR->_parent = parent;

    subL->_right = parent;
    parent->_parent = subL;

    if (parentParent == nullptr)
    {
        _root = subL;
        subL->_parent = nullptr;
    }
    else
    {
        if (parentParent->_left == parent)
            parentParent->_left = subL;
        else
            parentParent->_right = subL;
        subL->_parent = parentParent;
    }

    subL->_bf = parent->_bf = 0;
}

4.3 左右双旋 (Left-Right Rotation)

触发场景 :新节点插入在较高左子树的右侧 (LR型)。 操作 :先对左子树进行左单旋,再对当前树进行右单旋。 核心难点 :平衡因子的更新。根据插入位置的不同(subLR 的左边还是右边),最后的平衡因子结果不同。

cpp 复制代码
void _RotateLR(Node* parent)
{
    Node* subL = parent->_left;
    Node* subLR = subL->_right;
    int bf = subLR->_bf; // 记录旋转前 subLR 的平衡因子

    // 先左旋左孩子,再右旋自己
    _RotateL(parent->_left);
    _RotateR(parent);

    // 根据 subLR 原始的 bf 修正平衡因子
    if (bf == 0) // subLR 就是新增节点
    {
        parent->_bf = 0;
        subL->_bf = 0;
        subLR->_bf = 0;
    }
    else if (bf == -1) // 插入在 subLR 的左边
    {
        subL->_bf = 0;
        parent->_bf = 1; 
        subLR->_bf = 0;
    }
    else if (bf == 1) // 插入在 subLR 的右边
    {
        subL->_bf = -1;
        parent->_bf = 0;
        subLR->_bf = 0;
    }
}

4.4 右左双旋 (Right-Left Rotation)

触发场景 :新节点插入在较高右子树的左侧 (RL型)。 操作:先对右子树进行右单旋,再对当前树进行左单旋。

cpp 复制代码
void _RotateRL(Node* parent)
{
    Node* subR = parent->_right;
    Node* subRL = subR->_left;
    int bf = subRL->_bf;

    // 先右旋右孩子,再左旋自己
    _RotateR(parent->_right);
    _RotateL(parent);

    if (bf == 0)
    {
        parent->_bf = 0;
        subR->_bf = 0;
        subRL->_bf = 0;
    }
    else if (bf == 1) // 插入在 subRL 的右边
    {
        parent->_bf = -1;
        subR->_bf = 0;
        subRL->_bf = 0;
    }
    else if (bf == -1) // 插入在 subRL 的左边
    {
        parent->_bf = 0;
        subR->_bf = 1;
        subRL->_bf = 0;
    }
}

5.AVL树的验证

验证AVL树需要做两件事:

  1. 中序遍历:验证是否是有序序列。

  2. 平衡验证 :递归计算左右子树高度差,判断是否与当前节点的 _bf 相等,且绝对值不超过1。

注意 :这里的计算公式必须和插入时的逻辑一致(即:diff = 右 - 左)。

cpp 复制代码
int _Height(Node* root)
{
    if (root == nullptr) return 0;
    return std::max(_Height(root->_left), _Height(root->_right)) + 1;
}

bool _IsBalanceTree(Node* root)
{
    // 空树是平衡的
    if (root == nullptr) return true;

    // 计算高度
    int leftHeight = _Height(root->_left);
    int rightHeight = _Height(root->_right);
    
    // 按照插入逻辑:平衡因子 = 右 - 左
    int diff = rightHeight - leftHeight;

    // 检查1:当前计算的 diff 是否等于节点记录的 _bf
    // 检查2:绝对值是否超过 1
    if (diff != root->_bf)
    {
        std::cout << "平衡因子异常: " << root->_kv.first << " -> " << root->_bf << " vs " << diff << std::endl;
        return false;
    }
    if (abs(diff) > 1)
    {
        std::cout << "高度差超过1: " << root->_kv.first << std::endl;
        return false;
    }

    // 递归检查子树
    return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}

6.AVL树的性能

  • 性能:AVL树是严格平衡的,查找效率极其稳定,维持在 O(log N)。

  • 缺点:为了维持绝对平衡,AVL树在插入和删除时需要进行大量的旋转操作。

  • 应用场景 :适用于查询非常频繁 ,但插入和删除较少的静态数据场景。

  • 对比 :在实际应用(如C++ STL的 mapset)中,通常更倾向于使用红黑树。红黑树通过放弃"绝对平衡"(只保证最长路径不超过最短路径的2倍),换取了更少的旋转次数,在综合性能上更优。

结语

本文从二叉搜索树的退化问题出发,详细介绍了 AVL 树的定义、节点结构以及最核心的插入与旋转操作。AVL 树通过"平衡因子"和四种旋转策略(左旋、右旋、左右双旋、右左双旋),成功实现了绝对平衡,保证了树的高度维持在 O(log N)级别,从而解决了普通 BST 在极端场景下查询效率低下的问题。

虽然 AVL 树在查询性能上做到了极致,但其严格的平衡限制也导致了在插入和删除时需要频繁进行旋转调整,维护成本较高。这也是为什么在 C++ STL 的 mapset 中,更倾向于使用红黑树的原因------红黑树在"绝对平衡"与"高效维护"之间寻找到了更好的平衡点。在下一篇文章中,我们将深入探讨红黑树的奥秘。

以上就是本期博客的全部内容,感谢各位的阅读以及观看。如果内容有误请大佬们多多指教,一定积极改进,加以学习。

相关推荐
却话巴山夜雨时i42 分钟前
394. 字符串解码【中等】
java·数据结构·算法·leetcode
惊鸿.Jh1 小时前
503. 下一个更大元素 II
数据结构·算法·leetcode
客梦1 小时前
数据结构-栈与队列
数据结构·笔记
AKDreamer_HeXY1 小时前
ABC434E 题解
c++·算法·图论·atcoder
罗湖老棍子1 小时前
完全背包 vs 多重背包的优化逻辑
c++·算法·动态规划·背包
TL滕1 小时前
从0开始学算法——第四天(题目参考答案)
数据结构·笔记·python·学习·算法
potato_may1 小时前
C++ 发展简史与核心语法入门
开发语言·c++·算法
Liangwei Lin1 小时前
洛谷 P1443 马的遍历
数据结构·算法
老鱼说AI1 小时前
算法基础教学第二步:数组(超级详细原理级别讲解)
数据结构·神经网络·算法·链表