【C++ STL篇(十)】深入理解 AVL 树:代码实现、旋转图解与平衡因子详解

C++ STL篇(十) ------ AVL树详解

本篇文章将带你从零开始,一步步掌握 AVL树的底层逻辑 。全程干货,坐稳发车~ ദ്ദി˶ー̀֊ー́ )✧

文章目录

  • [C++ STL篇(十) ------ AVL树详解](#C++ STL篇(十) —— AVL树详解)
    • [1. 什么是 AVL 树?](#1. 什么是 AVL 树?)
      • [1.1 核心定义](#1.1 核心定义)
      • [1.2 引入"平衡因子"](#1.2 引入“平衡因子”)
    • [2. 树的结构定义](#2. 树的结构定义)
    • [3. AVL 树的插入](#3. AVL 树的插入)
      • [3.1 插入的大概流程](#3.1 插入的大概流程)
      • [3.2 平衡因子更新的规则](#3.2 平衡因子更新的规则)
      • [3.3 插入与更新平衡因子的代码实现](#3.3 插入与更新平衡因子的代码实现)
    • [4. 旋转](#4. 旋转)
      • [4.1 右单旋(RotateR)](#4.1 右单旋(RotateR))
      • [4.2 左单旋(RotateL)](#4.2 左单旋(RotateL))
      • [4.3 左右双旋(RotateLR)](#4.3 左右双旋(RotateLR))
      • [4.4 右左双旋(RotateRL)](#4.4 右左双旋(RotateRL))
    • [5. 辅助功能:查找与平衡检测](#5. 辅助功能:查找与平衡检测)
      • [5.1 查找(find)](#5.1 查找(find))
      • [5.2 平衡检测(IsBalanceTree)](#5.2 平衡检测(IsBalanceTree))
    • [6. 删除操作(简介)](#6. 删除操作(简介))
    • [7. 完整代码展示](#7. 完整代码展示)
    • 结语:

1. 什么是 AVL 树?

在聊 AVL 树之前,我们先回顾一下普通的二叉搜索树(BST) 。二叉搜索树规定:对于任意一个节点,其左子树所有节点的值都比它小,右子树所有节点的值都比它大。这个特性使得查找一个值的时间复杂度在理想情况下是 O(log N)

但二叉搜索树有一个致命的缺陷:如果插入的数据本身就是有序的,比如依次插入 1, 2, 3, 4, 5,那么这棵树会退化成一根"链表"。此时查找效率骤降为 O(N),完全丧失了树形结构的优势!

AVL 树正是为了解决二叉搜索树退化为链表的问题而诞生的。 它在二叉搜索树的基础上增加了一个"平衡 "规则,强制树保持"高度平衡 "的状态,从而保证了增、删、查、改的效率始终是 O(log N)

AVL 树的名字来源于它的两位苏联发明者:G.M. Adelson-Velsky 和 E.M. Landis,他们在 1962 年的论文中首次公开了这一结构。所以,AVL 取的就是两位科学家姓氏的首字母。

1.1 核心定义

AVL 树要么是一棵 空树,要么是满足以下条件的二叉搜索树:

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

简单来说,AVL 树是一棵"高平衡"的二叉搜索树。它通过严格控制左右子树的高度差,来防止树形向一边倾倒。

你可能会好奇:为什么规定高度差不超过 1,而不是必须为 0(即完美平衡)呢?因为完美的平衡(左右子树高度完全相等)在很多情况下根本做不到。比如,一棵只有 2 个节点或 4 个节点的树,无论你怎么摆放,总有一边的子树会多出一层,高度差最小也只能是 1。

所以,高度差不超过 1 是我们在任何节点数量下都能维持的、最严格的平衡条件。

1.2 引入"平衡因子"

为了量化每个节点的"平衡程度",我们引入一个概念叫平衡因子(balance factor,简称 bf)

  • 平衡因子(bf) = 右子树高度 - 左子树高度

根据 AVL 树的定义,任何一个节点的平衡因子只可能是 -1、0、1。如果某个节点的平衡因子超出了这个范围(比如变成了 2 或 -2),那就说明这棵树在该节点处已经"失衡"了!我们需要立刻进行调整。

平衡因子并不是 AVL 树算法必须存储的字段(我们完全可以在需要时临时计算左右子树高度),但把它直接记录在每个节点中,就像给树安装了一个"风向标",可以让我们在插入、删除时,非常方便地判断树的倾斜状态,并快速做出反应。因此,在实际实现中,我们都会保存这个值。

因为 AVL 树能够将树的高度始终控制在 O(log N) 的级别(节点分布接近于完全二叉树),所以所有的查找、插入、删除操作,其时间复杂度都能稳定在 O(log N)


2. 树的结构定义

AVL树的结构:

cpp 复制代码
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)   // 新节点没有子树,平衡因子初始为0
    {}
};

template<class K,class V>
class AVLTree
{
	typedef AVLTreeNode<K,V> Node;
public:
	//...
private:
	Node* _root = nullptr;
};

这里有几点值得特别说明:

  • _parent 指针必不可少 :在 AVL 树的插入操作中,我们经常需要沿着树向上回溯,更新祖先节点的平衡因子。有了指向父节点的指针,这个回溯过程就变得非常简单直接,否则我们只能用栈来记录路径,既麻烦又低效。这种同时指向左孩子、右孩子和父节点的结构,我们称为"三叉链"。
  • 键值对 pair<K, V> :这里使用了pair,让我们的树同时拥有"键"和"值"的信息,类似于字典。查找、删除、插入都基于键(K)进行比较。
  • _bf 初始化为 0:一个新创建的叶子节点,既没有左孩子也没有右孩子,所以它的左右子树高度都是 0,平衡因子自然为 0。

3. AVL 树的插入

AVL 树的插入是整个结构的核心。

3.1 插入的大概流程

向 AVL 树中插入一个新值,大体分为以下四步:

  1. 执行标准的二叉搜索树插入
      首先,按照二叉搜索树的规则,从根节点开始比较,找到新节点应该被放置的"空位置",然后创建新节点并连接上去。这一步和普通二叉搜索树的插入完全一样。
  2. 向上更新平衡因子
      新增结点以后,只会影响祖先结点的高度,也就是可能会影响部分祖先结点的平衡因子,所以更新从新增结点->根结点路径上的平衡因子,实际中最坏情况下要更新到根,有些情况更新到中间就可以停止了,具体情况我们下面再详细分析。
  3. 检测失衡
      在更新平衡因子的过程中,如果发现某个祖先节点的平衡因子变成了 2 或 -2,说明以该节点为根的子树已经违反了 AVL 树的平衡规则,必须马上处理。
  4. 通过旋转恢复平衡
      对于失衡的子树,我们要对其进行"旋转"操作。旋转不仅能重新平衡这棵子树,还会巧妙地降低这棵子树的高度,使其恢复到插入之前的高度。因为高度恢复了,这棵子树对更上层的祖先就不会再有影响,所以旋转完成后,整个插入过程就结束了。

3.2 平衡因子更新的规则

更新平衡因子是 AVL 插入中最细腻的环节。我们只需要记住一条原则:只有子树高度发生了变化,当前节点的平衡因子才需要更新。 新插入节点肯定会增加高度,所以我们要从它开始向上更新。

更新的具体操作是:

  • 如果新节点插入在 parent右子树 ,则 parent->_bf++
  • 如果新节点插入在 parent左子树 ,则 parent->_bf--

那么,更新完一个 parent 节点后,我们怎么判断是继续向上,还是停下来呢?一共有三种情况:

  • 停止更新:parent->_bf 变为 0

    • 这代表更新前 parent 的平衡因子是 -1 或 1(即一边高一边低),而我们恰巧把新节点插在了的那一边。
    • 结果:两边高度变得相等,parent 这棵子树的总高度没有增加!
    • 既然高度没变,它就不会影响更上层的祖先节点,所以我们可以立刻"刹车",停止更新。
  • 继续向上:parent->_bf 变为 1 或 -1

    • 这代表更新前 parent 的平衡因子是 0(即两边一样高),新节点插入后,让某一边变高了。
    • 结果:parent 这棵子树的高度增加了 1,但依然满足平衡规则(因为高度差没有超过 1)。
    • 子树高度增加,意味着它的父节点会受到影响,所以我们需要把"当前节点"指针从 parent 向上移动到它的父节点,继续循环。
  • 旋转处理:parent->_bf 变为 2 或 -2
    • 这代表更新前 parent 的平衡因子就是 1 或 -1(已经一边高了),而新节点偏偏又插在了原本就的那一边。
    • 结果:parent 这棵子树严重失衡,必须立即进行旋转治疗。
    • 旋转的目标有两个:1. 把子树重新调整平衡;2. 让子树的高度恢复到插入前的水平。所以旋转完成后,树的高度不变,也就不用再向上更新了,插入过程结束。

如果一路畅通无阻,最后更新到了整棵树的根节点,并且根节点的平衡因子变成了 1 或 -1,那也代表更新完成,可以停止了。

3.3 插入与更新平衡因子的代码实现

现在我们把这个过程写成代码。它被封装在 AVLTree 类的 Insert 函数中:

cpp 复制代码
bool 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 // 树中已存在相同的键,插入失败
        {
            return false;
        }
    }

    // 3. 创建新节点并插入
    cur = new Node(kv);
    if (parent->_kv.first < kv.first)
    {
        parent->_right = cur;
    }
    else
    {
        parent->_left = cur;
    }
    // 千万别忘了把新节点的_parent指针指回去
    cur->_parent = parent;

    // 4. 核心:控制平衡,向上更新平衡因子
    while (parent)
    {
        // 先根据 cur 是父节点的哪边孩子,更新父节点的平衡因子
        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) // 失衡,需要旋转
        {
            //旋转处理
            break;
        }
        else
        {
            // 平衡因子出现其他值,代表逻辑出错,强制报错
            assert(false);
        }
    }
    return true;
}

在上面的代码中,cur 代表新插入的节点,parentcur 的父节点。在循环更新中,curparent 会一起上移。失衡判断中,我们用会用 parentcur 的平衡因子组合来决定调用哪个旋转函数。


4. 旋转

旋转是 AVL 树的灵魂。它的原则只有两条:

  1. 维持搜索树规则:旋转前后,中序遍历的顺序不能变,必须严格遵循"左 < 根 < 右"的关系。
  2. 降低高度,恢复平衡:旋转必须让失衡的子树重新满足平衡条件,并且整体高度降回插入前的水平。

所有复杂的旋转场景,都可以通过将一些子树"抽象化 "来理解。假设总是有一棵局部子树,其根节点我们用 parent 表示,而一些子树我们用长方形来表示,它们的高度记为 h。这样,我们就能用一种通用的模型来覆盖所有情况。

4.1 右单旋(RotateR)

场景 :新节点插入到了较高左子树的左侧(简称"左左失衡")。在代码中表现为 parent->_bf == -2cur->_bf == -1

抽象模型 :假设有一棵以节点 10 为根的子树(parent),它的左孩子是 5(我们称之为 subL)。5 的左子树是 a,右子树是 b10 的右子树是 cabc 都是高度为 h 的 AVL 子树。新节点被插入了 a 子树,导致 a 的高度变为 h+1,最终让 10 的平衡因子降为 -2。

旋转操作

  1. 5 的右子树 b 连接到 10 的左子树上。
  2. 10 连接到 5 的右子树上。
  3. 至此,5 成为了这棵子树新的根。

因为 510 以及 b 子树之间的大小关系是 5 < b < 10,这样的连接完美地维持了搜索树的特性。同时,旋转后树的高度又恢复到了 h+2,和插入前一样,所以不会影响上层节点。

具体场景:

下面我们把每一个抽象子树内部的情况拆开来分析

情况一:插入前a/b/c 三个子树的高度都为0

情况二:插入前a/b/c 三个子树的高度都为1

情况三:a/b/c 三个子树的高度都为2

这种情况就变得比较复杂了,当a/b/c 三个子树的高度都为2时,子树的形状就会有三种 (我们用x,y,z来表示):

这里我们先说个结论:b和c子树可以是x/y/z中任意一种,但是a子树必须是x,这是为什么呢?
分析:

如果a子树是y/z,那么在左子树插入节点后,a子树会出现两种情况,要么a子树的根节点的平衡因子直接更新为0,要么会在a子树中直接发生旋转,这样就更新不到上面了。而我们希望的是继续向上更新,然后以10为根节点进行旋转。

下面简单画了个图帮助理解:

综上,a子树必须是x,插入的时候 b 和 c 有3种选择(x/y/z),a有1种选择(x),a中有4个可以插入的位置,所以一共有 3x3x4 = 36 种场景

情况四:a/b/c 三个子树的高度 >=3

我们先来计算一下如果a/b/c 三个子树的高度都为3时,子树的情况:

x: 高度为3的满二叉树的AVL树

y-c: 代表一个组合,下面四个叶子节点保留任意1个/任意2个/任意3个,都满足高度为3的AVL树。合计: C(4,1)+C(4,2)+C(4,3) = 4+6+4 = 14 种形状。

  • b和c可以是x/y-C中任意一种,组合:15*15
  • a的情况跟情况三类似,要满a必须插入新结点后,a自身不旋转,a高度+1不段向上更新,引发10结点旋转。
  • a如果是x,插入位置可以是4个叶子的任意孩子位置,有8个。
  • a如果是y-C中4个叶子节点保留3个有4种形状,插入位置在有两个结点那边任意孩子位置,有4个。
  • 组合一下:这里合计 15x15x(8+4x4)=5400 种场景

算完了高度等于3的情况,你肯定能猜到我们使用抽象模型的原因了,因为当a/b/c 三个子树的高度 >= 3时,组合的情况真的非常之多,这些情况我们就不展开来分析了。

知道了具体的场景,我们可以发现,只要满足抽象模型中的结构关系,不管a/b/c子树有多高,发生失衡时都可以通过一次右旋恢复平衡。

接下来就开始写代码了:

代码拆解(对照上面的模型阅读):

cpp 复制代码
void RotateR(Node* parent)
{
    // parent 就是图中的 10
    Node* subL = parent->_left;      // subL 是 5
    Node* subLR = subL->_right;      // subLR 是 b子树
    Node* pParent = parent->_parent; // 提前记录10的父节点,以便旋转后连接上层

    // 步骤1:把b子树(subLR)变成parent(10)的左子树
    parent->_left = subLR;
    // b子树可能为空,如果不为空,要把它的父指针指向10
    if (subLR)
    {
        subLR->_parent = parent;
    }

    // 步骤2:把parent(10)变成subL(5)的右子树
    subL->_right = parent;
    parent->_parent = subL;

    // 步骤3:让subL(5)占据原来parent(10)在整棵树中的位置
    if (parent == _root) // 如果10原本是整棵树的根
    {
        _root = subL;
        subL->_parent = nullptr;
    }
    else // 如果10是某个大树的局部子树
    {
        if (pParent->_left == parent) // 10是它父节点的左孩子
        {
            pParent->_left = subL;
        }
        else // 10是它父节点的右孩子
        {
            pParent->_right = subL;
        }
        subL->_parent = pParent;
    }

    // 步骤4:更新平衡因子。
    // 旋转后,10和5的平衡因子都归0,因为它们的左右子树高度都变得一样了
    parent->_bf = 0;
    subL->_bf = 0;
}

4.2 左单旋(RotateL)

场景 :新节点插入到了较高右子树的右侧(简称"右右失衡")。代码中表现为 parent->_bf == 2cur->_bf == 1

旋转操作

  1. 15 的左子树 b 连接到 10 的右子树上。
  2. 10 连接到 15 的左子树上。
  3. 15 成为新的根。

具体的原理我们就不展开了,直接看代码。

代码拆解

cpp 复制代码
void RotateL(Node* parent)
{
    // parent 是 10
    Node* subR = parent->_right;     // subR 是 15
    Node* subRL = subR->_left;       // subRL 是 b子树
    Node* pParent = parent->_parent;

    // 步骤1:把b子树变成10的右子树
    parent->_right = subRL;
    if (subRL)
        subRL->_parent = parent;

    // 步骤2:把10变成15的左子树
    subR->_left = parent;
    parent->_parent = subR;

    // 步骤3:链接上层
    if (parent == _root)
    {
        _root = subR;
        subR->_parent = nullptr;
    }
    else
    {
        if (pParent->_left == parent)
            pParent->_left = subR;
        else
            pParent->_right = subR;
        subR->_parent = pParent;
    }

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

4.3 左右双旋(RotateLR)

单旋可以解决纯粹的"直线型"失衡(左左或右右)。但如果失衡路径是一个"折线"呢?比如新节点插入到了左子树的右侧(简称"左右失衡")。代码中表现为 parent->_bf == -2cur->_bf == 1

这种场景下,如果我们只对 parent 做一次右单旋,会发现树依然是不平衡的。问题的根源在于,对于 parent(如值为 10)来说是左边高,但对于它的左孩子 5 来说,却是右边高。一次旋转解决不了这个问题,需要两次旋转。

操作思想 :先对 parent 的左子树(5)进行一次左单旋 ,让"折线"变成"直线",变成纯粹的左左失衡;然后再对 parent10)进行一次右单旋,就大功告成了。

但这里有一个非常关键的细节:旋转后,各个节点的平衡因子如何调整?

下面我们将a/b/c子树抽象为高度h的AVL 子树进行分析,另外我们需要把b子树的细节进一步展开为8和左子树高度为h-1ef子树,因为我们要以b的父亲5为旋转点进行左单旋,左单旋需要动b树中的左子树。

b子树中新增结点的位置不同,平衡因子更新的细节也不同,通过观察8的平衡因子不同,这里我们要分三个场景讨论。

假设 subLR(节点 8)有左子树 e 和右子树 f,它们的高度都是 h-1。根据新节点插入位置的不同,分为三种情况:
情况一:

新节点插入在 e 子树h >= 1 时)。e 子树高度变为 h,导致 subLR 的平衡因子变为 -1。在进行了先左旋后右旋的操作后,最终平衡因子的结果是:subLR 为 0,subL(节点 5)为 0,parent(节点 10)为 1。

情况二:

新节点插入在 f 子树h >= 1 时)。f 子树高度变为 h,导致 subLR 的平衡因子变为 1。旋转后,最终平衡因子为:subLR 为 0,subL 为 -1,parent 为 0。

情况三:

h == 0 ,即 a, b, c 都是空树,subLR(节点 8)本身就是那个新插入的节点。此时 subLR 的平衡因子为 0。旋转后,所有节点的平衡因子都为 0

你有没有发现一个规律?旋转前 subLR 的平衡因子,恰好可以作为我们判断属于哪种场景的关键 !所以,我们的代码逻辑就变得异常清晰:先记录下 subLR 的平衡因子,然后进行两次旋转,最后根据记录的平衡因子值来更新。

代码拆解

cpp 复制代码
void RotateLR(Node* parent)
{
    // parent 是 10
    Node* subL = parent->_left;      // subL 是 5
    Node* subLR = subL->_right;      // subLR 是 8 (折点)
    
    // 关键:提前记录 subLR 的平衡因子,它决定了旋转后的平衡因子调整策略
    int bf = subLR->_bf;

    // 第一步:对 subL(5) 为根进行左单旋,让树变成左左失衡的直线型
    RotateL(subL);
    // 第二步:对 parent(10) 为根进行右单旋,完成平衡
    RotateR(parent);

    // 第三步:根据记录的 bf 值,精准调整平衡因子
    if (bf == -1) // 场景1:新节点插在e子树
    {
        subLR->_bf = 0;
        subL->_bf = 0;
        parent->_bf = 1;
    }
    else if (bf == 1) // 场景2:新节点插在f子树
    {
        subLR->_bf = 0;
        subL->_bf = -1;
        parent->_bf = 0;
    }
    else if (bf == 0) // 场景3:subLR自己就是新节点
    {
        subLR->_bf = 0;
        subL->_bf = 0;
        parent->_bf = 0;
    }
    else // 程序不应该运行到这里,用来DEBUG
    {
        assert(false);
    }
}

4.4 右左双旋(RotateRL)

理解了左右双旋,右左双旋就完全是对称逻辑了。它适用于"右左失衡"的场景,也就是 parent->_bf == 2cur->_bf == -1

操作思想 :先对 parent 的右子树进行一次右单旋 ,使其变成纯粹的右右失衡(直线型);再对 parent 进行一次左单旋

同样,我们用 subRL(即 parent->_right->_left,值为 12)作为折点,它也有左子树 e 和右子树 f。根据 subRL 的平衡因子,也分三种场景:

  1. 情况1:新节点插在 e 子树subRL 的 bf 为 -1。旋转后:subRL 为 0,subR15)为 1,parent10)为 0。(注意此处的值跟左右双旋场景1不同)
  2. 情况2:新节点插在 f 子树subRL 的 bf 为 1。旋转后:subRL 为 0,subR 为 0,parent 为 -1。
  3. 情况3:subRL 就是新节点subRL 的 bf 为 0。旋转后:所有节点 bf 都为 0。

代码拆解

cpp 复制代码
void RotateRL(Node* parent)
{
    // parent 是 10
    Node* subR = parent->_right;     // subR 是 15
    Node* subRL = subR->_left;       // subRL 是 12 (折点)
    
    // 关键:记录 subRL 的平衡因子
    int bf = subRL->_bf;

    // 第一步:对 subR(15) 为根进行右单旋,变成直线型
    RotateR(subR);
    // 第二步:对 parent(10) 为根进行左单旋
    RotateL(parent);

    // 第三步:根据 bf 值调整平衡因子
    if (bf == 1) // 场景2:新节点插在f子树
    {
        subRL->_bf = 0;
        subR->_bf = 0;
        parent->_bf = -1;
    }
    else if (bf == -1) // 场景1:新节点插在e子树
    {
        subRL->_bf = 0;
        subR->_bf = 1;
        parent->_bf = 0;
    }
    else if (bf == 0) // 场景3:subRL自己就是新节点
    {
        subRL->_bf = 0;
        subR->_bf = 0;
        parent->_bf = 0;
    }
    else
    {
        assert(false);
    }
}

到这里,四种旋转就全部讲完了。你可能会觉得双旋的平衡因子更新有点复杂,但只要记住"根据中间节点(subLR/subRL)旋转前的平衡因子,来分派旋转后的平衡因子"这个核心思想,并在纸上画一画每种情况,就能很快掌握。

现在我们将实现代码的接口写入插入接口代码的实现中(只展示了部分):

cpp 复制代码
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)//右单旋
				{
					RotateR(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == 1)//左单旋
				{
					RotateL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == 1)//左右双旋
				{
					RotateLR(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == -1)//右左双旋
				{
					RotateRL(parent);
				}
				else
				{
					assert(false);
				}
				break;
			}
			else
			{
				assert(false);
			}
		}

5. 辅助功能:查找与平衡检测

5.1 查找(find)

查找操作非常简单,因为它不改变树的结构,所以完全就是标准二叉搜索树的查找逻辑。

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; // 找到空节点,说明不存在
}

5.2 平衡检测(IsBalanceTree)

如何证明我们写的旋转和平衡因子更新逻辑没有漏洞?最可靠的方法就是写一个检查程序,递归地检查树中的每一个节点:

  1. 检查高度差:计算当前节点左右子树的真实高度,看差的绝对值是否小于 2。
  2. 检查平衡因子 :用真实高度差(右-左)和我们记录的 _bf 进行比较,看是否一致。

只要任何一个节点没通过检查,就说明我们的代码有 Bug。

cpp 复制代码
bool _IsBalanceTree(Node* root)
{
    if (root == nullptr)
        return true;

    // 计算左右子树真实高度
    int leftHeight = _Height(root->_left);
    int rightHeight = _Height(root->_right);
    int diff = rightHeight - leftHeight;

    // 检查1:高度差是否超出 [-1, 1] 范围
    if (abs(diff) >= 2)
    {
        cout << root->_kv.first << "高度差异常" << endl;
        return false;
    }

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

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

这里 _Height 函数会递归计算一个节点的高度,即 1 + max(左子树高度, 右子树高度)

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;
	}

6. 删除操作(简介)

AVL树的删除比插入更复杂,因为删除后可能需要从被删除结点的父结点一直向上回溯调整平衡因子,并进行旋转。由于篇幅和难度,本文不作详细实现,有兴趣的同学可以参考《殷人昆 数据结构:用面向对象方法与C++语言描述》中的讲解。


7. 完整代码展示

cpp 复制代码
#pragma once
#include<iostream>
#include<cassert>
#include<cstdlib>
using namespace std;

template<class K,class V>
struct AVLTreeNode
{
	//需要parent指针,更新平衡因子更方便
	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)
	{ }
};

template<class K,class V>
class AVLTree
{
	typedef AVLTreeNode<K,V> Node;
public:

	//右单旋
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		Node* pParent = parent->_parent;

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

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

			subL->_parent = pParent;
		}

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

	//左单旋
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		Node* pParent = parent->_parent;

		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;

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

			subR->_parent = pParent;
		}

		subR->_bf = 0;
		parent->_bf = 0;
	}
	//左右双旋
	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;

		RotateL(subL);
		RotateR(parent);

		if (bf == -1)
		{
			subLR->_bf = 0;
			subL->_bf = 0;
			parent->_bf = 1;
		}
		else if (bf == 1)
		{
			subLR->_bf = 0;
			subL->_bf = -1;
			parent->_bf = 0;
		}
		else if (bf == 0)
		{
			subLR->_bf = 0;
			subL->_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(subR);
		RotateL(parent);

		if (bf == 1)
		{
			subRL->_bf = 0;
			subR->_bf = 0;
			parent->_bf = -1;
		}
		else if (bf == -1)
		{
			subRL->_bf = 0;
			subR->_bf = 1;
			parent->_bf = 0;
		}
		else if (bf == 0)
		{
			subRL->_bf = 0;
			subR->_bf = 0;
			parent->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}
	//插入操作
	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)//右单旋
				{
					RotateR(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == 1)//左单旋
				{
					RotateL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == 1)
				{
					RotateLR(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == -1)
				{
					RotateRL(parent);
				}
				else
				{
					assert(false);
				}
				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;
	}
	
	int Height()
	{
		return _Height(_root);
	}
	void Inorder()
	{
		_Inorder(_root);
	}

	bool IsBalanceTree()
	{
		return _IsBalanceTree(_root);
	}

	int Size()
	{
		return _Size(_root);
	}
private:
	void _Inorder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}

		_Inorder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << endl;
		_Inorder(root->_right);
	}

	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;
	}

	int _Size(Node* root)
	{
		if (root == nullptr)
			return 0;

		return _Size(root->_left) + _Size(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;

		if (abs(diff) >= 2)
		{
			cout << root->_kv.first << "高度差异常" << endl;
			return false;
		}

		if (diff != root->_bf)
		{
			cout << root->_kv.first << "平衡因子异常" << endl;
			return false;
		}

		return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
	}
private:
	Node* _root = nullptr;
};

结语:

今天的内容到这里就结束了,希望你能有所收获~

干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _

相关推荐
小明同学016 小时前
C++后端项目:统一大模型接入 SDK(四)
服务器·开发语言·c++·计算机网络·chatgpt
安妮的小熊呢6 小时前
CRMEB开源商城系统 & 标准版系统(PHP)开发规范
开发语言·javascript·php
子榆.6 小时前
CANN ATC编译器:模型从Python到达芬奇指令走了多远
开发语言·python·neo4j
Dontla6 小时前
Multi-Agent多智能体项目如何从MVP过渡到生产项目?
开发语言
兰令水7 小时前
topcode【随机算法题】【2026.5.20打卡-java版本】
java·开发语言·算法
我还记得那天7 小时前
C语言递归实现汉诺塔问题
c语言·开发语言
不吃土豆的马铃薯7 小时前
Spdlog 入门:日志记录器与日志槽基础详解
服务器·开发语言·c++·c·日志·spdlog
此生决int7 小时前
算法从入门到精通——前缀和
c++·算法·蓝桥杯
凯瑟琳.奥古斯特7 小时前
传输层核心功能解析
开发语言·网络·职场和发展