C++ AVL 树:平衡原理到完整实现(自平衡二叉搜索树)

前言

在二叉搜索树(BST)的使用中,我们会遇到一个致命问题:当数据有序插入时,普通二叉搜索树会退化成单链表,增删查改的时间复杂度从理想的O(logN)骤降至O(N),完全失去了搜索树的性能优势。

为了解决这个问题,前苏联科学家 G. M. Adelson-Velsky 和 E. M. Landis 在 1962 年的论文中提出了AVL 树 ------ 这是世界上第一个自平衡二叉搜索树,通过严格控制树的左右子树高度差,保证树的高度始终稳定在O(logN)级别,让所有操作的时间复杂度都能稳定在O(logN),从根本上解决了普通 BST 的退化问题。

一、AVL 树的核心概念

1.1 AVL 树的定义

AVL 树本质是一颗高度平衡的二叉搜索树,它要么是空树,要么必须同时满足以下两个性质:

  1. 它的左右子树都是 AVL 树;
  2. 左右子树的高度差的绝对值不超过 1。

1.2 平衡因子(Balance Factor)

为了方便监控和控制树的平衡状态,我们引入 ** 平衡因子(BF)** 的概念:

结点的平衡因子 = 该结点右子树的高度 - 左子树的高度

根据 AVL 树的定义,任何结点的平衡因子只能是 0、1、-1,一旦平衡因子的绝对值超过 1,就说明该结点所在的子树已经不平衡,需要进行旋转调整。平衡因子就像树平衡状态的 "风向标",让我们无需每次都递归计算子树高度,就能快速判断平衡状态。

1.3 设计细节:为什么高度差不超过 1,而不是 0?

很多初学者会有疑问:左右子树高度完全相等(高度差 0)不是更平衡吗?答案是:理想很美好,但现实中无法实现。比如当树只有 2 个结点、4 个结点时,无论怎么排列,都无法做到左右子树高度差为 0,最优情况就是高度差为 1。因此 AVL 树设计为 "高度差绝对值不超过 1",在保证平衡的同时,兼顾了实现的可行性。

1.4 性能优势

AVL 树的结点分布和完全二叉树高度相似,整棵树的高度可以严格控制在logN级别,因此无论是插入、删除还是查找操作,时间复杂度都能稳定在O(logN),相比普通二叉搜索树有了本质的性能提升。

二、AVL 树的结点结构设计

AVL 树的结点需要存储键值对、孩子指针、父指针和平衡因子,其中父指针是关键------ 插入新结点后,我们需要从新增结点向上遍历祖先结点更新平衡因子,父指针能让这个过程变得非常便捷。

完整的结点与 AVL 树类框架代码如下:

cpp 复制代码
#include <iostream>

using namespace std;

// AVL树结点结构
template<class K, class V>
struct AVLTreeNode
{
    pair<K, V> _kv;          // 键值对,保证key的唯一性
    AVLTreeNode<K, V>* _left;  // 左孩子指针
    AVLTreeNode<K, V>* _right; // 右孩子指针
    AVLTreeNode<K, V>* _parent;// 父结点指针
    int _bf;                   // 平衡因子 balance factor

    // 构造函数
    AVLTreeNode(const pair<K, V>& kv)
        :_kv(kv)
        , _left(nullptr)
        , _right(nullptr)
        , _parent(nullptr)
        , _bf(0)
    {}
};

// AVL树类
template<class K, class V>
class AVLTree
{
    typedef AVLTreeNode<K, V> Node;
public:
    // 成员函数声明,后续逐步实现
    bool Insert(const pair<K, V>& kv);
    Node* Find(const K& key);
    int Height();
    bool IsBalanceTree();

private:
    // 内部私有成员函数
    void RotateR(Node* parent);
    void RotateL(Node* parent);
    void RotateLR(Node* parent);
    void RotateRL(Node* parent);
    int _Height(Node* root);
    bool _IsBalanceTree(Node* root);

    // 根结点
    Node* _root = nullptr;
};

三、AVL 树的插入操作

插入是 AVL 树最核心的操作,分为两大核心阶段:按照二叉搜索树规则插入新结点向上更新平衡因子并处理不平衡

3.1 插入的整体流程

AVL 树的插入分为 4 个核心步骤:

  1. 按照二叉搜索树的规则,找到合适的插入位置,创建并插入新结点;
  2. 新增结点只会影响其祖先结点的高度,因此从新增结点的父结点开始,向上更新路径上所有祖先结点的平衡因子;
  3. 更新过程中,若所有结点的平衡因子始终合法(0/1/-1),没有出现不平衡,插入直接结束;
  4. 若更新中出现平衡因子为 2 或 - 2,说明该结点所在子树已不平衡,需要对该子树进行旋转调平衡;旋转后子树高度会恢复到插入前的水平,不会影响上层结点,插入结束。

3.2 平衡因子的更新规则

平衡因子的更新是插入操作的核心,我们需要严格遵循以下规则:

更新原则
  • 平衡因子 = 右子树高度 - 左子树高度;
  • 只有子树高度发生变化,才会影响父结点的平衡因子;
  • 新增结点在父结点的左子树,父结点的平衡因子--;新增结点在父结点的右子树,父结点的平衡因子++
更新停止条件

更新过程中,根据父结点更新后的平衡因子,分为三种情况决定是否继续向上更新:

  1. 更新后父结点平衡因子 = 0 :说明更新前父结点的平衡因子是 1 或 - 1,新增结点补在了矮的那一侧,插入后父结点所在子树的整体高度不变,不会影响上层祖先的平衡因子,直接停止更新
  2. 更新后父结点平衡因子 = 1 或 - 1 :说明更新前父结点的平衡因子是 0,新增结点让子树一侧变高,父结点所在子树的整体高度 + 1,会影响上层祖先的平衡因子,需要继续向上更新
  3. 更新后父结点平衡因子 = 2 或 - 2 :说明父结点所在子树已经违反平衡规则,出现了不平衡,需要立即进行旋转调平衡,旋转后停止更新

3.3 插入操作的完整代码实现

结合上述流程和规则,我们实现完整的 Insert 函数,其中旋转调平衡的逻辑会在后续章节详细实现:

cpp 复制代码
template<class K, class V>
bool AVLTree<K, V>::Insert(const pair<K, V>& kv)
{
    // 1. 空树,直接创建根结点
    if (_root == nullptr)
    {
        _root = new Node(kv);
        return true;
    }

    // 2. 按照二叉搜索树规则,找到插入位置
    Node* parent = nullptr;
    Node* cur = _root;
    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
        {
            // key已存在,插入失败
            return false;
        }
    }

    // 3. 创建新结点,链接到父结点
    cur = new Node(kv);
    if (parent->_kv.first < kv.first)
    {
        parent->_right = cur;
    }
    else
    {
        parent->_left = cur;
    }
    cur->_parent = parent;

    // 4. 向上更新平衡因子,并处理不平衡
    while (parent)
    {
        // 更新当前父结点的平衡因子
        if (cur == parent->_left)
            parent->_bf--;
        else
            parent->_bf++;

        // 根据平衡因子判断后续操作
        if (parent->_bf == 0)
        {
            // 子树高度不变,停止更新
            break;
        }
        else if (parent->_bf == 1 || parent->_bf == -1)
        {
            // 子树高度+1,继续向上更新
            cur = parent;
            parent = parent->_parent;
        }
        else if (parent->_bf == 2 || parent->_bf == -2)
        {
            // 子树不平衡,旋转调平衡
            if (parent->_bf == 2)
            {
                if (parent->_right->_bf == 1)
                {
                    // 右右场景,左单旋
                    RotateL(parent);
                }
                else if (parent->_right->_bf == -1)
                {
                    // 右左场景,右左双旋
                    RotateRL(parent);
                }
            }
            else // parent->_bf == -2
            {
                if (parent->_left->_bf == -1)
                {
                    // 左左场景,右单旋
                    RotateR(parent);
                }
                else if (parent->_left->_bf == 1)
                {
                    // 左右场景,左右双旋
                    RotateLR(parent);
                }
            }
            // 旋转后子树高度恢复,无需继续向上更新
            break;
        }
        else
        {
            // 平衡因子异常,触发断言
            assert(false);
        }
    }
    return true;
}

四、AVL 树的核心:旋转调平衡

当插入结点导致子树不平衡时,我们需要通过旋转来恢复平衡,旋转必须遵循两个核心原则:

  1. 旋转后必须保持二叉搜索树的性质(左子树所有结点 < 根结点 < 右子树所有结点);
  2. 让不平衡的子树恢复平衡,同时将子树的高度恢复到插入前的水平,彻底消除对上层结点的影响。

根据不平衡的场景,旋转分为四种:右单旋、左单旋、左右双旋、右左双旋,下面我们逐一拆解。

4.1 右单旋(左左场景)

触发场景

当不平衡结点的平衡因子为-2,且其左孩子的平衡因子为-1时触发,也就是新结点插入在不平衡结点左孩子的左子树中,属于纯粹的 "左边高",我们称之为左左场景

旋转原理

我们用抽象模型来讲解:parent是不平衡的根结点,subLparent的左孩子,subLRsubL的右子树,a/b/c是高度为h的 AVL 子树。

  • 二叉搜索树规则保证:subL < subLR < parent
  • 旋转核心步骤:将subLR变成parent的左子树,将parent变成subL的右子树,最终subL成为这棵子树新的根结点;
  • 旋转后,子树高度恢复到插入前的水平,parentsubR的平衡因子都置为 0。
代码实现
cpp 复制代码
template<class K, class V>
void AVLTree<K, V>::RotateR(Node* parent)
{
    Node* subL = parent->_left;
    Node* subLR = subL->_right;

    // 1. 修改孩子指针,保持二叉搜索树规则
    parent->_left = subLR;
    subL->_right = parent;

    // 2. 处理subLR的父指针
    if (subLR)
        subLR->_parent = parent;

    // 3. 保存parent的父结点,处理上层链接
    Node* parentParent = parent->_parent;

    // 4. 更新parent和subL的父指针
    parent->_parent = subL;

    // 5. 处理子树与上层的链接
    if (parentParent == nullptr)
    {
        // parent是整棵树的根,更新_root
        _root = subL;
        subL->_parent = nullptr;
    }
    else
    {
        // parent是局部子树,链接到父结点
        if (parent == parentParent->_left)
            parentParent->_left = subL;
        else
            parentParent->_right = subL;
        subL->_parent = parentParent;
    }

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

4.2 左单旋(右右场景)

触发场景

当不平衡结点的平衡因子为2,且其右孩子的平衡因子为1时触发,也就是新结点插入在不平衡结点右孩子的右子树中,属于纯粹的 "右边高",我们称之为右右场景

旋转原理

左单旋是右单旋的完全对称操作:parent是不平衡的根结点,subRparent的右孩子,subRLsubR的左子树。

  • 二叉搜索树规则保证:parent < subRL < subR
  • 旋转核心步骤:将subRL变成parent的右子树,将parent变成subR的左子树,最终subR成为这棵子树新的根结点;
  • 旋转后,子树高度恢复到插入前的水平,parentsubR的平衡因子都置为 0。
代码实现
cpp 复制代码
template<class K, class V>
void AVLTree<K, V>::RotateL(Node* parent)
{
    Node* subR = parent->_right;
    Node* subRL = subR->_left;

    // 1. 修改孩子指针
    parent->_right = subRL;
    subR->_left = parent;

    // 2. 处理subRL的父指针
    if (subRL)
        subRL->_parent = parent;

    // 3. 保存parent的父结点
    Node* parentParent = parent->_parent;

    // 4. 更新parent和subR的父指针
    parent->_parent = subR;

    // 5. 处理子树与上层的链接
    if (parentParent == nullptr)
    {
        // parent是整棵树的根
        _root = subR;
        subR->_parent = nullptr;
    }
    else
    {
        // parent是局部子树
        if (parent == parentParent->_left)
            parentParent->_left = subR;
        else
            parentParent->_right = subR;
        subR->_parent = parentParent;
    }

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

4.3 左右双旋(左右场景)

触发场景

当不平衡结点的平衡因子为-2,且其左孩子的平衡因子为1时触发,也就是新结点插入在不平衡结点左孩子的右子树中,我们称之为左右场景

这种场景下,单纯的右单旋无法解决问题,旋转后树依然不平衡,需要分两步完成:

  1. 先对不平衡结点的左孩子,执行一次左单旋;
  2. 再对不平衡结点本身,执行一次右单旋。
平衡因子更新规则

双旋的核心难点在于平衡因子的更新,需要根据中间结点subLR(左孩子的右子树根)的平衡因子,分三种场景处理:

  1. subLR->_bf == 0:旋转后,subLsubLRparent的平衡因子全部置为 0;
  2. subLR->_bf == -1:旋转后,subL->_bf=0subLR->_bf=0parent->_bf=1
  3. subLR->_bf == 1:旋转后,subL->_bf=-1subLR->_bf=0parent->_bf=0
代码实现
cpp 复制代码
template<class K, class V>
void AVLTree<K, V>::RotateLR(Node* parent)
{
    Node* subL = parent->_left;
    Node* subLR = subL->_right;
    // 保存subLR的平衡因子,用于后续更新
    int bf = subLR->_bf;

    // 1. 先对左孩子执行左单旋
    RotateL(parent->_left);
    // 2. 再对不平衡结点执行右单旋
    RotateR(parent);

    // 3. 根据subLR的原始平衡因子,更新各结点平衡因子
    if (bf == 0)
    {
        subL->_bf = 0;
        subLR->_bf = 0;
        parent->_bf = 0;
    }
    else if (bf == -1)
    {
        subL->_bf = 0;
        subLR->_bf = 0;
        parent->_bf = 1;
    }
    else if (bf == 1)
    {
        subL->_bf = -1;
        subLR->_bf = 0;
        parent->_bf = 0;
    }
    else
    {
        // 非法平衡因子
        assert(false);
    }
}

4.4 右左双旋(右左场景)

触发场景

当不平衡结点的平衡因子为2,且其右孩子的平衡因子为-1时触发,也就是新结点插入在不平衡结点右孩子的左子树中,我们称之为右左场景

这是左右双旋的对称场景,同样需要两步旋转:

  1. 先对不平衡结点的右孩子,执行一次右单旋;
  2. 再对不平衡结点本身,执行一次左单旋。
平衡因子更新规则

根据中间结点subRL(右孩子的左子树根)的平衡因子,分三种场景处理:

  1. subRL->_bf == 0:旋转后,subRsubRLparent的平衡因子全部置为 0;
  2. subRL->_bf == 1:旋转后,subR->_bf=0subRL->_bf=0parent->_bf=-1
  3. subRL->_bf == -1:旋转后,subR->_bf=1subRL->_bf=0parent->_bf=0
代码实现
cpp 复制代码
template<class K, class V>
void AVLTree<K, V>::RotateRL(Node* parent)
{
    Node* subR = parent->_right;
    Node* subRL = subR->_left;
    // 保存subRL的平衡因子
    int bf = subRL->_bf;

    // 1. 先对右孩子执行右单旋
    RotateR(parent->_right);
    // 2. 再对不平衡结点执行左单旋
    RotateL(parent);

    // 3. 更新平衡因子
    if (bf == 0)
    {
        subR->_bf = 0;
        subRL->_bf = 0;
        parent->_bf = 0;
    }
    else if (bf == 1)
    {
        subR->_bf = 0;
        subRL->_bf = 0;
        parent->_bf = -1;
    }
    else if (bf == -1)
    {
        subR->_bf = 1;
        subRL->_bf = 0;
        parent->_bf = 0;
    }
    else
    {
        assert(false);
    }
}

五、AVL 树的查找操作

AVL 树的查找完全遵循二叉搜索树的规则,从根结点开始,比当前结点 key 大就走右子树,比当前结点 key 小就走左子树,相等则找到,遍历到空则说明 key 不存在。

得益于 AVL 树的高度平衡特性,查找的时间复杂度稳定在O(logN),不会出现普通 BST 的退化问题。

代码实现
cpp 复制代码
template<class K, class V>
typename AVLTree<K, V>::Node* AVLTree<K, V>::Find(const K& key)
{
    Node* cur = _root;
    while (cur)
    {
        if (cur->_kv.first < key)
        {
            // key更大,走右子树
            cur = cur->_right;
        }
        else if (cur->_kv.first > key)
        {
            // key更小,走左子树
            cur = cur->_left;
        }
        else
        {
            // 找到目标结点
            return cur;
        }
    }
    // 未找到
    return nullptr;
}

六、AVL 树的平衡校验

写完 AVL 树的核心逻辑后,我们需要通过程序反向验证树的平衡性,确保代码实现的正确性,同时通过测试用例验证功能和性能。

6.1 树高度计算与平衡校验

我们需要实现两个核心工具函数:

  1. _Height:递归计算子树的高度;
  2. _IsBalanceTree:递归校验整棵树是否符合 AVL 树的规则,检查两点:
    • 每个结点的左右子树高度差绝对值是否小于 2;
    • 结点存储的平衡因子,是否等于实际计算的 "右子树高度 - 左子树高度";
    • 左右子树是否都是合法的 AVL 树。
代码实现
cpp 复制代码
// 计算子树高度
template<class K, class V>
int AVLTree<K, V>::_Height(Node* root)
{
    if (root == nullptr)
        return 0;

    int leftHeight = _Height(root->_left);
    int rightHeight = _Height(root->_right);

    return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

// 对外暴露的高度计算接口
template<class K, class V>
int AVLTree<K, V>::Height()
{
    return _Height(_root);
}

// 内部平衡校验函数
template<class K, class V>
bool AVLTree<K, V>::_IsBalanceTree(Node* root)
{
    // 空树是合法的AVL树
    if (nullptr == root)
        return true;

    // 计算当前结点的实际高度差
    int leftHeight = _Height(root->_left);
    int rightHeight = _Height(root->_right);
    int diff = rightHeight - leftHeight;

    // 校验1:高度差绝对值是否超过1
    if (abs(diff) >= 2)
    {
        cout << root->_kv.first << " 结点高度差异常,高度差:" << diff << endl;
        return false;
    }

    // 校验2:存储的平衡因子是否与实际高度差一致
    if (root->_bf != diff)
    {
        cout << root->_kv.first << " 结点平衡因子异常,存储BF:" << root->_bf
             << ",实际BF:" << diff << endl;
        return false;
    }

    // 递归校验左右子树
    return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}

// 对外暴露的平衡校验接口
template<class K, class V>
bool AVLTree<K, V>::IsBalanceTree()
{
    return _IsBalanceTree(_root);
}

七、总结

AVL 树作为最早的自平衡二叉搜索树,通过严格的高度差控制,彻底解决了普通二叉搜索树的退化问题,实现了稳定的O(logN)级别的增删查改操作。

本文完整覆盖了 AVL 树的核心知识点:

  1. 核心概念:平衡定义、平衡因子的设计与作用;
  2. 结点设计:带父指针的三叉链结构,方便平衡因子更新;
  3. 插入操作:BST 插入 + 平衡因子更新的完整流程;
  4. 旋转调平衡:四种旋转场景的原理与代码实现,这是 AVL 树的核心;
  5. 查找与校验:查找逻辑与平衡校验方法,确保实现的正确性。

关于 AVL 树的删除操作,其核心逻辑是先按照二叉搜索树规则删除结点,再从删除位置向上更新平衡因子,遇到不平衡时同样通过旋转调平衡,实现复杂度较高,有兴趣的同学可以参考《殷人昆 数据结构:用面向对象方法与 C++ 语言描述》中的详细讲解。

在实际工程中,AVL 树因为严格的平衡要求,插入删除时的旋转次数较多,更适合查询频繁、插入删除较少的场景;而后续出现的红黑树,通过更宽松的平衡规则,减少了旋转次数,成为了工业界更主流的选择。但 AVL 树作为自平衡搜索树的基础,是理解红黑树、B + 树等高级数据结构的必经之路。

相关推荐
贾斯汀玛尔斯2 小时前
每天学一个算法--最短路径问题与三类基本算法
算法
啊我不会诶2 小时前
2025浙江省赛补题
c++·算法
@小柯555m2 小时前
算法(字母异位词分组)
java·开发语言·算法·leetcode
故事和你912 小时前
洛谷-算法2-1-前缀和、差分与离散化2
开发语言·数据结构·算法·深度优先·动态规划·图论
贾斯汀玛尔斯2 小时前
每天学一个算法--DFS / BFS
算法·深度优先·宽度优先
郝学胜-神的一滴2 小时前
epoll 边缘触发 vs 水平触发:从管道到套接字的深度实战
linux·服务器·开发语言·c++·网络协议·unix
cpp_25012 小时前
P1877 [HAOI2012] 音量调节
数据结构·c++·算法·动态规划·题解·洛谷·背包dp
dragen_light2 小时前
1.ROS2-Install
c++·python·ros
Gary Studio2 小时前
基于PMSM理论研究加实践
算法