de风——【从零开始学习C++】(十八):AVL树——让二叉搜索树自己“站起来“的自平衡黑科技


✍️ 前言

🎯 一句话总结:AVL树是二叉搜索树的"进化版",它通过旋转操作自动保持平衡,让增删查的效率永远稳定在 O(logN)。

为什么要学 AVL 树?

在上一篇博客中,我们学习了二叉搜索树(BST)。BST 有一个致命缺陷:当插入的数据有序时,它会退化成链表。比如依次插入 1、2、3、4、5,BST 就变成了一条向右倾斜的"一字长蛇阵",查找效率从 O(logN) 暴跌到 O(N)。

AVL 树就是为了解决这个问题而诞生的。它在每次插入/删除后都会检查平衡性,一旦发现左右子树高度差超过 1,就立刻通过旋转操作恢复平衡。无论你怎么插入数据,它都能保持树的高度在 logN 级别。

本篇你将学到什么?

学习目标 具体内容
🧩 AVL 概念 自平衡原理、平衡因子的定义与作用
📦 树的结构 结点定义、parent 指针的必要性
🔧 插入操作 BST 插入 + 平衡因子更新 + 旋转触发
🔄 四种旋转 右单旋、左单旋、左右双旋、右左双旋
🔍 查找与验证 O(logN) 查找、平衡性检测代码
🐛 经典 Bug 旋转中容易踩的坑

💡 前置知识 :建议先阅读本系列第十六篇:二叉搜索树,理解 BST 的插入、查找和删除操作,因为 AVL 树就是在 BST 的基础上增加了平衡控制。

准备好了吗?让我们开始吧!🚀


🧠 全文思维导图

以下思维导图总结了本文所有核心知识点,建议先浏览一遍建立整体印象,再逐节深入学习。


📋 目录


一、AVL树的概念

1. 简介

AVL 树是最先发明的自平衡二叉搜索树,由两位前苏联科学家 G. M. Adelson-Velsky 和 E. M. Landis 在 1962 年的论文中提出,AVL 就得名于他们名字的缩写。

AVL 树是一棵空树,或者具备以下性质的二叉搜索树:

  • 它的左右子树都是 AVL 树
  • 左右子树的高度差的绝对值不超过 1

通过控制高度差,AVL 树确保整棵树始终保持"矮胖"形态,高度被控制在 logN 级别,增删查的效率都能稳定在 O(logN)

平衡因子(Balance Factor)

为了方便观察和控制树的平衡性,AVL 树引入了平衡因子的概念:

平衡因子 = 右子树高度 - 左子树高度

在 AVL 树中,任何结点的平衡因子只能是 0、1 或 -1。如果某个结点的平衡因子变成了 2 或 -2,就说明树失衡了,需要旋转调整。

💡 思考:为什么 AVL 树要求高度差不超过 1,而不是高度差为 0?因为有些情况下做不到高度差为 0!比如只有 2 个结点的树,左右子树高度差必然是 1。

BST 退化问题演示

复制代码
普通BST(退化成链表):          AVL树(自动平衡):

    1                              2
     \                           /   \
      2                        1      3
       \                                \
        3                                4
         \                                \
          4                                5
           \
            5

  高度 = 5, 查找O(N)            高度 = 3, 查找O(logN)

二、AVL树的结点定义与结构

1. 简介

AVL 树的结点与普通 BST 结点相比,多了两个关键成员:

  1. _parent 指针 :指向父结点。因为插入新结点后,需要从新结点开始向上更新祖先结点的平衡因子,没有 parent 指针就无法回溯。
  2. _bf 平衡因子:记录当前结点的平衡状态。

2. 代码示例

AVL 树结点定义------三叉链 + 平衡因子
cpp 复制代码
#include <iostream>
#include <assert.h>
using namespace std;

// AVL树结点定义(三叉链)
template<class K, class V>
struct AVLTreeNode
{
    pair<K, V> _kv;                    // 键值对
    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:
    // 插入、查找、旋转等接口...
    
private:
    Node* _root = nullptr;
};

💡 为什么用 struct 而不是 class 因为结点需要直接访问成员,struct 默认公有更方便。树主体用 class 封装,对外只暴露公共接口。

3. 经典Bug:忘记 parent 指针

问题描述 :新手定义结点时只写了 _left_right(二叉链),忘了加 _parent。结果插入新结点后,不知道怎么向上更新祖先的平衡因子。

错误写法

cpp 复制代码
// ❌ 错误:没有parent指针,无法向上回溯
template<class K, class V>
struct AVLTreeNode
{
    pair<K, V> _kv;
    AVLTreeNode<K, V>* _left;
    AVLTreeNode<K, V>* _right;
    int _bf;  // 有了平衡因子,但没法向上更新!
};

正确做法 :必须使用三叉链(_left + _right + _parent),这样才能从新插入的结点一路向上更新平衡因子直到根结点。


三、AVL树的插入操作

1. 简介

AVL 树的插入分为两大步骤:

  1. 按 BST 规则插入:比当前结点小往左走,大往右走,找到空位置插入。
  2. 更新平衡因子 + 必要时旋转:从新结点的 parent 开始向上更新平衡因子,如果发现某个结点的平衡因子变成 ±2,就进行旋转处理。

插入的完整流程

复制代码
1. BST规则找到插入位置
2. 插入新结点,链接parent
3. 从parent开始向上更新平衡因子:
   - 新结点在parent左边 → parent->_bf--
   - 新结点在parent右边 → parent->_bf++
4. 根据更新后的平衡因子决定后续操作:
   - bf == 0  → 子树高度没变,停止更新
   - bf == ±1 → 子树高度变了,继续向上更新
   - bf == ±2 → 子树失衡,旋转处理,然后停止

2. 代码示例

插入操作完整实现------BST插入 + 平衡因子更新
cpp 复制代码
bool Insert(const pair<K, V>& kv)
{
    // 1. 空树直接插入
    if (_root == nullptr)
    {
        _root = new Node(kv);
        return true;
    }

    // 2. 按BST规则找到插入位置
    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. 创建新结点并链接到parent
    cur = new Node(kv);
    if (parent->_kv.first < kv.first)
    {
        parent->_right = cur;
    }
    else
    {
        parent->_left = cur;
    }
    cur->_parent = parent;  // 别忘了设置parent指针!

    // 4. 从parent开始向上更新平衡因子
    while (parent)
    {
        if (cur == parent->_left)
            parent->_bf--;      // 插在左边,平衡因子减1
        else
            parent->_bf++;      // 插在右边,平衡因子加1

        if (parent->_bf == 0)
        {
            // 平衡因子从±1变成0,说明矮的一边被填上了
            // 子树整体高度没变,不需要继续向上更新
            break;
        }
        else if (parent->_bf == 1 || parent->_bf == -1)
        {
            // 平衡因子从0变成±1,说明子树高度增加了
            // 需要继续向上更新祖先的平衡因子
            cur = parent;
            parent = parent->_parent;
        }
        else if (parent->_bf == 2 || parent->_bf == -2)
        {
            // 平衡因子变成±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
        {
            // 理论上不应该走到这里,走到了说明逻辑有bug
            assert(false);
        }
    }
    return true;
}
中序遍历验证------验证BST性质是否保持
cpp 复制代码
void _InOrder(Node* root)
{
    if (root == nullptr)
        return;
    _InOrder(root->_left);
    cout << root->_kv.first << ":" << root->_kv.second << " ";
    _InOrder(root->_right);
}

void InOrder()
{
    _InOrder(_root);
    cout << endl;
}

💡 验证方式:中序遍历如果输出有序序列,说明 BST 性质没有被旋转破坏。AVL 树旋转后仍然是一棵合法的二叉搜索树。

3. 经典Bug:旋转条件判断错误

问题描述:新手容易只看 parent 的平衡因子来决定旋转类型,忘了看 cur 的平衡因子。

错误写法

cpp 复制代码
// ❌ 错误:只看parent,没看cur
if (parent->_bf == 2)
    RotateL(parent);     // 万一是右左双旋的情况呢?
else if (parent->_bf == -2)
    RotateR(parent);     // 万一是左右双旋的情况呢?

正确做法 :必须同时看 parent 和 cur 的平衡因子,四种组合对应四种旋转:

parent->_bf cur->_bf 旋转类型 含义
-2 -1 右单旋 左边的左边高
2 1 左单旋 右边的右边高
-2 1 左右双旋 左边的右边高
2 -1 右左双旋 右边的左边高

四、平衡因子的更新规则

1. 简介

平衡因子的更新是 AVL 树最核心的逻辑之一。理解了它,就理解了 AVL 树"什么时候需要旋转"。

更新原则

  • 平衡因子 = 右子树高度 - 左子树高度
  • 插入结点在 parent 的左子树 → parent 的平衡因子 --
  • 插入结点在 parent 的右子树 → parent 的平衡因子 ++

三种停止条件

更新后 parent->_bf 含义 操作
0 原来±1,矮的一边被填上,子树高度不变 停止更新
±1 原来0,两边变成一高一低,子树高度+1 继续向上更新
±2 原来±1,高的一边更高了,子树失衡 旋转处理,然后停止

2. 代码示例

三种停止条件的场景演示
cpp 复制代码
// 场景1:bf从-1变成0 → 停止
// 插入前:左子树高1,右子树高0,bf=-1
// 在左子树插入后:左子树高1,右子树高0... 不对
// 实际是:插入前左低右高bf=1,插在左边后两边等高bf=0
//
//    5          5
//     \   →    / \
//      8      3   8     bf: 1→0,高度不变,停止!
//     /
//    3  ← 新插入的结点

// 场景2:bf从0变成1 → 继续向上
//
//    5          5
//   / \   →    / \
//  3   8      3   8
//                \
//                 9  ← 新插入
// 5的bf: 0→1,子树高度变了,需要继续向上更新!

// 场景3:bf从1变成2 → 旋转!
//
//    5              8
//     \     →      / \
//      8          5   9     5的bf从1变2,左单旋!
//       \
//        9  ← 新插入

💡 记忆口诀0停、1传、2转。bf变成0就停,变成±1就往上传,变成±2就旋转。


五、四种旋转详解

旋转原则

  1. 保持搜索树的规则(中序遍历仍然有序)
  2. 让旋转的树从不平衡变平衡,同时降低高度

旋转分为四种:右单旋、左单旋、左右双旋、右左双旋。


5.1 右单旋(RotateR)

简介

适用场景:左边太高(parent->_bf == -2 且 cur->_bf == -1),即"左边的左边高"。

核心思路:把 parent 往右下方"压",让左孩子 subL 上来当新的根。subL 的右子树 subLR 变成 parent 的左子树。

图解

复制代码
旋转前:              旋转后:
      10                5
      / \              / \
     5   c    →       a   10
    / \                  / \
   a   b                b   c

a/b/c是高度为h的子树
在a中插入导致10的bf从-1变-2
旋转后:5成为新根,b变成10的左子树
高度恢复到插入前的h+2
代码示例
右单旋实现------parent左子树太高时右旋
cpp 复制代码
void RotateR(Node* parent)
{
    Node* subL = parent->_left;       // parent的左孩子
    Node* subLR = subL->_right;      // subL的右子树(需要转移给parent)

    // 1. subLR变成parent的左子树
    parent->_left = subLR;
    if (subLR)                        // subLR可能为空!必须判断
        subLR->_parent = parent;

    // 2. parent变成subL的右子树
    Node* parentParent = parent->_parent;  // 先保存parent的父亲
    subL->_right = parent;
    parent->_parent = subL;

    // 3. 处理subL与上层(parentParent)的链接
    if (parentParent == nullptr)
    {
        // parent原来是整棵树的根
        _root = subL;
        subL->_parent = nullptr;
    }
    else
    {
        // parent原来是局部子树,需要和上层链接
        if (parent == parentParent->_left)
        {
            parentParent->_left = subL;
        }
        else
        {
            parentParent->_right = subL;
        }
        subL->_parent = parentParent;
    }

    // 4. 更新平衡因子
    parent->_bf = subL->_bf = 0;
}
经典Bug:subLR为空未判断
cpp 复制代码
// ❌ 错误写法:直接解引用subLR,可能空指针崩溃
parent->_left = subLR;
subLR->_parent = parent;   // 如果subLR是nullptr,直接崩!

// ✅ 正确写法:先判断是否为空
parent->_left = subLR;
if (subLR)
    subLR->_parent = parent;

5.2 左单旋(RotateL)

简介

适用场景:右边太高(parent->_bf == 2 且 cur->_bf == 1),即"右边的右边高"。

核心思路:和右单旋对称。把 parent 往左下方"压",让右孩子 subR 上来当新的根。subR 的左子树 subRL 变成 parent 的右子树。

图解

复制代码
旋转前:              旋转后:
  10                    15
  / \                  /  \
 a   15      →       10    c
     / \             / \
    b   c           a   b

在c中插入导致10的bf从1变2
旋转后:15成为新根,b变成10的右子树
代码示例
左单旋实现------parent右子树太高时左旋
cpp 复制代码
void RotateL(Node* parent)
{
    Node* subR = parent->_right;       // parent的右孩子
    Node* subRL = subR->_left;        // subR的左子树(需要转移给parent)

    // 1. subRL变成parent的右子树
    parent->_right = subRL;
    if (subRL)                         // subRL可能为空!
        subRL->_parent = parent;

    // 2. parent变成subR的左子树
    Node* parentParent = parent->_parent;
    subR->_left = parent;
    parent->_parent = subR;

    // 3. 处理subR与上层的链接
    if (parentParent == nullptr)
    {
        _root = subR;
        subR->_parent = nullptr;
    }
    else
    {
        if (parent == parentParent->_left)
        {
            parentParent->_left = subR;
        }
        else
        {
            parentParent->_right = subR;
        }
        subR->_parent = parentParent;
    }

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

💡 规律:左单旋和右单旋完全对称。左单旋操作右孩子,右单旋操作左孩子。代码结构一模一样,只是左右互换。


5.3 左右双旋(RotateLR)

简介

适用场景:左边高,但插入在左孩子的右子树上(parent->_bf == -2 且 cur->_bf == 1),即"左边的右边高"。

核心思路:右单旋解决不了这个问题!因为问题不在"纯粹的左边高",而是左孩子 subL 的右边高。需要两步:

  1. 先以 subL 为旋转点做左单旋(把右边的"折"拉直)
  2. 再以 parent 为旋转点做右单旋

图解

复制代码
旋转前:           第一步:左旋subL       第二步:右旋parent
     10                10                    8
    /  \              /  \                 /   \
   5    c    →      8    c       →       5     10
    \              / \                     / \   / \
     8            5   b                   a   b b'  c
    / \           /
   a   b'        a

插入在b'位置(左孩子的右子树)
单次右旋无法解决,需要先左旋拉直,再右旋平衡
三种子场景

双旋后平衡因子的更新取决于新结点插入在哪个位置:

场景 subLR->_bf(旋转前) 旋转后 subL->_bf 旋转后 parent->_bf 含义
场景1 -1 0 1 插入在 subLR 的左子树
场景2 1 -1 0 插入在 subLR 的右子树
场景3 0 0 0 subLR 本身就是新插入的结点
代码示例
左右双旋实现------先左旋再右旋 + 分场景更新平衡因子
cpp 复制代码
void RotateLR(Node* parent)
{
    Node* subL = parent->_left;
    Node* subLR = subL->_right;     // subLR是双旋的关键结点
    int bf = subLR->_bf;            // 旋转前先保存subLR的平衡因子

    // 第一步:以subL为旋转点左单旋
    RotateL(parent->_left);

    // 第二步:以parent为旋转点右单旋
    RotateR(parent);

    // 第三步:根据subLR旋转前的平衡因子,分场景更新
    if (bf == 0)
    {
        // 场景3:subLR本身就是新增结点
        subL->_bf = 0;
        subLR->_bf = 0;
        parent->_bf = 0;
    }
    else if (bf == -1)
    {
        // 场景1:新增结点在subLR的左子树
        subL->_bf = 0;
        subLR->_bf = 0;
        parent->_bf = 1;
    }
    else if (bf == 1)
    {
        // 场景2:新增结点在subLR的右子树
        subL->_bf = -1;
        subLR->_bf = 0;
        parent->_bf = 0;
    }
    else
    {
        assert(false);  // 理论上不可能
    }
}

💡 关键点 :双旋调用了两次单旋,但单旋内部会把平衡因子都设为0,所以双旋需要在调用单旋之前 保存 subLR 的平衡因子,然后在单旋之后手动修正三个结点的平衡因子。

经典Bug:双旋后平衡因子全设为0
cpp 复制代码
// ❌ 错误写法:双旋后直接全设为0
void RotateLR(Node* parent)
{
    RotateL(parent->_left);
    RotateR(parent);
    // 单旋内部已经把bf设为0了,但双旋场景下不全都是0!
}

// ✅ 正确做法:保存subLR的bf,旋转后分场景更新
void RotateLR(Node* parent)
{
    Node* subL = parent->_left;
    Node* subLR = subL->_right;
    int bf = subLR->_bf;  // 先保存!
    RotateL(parent->_left);
    RotateR(parent);
    // 根据bf分三种情况更新...
}

5.4 右左双旋(RotateRL)

简介

适用场景:右边高,但插入在右孩子的左子树上(parent->_bf == 2 且 cur->_bf == -1),即"右边的左边高"。

核心思路:与左右双旋完全对称。两步:

  1. 先以 subR 为旋转点做右单旋(把左边的"折"拉直)
  2. 再以 parent 为旋转点做左单旋

图解

复制代码
旋转前:           第一步:右旋subR       第二步:左旋parent
  10                 10                     12
  / \                / \                   /    \
 a   15     →      a   12        →       10     15
    /                  / \                / \      \
   12                 b   15             a   b      c
    \
     c

插入在c位置(右孩子的左子树的右子树)
先右旋拉直,再左旋平衡
三种子场景
场景 subRL->_bf(旋转前) 旋转后 parent->_bf 旋转后 subR->_bf 含义
场景1 -1 0 1 插入在 subRL 的左子树
场景2 1 -1 0 插入在 subRL 的右子树
场景3 0 0 0 subRL 本身就是新结点
代码示例
右左双旋实现------先右旋再左旋 + 分场景更新平衡因子
cpp 复制代码
void RotateRL(Node* parent)
{
    Node* subR = parent->_right;
    Node* subRL = subR->_left;      // subRL是双旋的关键结点
    int bf = subRL->_bf;            // 旋转前先保存

    // 第一步:以subR为旋转点右单旋
    RotateR(parent->_right);

    // 第二步:以parent为旋转点左单旋
    RotateL(parent);

    // 第三步:根据subRL旋转前的平衡因子,分场景更新
    if (bf == 0)
    {
        // 场景3:subRL本身就是新增结点
        subR->_bf = 0;
        subRL->_bf = 0;
        parent->_bf = 0;
    }
    else if (bf == 1)
    {
        // 场景2:新增结点在subRL的右子树
        subR->_bf = 0;
        subRL->_bf = 0;
        parent->_bf = -1;
    }
    else if (bf == -1)
    {
        // 场景1:新增结点在subRL的左子树
        subR->_bf = 1;
        subRL->_bf = 0;
        parent->_bf = 0;
    }
    else
    {
        assert(false);
    }
}

四种旋转总结对比

旋转类型 触发条件 (parent, cur) 核心操作 适用场景
右单旋 RotateR (-2, -1) parent右旋,subL上位 左边的左边高
左单旋 RotateL (2, 1) parent左旋,subR上位 右边的右边高
左右双旋 RotateLR (-2, 1) 先左旋subL,再右旋parent 左边的右边高
右左双旋 RotateRL (2, -1) 先右旋subR,再左旋parent 右边的左边高

💡 记忆技巧

  • 单旋:直线型失衡 → 一次旋转解决
  • 双旋:折线型失衡 → 先把折线拉直(单旋一次),再平衡(单旋一次)

六、AVL树的查找

1. 简介

AVL 树的查找逻辑与普通 BST 完全相同:从根结点开始,比目标值小往右走,大往左走,相等就找到了。由于 AVL 树始终保持平衡,查找效率稳定在 O(logN)

2. 代码示例

查找操作------与BST逻辑一致
cpp 复制代码
Node* Find(const K& key)
{
    Node* cur = _root;
    while (cur)
    {
        if (cur->_kv.first < key)
        {
            cur = cur->_right;     // 目标比当前大,往右找
        }
        else if (cur->_kv.first > key)
        {
            cur = cur->_left;      // 目标比当前小,往左找
        }
        else
        {
            return cur;            // 找到了!
        }
    }
    return nullptr;                // 没找到
}
查找测试
cpp 复制代码
void TestFind()
{
    AVLTree<int, int> t;
    int a[] = {16, 3, 7, 11, 9, 26, 18, 14, 15};
    for (auto e : a)
        t.Insert({e, e});

    // 测试查找存在的值
    auto node = t.Find(11);
    if (node)
        cout << "找到: " << node->_kv.first << endl;
    else
        cout << "没找到" << endl;

    // 测试查找不存在的值
    node = t.Find(100);
    if (node)
        cout << "找到: " << node->_kv.first << endl;
    else
        cout << "没找到100" << endl;
}
// 输出:
// 找到: 11
// 没找到100

七、AVL树的平衡验证

1. 简介

我们实现了 AVL 树,但怎么知道写得对不对?最可靠的方法是:递归计算每个结点左右子树的实际高度差,验证高度差不超过 1,并且和结点存储的平衡因子一致

2. 代码示例

高度计算函数------递归求子树高度
cpp 复制代码
int _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;
}
平衡验证函数------检查高度差 + 平衡因子一致性
cpp 复制代码
bool _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 << "高度差异常" << endl;
        return false;
    }

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

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

// 对外接口
bool IsBalanceTree()
{
    return _IsBalanceTree(_root);
}
综合测试------常规用例 + 大量随机值
cpp 复制代码
void TestAVLTree1()
{
    AVLTree<int, int> t;
    // 包含双旋场景的测试用例
    int a[] = {4, 2, 6, 1, 3, 5, 15, 7, 16, 14};
    for (auto e : a)
    {
        t.Insert({e, e});
    }
    t.InOrder();                         // 中序遍历验证有序性
    cout << "是否平衡: " << t.IsBalanceTree() << endl;
}
// 输出:
// 1:1 2:2 3:3 4:4 5:5 6:6 7:7 14:14 15:15 16:16
// 是否平衡: 1

void TestAVLTree2()
{
    const int N = 100000;
    vector<int> v;
    v.reserve(N);
    srand(time(0));
    for (size_t i = 0; i < N; i++)
    {
        v.push_back(rand() + i);
    }

    AVLTree<int, int> t;
    for (auto e : v)
    {
        t.Insert(make_pair(e, e));
    }

    cout << "是否平衡: " << t.IsBalanceTree() << endl;
    cout << "树高度: " << t.Height() << endl;
    cout << "结点数: " << t.Size() << endl;
}
// 输出示例:
// 是否平衡: 1
// 树高度: 19        ← 10万结点高度才19,logN级别!
// 结点数: 100000

💡 10 万个结点,树高才 19 层! 这就是 AVL 树的威力------log₂(100000) ≈ 16.6,加上平衡因子的容差,高度控制在 19 左右,查找最多比较 19 次就能找到目标。


八、性能分析

1. 简介

AVL 树的性能优势在于:无论数据以什么顺序插入,它都能保持平衡,增删查的时间复杂度都稳定在 O(logN)。

性能对比

对比项 普通BST(最差) 普通BST(最优) AVL树
树高度 O(N) O(logN) O(logN)
查找效率 O(N) O(logN) O(logN)
插入效率 O(N) O(logN) O(logN)
删除效率 O(N) O(logN) O(logN)
是否自平衡
数据敏感 是(有序数据退化)

实际数据验证

结点数量 AVL树高度 理论log₂(N) 查找最大比较次数
100 ~8 6.6 8
1,000 ~11 10.0 11
10,000 ~15 13.3 15
100,000 ~19 16.6 19
1,000,000 ~23 19.9 23

💡 关键结论:AVL 树的高度始终控制在 logN 附近,即使百万级数据,查找也只需要 23 次比较。相比之下,退化的 BST 可能需要比较 100 万次!

AVL 树的适用场景

  • 适合:查找密集型场景(查多改少),因为 AVL 树是严格平衡的,查找效率极高
  • 不太适合:插入/删除密集型场景,因为严格平衡要求频繁旋转,维护成本较高。这种场景更适合红黑树(放宽平衡条件,减少旋转次数)

💡 延伸 :C++ STL 中的 setmap 底层用的是红黑树而不是 AVL 树。红黑树是 AVL 树的"宽松版",它不要求高度差不超过 1,而是通过颜色约束保证最长路径不超过最短路径的 2 倍,从而减少了插入/删除时的旋转次数。我们将在下一篇博客中学习红黑树。


九、完整代码汇总

以下是 AVL 树的完整实现代码,可以直接编译运行:

cpp 复制代码
#include <iostream>
#include <assert.h>
#include <vector>
#include <ctime>
#include <cstdlib>
using namespace std;

template<class K, class V>
struct AVLTreeNode
{
    pair<K, V> _kv;
    AVLTreeNode<K, V>* _left;
    AVLTreeNode<K, V>* _right;
    AVLTreeNode<K, V>* _parent;
    int _bf;

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

template<class K, class V>
class AVLTree
{
    typedef AVLTreeNode<K, V> Node;
public:
    bool Insert(const pair<K, V>& kv)
    {
        if (_root == nullptr)
        {
            _root = new Node(kv);
            return true;
        }

        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
            {
                return false;
            }
        }

        cur = new Node(kv);
        if (parent->_kv.first < kv.first)
            parent->_right = cur;
        else
            parent->_left = cur;
        cur->_parent = parent;

        while (parent)
        {
            if (cur == parent->_left)
                parent->_bf--;
            else
                parent->_bf++;

            if (parent->_bf == 0)
                break;
            else if (parent->_bf == 1 || parent->_bf == -1)
            {
                cur = parent;
                parent = parent->_parent;
            }
            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
                assert(false);
        }
        return true;
    }

    Node* Find(const K& key)
    {
        Node* cur = _root;
        while (cur)
        {
            if (cur->_kv.first < key)
                cur = cur->_right;
            else if (cur->_kv.first > key)
                cur = cur->_left;
            else
                return cur;
        }
        return nullptr;
    }

    void InOrder() { _InOrder(_root); cout << endl; }
    bool IsBalanceTree() { return _IsBalanceTree(_root); }
    int Height() { return _Height(_root); }

private:
    void RotateR(Node* parent)
    {
        Node* subL = parent->_left;
        Node* subLR = subL->_right;
        parent->_left = subLR;
        if (subLR) subLR->_parent = parent;
        Node* ppnode = parent->_parent;
        subL->_right = parent;
        parent->_parent = subL;
        if (ppnode == nullptr) { _root = subL; subL->_parent = nullptr; }
        else { if (ppnode->_left == parent) ppnode->_left = subL; else ppnode->_right = subL; subL->_parent = ppnode; }
        parent->_bf = subL->_bf = 0;
    }

    void RotateL(Node* parent)
    {
        Node* subR = parent->_right;
        Node* subRL = subR->_left;
        parent->_right = subRL;
        if (subRL) subRL->_parent = parent;
        Node* ppnode = parent->_parent;
        subR->_left = parent;
        parent->_parent = subR;
        if (ppnode == nullptr) { _root = subR; subR->_parent = nullptr; }
        else { if (ppnode->_left == parent) ppnode->_left = subR; else ppnode->_right = subR; subR->_parent = ppnode; }
        parent->_bf = subR->_bf = 0;
    }

    void RotateLR(Node* parent)
    {
        Node* subL = parent->_left;
        Node* subLR = subL->_right;
        int bf = subLR->_bf;
        RotateL(parent->_left);
        RotateR(parent);
        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);
    }

    void RotateRL(Node* parent)
    {
        Node* subR = parent->_right;
        Node* subRL = subR->_left;
        int bf = subRL->_bf;
        RotateR(parent->_right);
        RotateL(parent);
        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);
    }

    void _InOrder(Node* root)
    {
        if (root == nullptr) return;
        _InOrder(root->_left);
        cout << root->_kv.first << ":" << root->_kv.second << " ";
        _InOrder(root->_right);
    }

    int _Height(Node* root)
    {
        if (root == nullptr) return 0;
        int lh = _Height(root->_left);
        int rh = _Height(root->_right);
        return lh > rh ? lh + 1 : rh + 1;
    }

    bool _IsBalanceTree(Node* root)
    {
        if (root == nullptr) return true;
        int lh = _Height(root->_left);
        int rh = _Height(root->_right);
        int diff = rh - lh;
        if (abs(diff) >= 2) { cout << root->_kv.first << "高度差异常" << endl; return false; }
        if (root->_bf != diff) { cout << root->_kv.first << "平衡因子异常" << endl; return false; }
        return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
    }

    Node* _root = nullptr;
};

// ============ 测试代码 ============
int main()
{
    // 测试1:常规用例(包含双旋场景)
    AVLTree<int, int> t;
    int a[] = {4, 2, 6, 1, 3, 5, 15, 7, 16, 14};
    for (auto e : a)
        t.Insert({e, e});

    cout << "中序遍历: ";
    t.InOrder();
    cout << "是否平衡: " << t.IsBalanceTree() << endl;
    cout << "树高度: " << t.Height() << endl;

    // 测试2:大量随机值
    const int N = 100000;
    vector<int> v;
    v.reserve(N);
    srand(time(0));
    for (size_t i = 0; i < N; i++)
        v.push_back(rand() + i);

    AVLTree<int, int> t2;
    for (auto e : v)
        t2.Insert(make_pair(e, e));

    cout << "\n=== 10万随机值测试 ===" << endl;
    cout << "是否平衡: " << t2.IsBalanceTree() << endl;
    cout << "树高度: " << t2.Height() << endl;

    return 0;
}

十、总结

AVL 树核心知识点回顾

知识点 核心内容
定义 自平衡二叉搜索树,左右子树高度差 ≤ 1
平衡因子 bf = 右子树高度 - 左子树高度,取值 0/±1
结点结构 三叉链(left/right/parent)+ 平衡因子
插入 BST插入 → 向上更新bf → 失衡则旋转
右单旋 左边高(-2, -1),parent右旋
左单旋 右边高(2, 1),parent左旋
左右双旋 左边的右边高(-2, 1),先左旋subL再右旋parent
右左双旋 右边的左边高(2, -1),先右旋subR再左旋parent
查找 与BST相同,O(logN)
验证 递归检查高度差 ≤ 1 且 bf 与实际一致
性能 增删查均 O(logN),高度控制在 logN

新手容易踩的坑

Bug 原因 解决方案
空指针崩溃 subLR/subRL 可能为空 旋转时判断 if (subLR)
平衡因子错误 双旋后全设为0 保存 subLR/subRL 的 bf,分场景更新
旋转条件错误 只看 parent 的 bf 同时看 parent 和 cur 的 bf
无法向上更新 结点没有 parent 指针 使用三叉链
parent 链接错误 忘记判断 parent 是根还是子树 检查 parentParent 是否为 nullptr

💡 下一篇预告 :我们将学习红黑树 ------AVL 树的"宽松版",也是 C++ STL 中 setmap 的真正底层结构。红黑树通过颜色约束代替严格的高度差控制,在保持 O(logN) 效率的同时减少了旋转次数,是工程实践中更常用的自平衡树。


如果觉得本文有帮助,别忘了 点赞收藏 + 关注 哦!有问题欢迎评论区交流~ 🌸

相关推荐
牛油果子哥q1 小时前
C++STL容器详解:vector/list/string底层扩容、迭代器失效终极解决、容器选型避坑
开发语言·c++·list
MartinYeung52 小时前
[论文学习]MCPTox:面向真实世界MCP服务器的工具投毒攻击基准测试
运维·服务器·学习
math_hongfan2 小时前
事务下单与状态流转:ArkTS 的 JOIN 联表在鸿蒙订单里实战
android·学习·华为·harmonyos
welfare9622 小时前
新号别搞:c++类与对象
开发语言·c++
weixin_431600442 小时前
NestJS 入门(9):连上数据库,SQL 写在哪?
数据库·后端·sql·学习·nest.js
举手2 小时前
Epoll模型
linux·c++·学习
MartinYeung52 小时前
[论文学习]JBShield:通过激活概念分析与操纵防御大语言模型越狱攻击
人工智能·学习·语言模型
Shadow(⊙o⊙)2 小时前
Linux网络部分——TCP服务端、客户端架构分析多进程、多线程、线程池
c++·tcp/ip·架构
MartinYeung53 小时前
[论文学习]SMSR:带平滑检索的签名记忆——针对持久化LLM智能体系统运行时内存投毒的认证防御
人工智能·学习