
系列文章目录
提示:这里是系列文章的专栏
并不喜欢吃鱼的C++专栏
提示:以下是文章目录哦!
文章目录
目录
[一、AVL 树的核心概念](#一、AVL 树的核心概念)
[1.1 什么是 AVL 树?](#1.1 什么是 AVL 树?)
[1.2 平衡因子](#1.2 平衡因子)
[1.3 AVL 树的性质](#1.3 AVL 树的性质)
[1.3.1 AVL树的时间复杂度](#1.3.1 AVL树的时间复杂度)
[1.3.2 为什么 AVL 树的时间复杂度能稳定在 O (logN)?](#1.3.2 为什么 AVL 树的时间复杂度能稳定在 O (logN)?)
[1.1 AVL树节点的实现](#1.1 AVL树节点的实现)
[1.2 AVL树的实现](#1.2 AVL树的实现)
[1.3 AVL树的插入的实现](#1.3 AVL树的插入的实现)
[1.3.1 AVL 树插入一个值的整体流程](#1.3.1 AVL 树插入一个值的整体流程)
[1.3.2 平衡因子更新规则](#1.3.2 平衡因子更新规则)
[1.3.3 平衡因子更新的 3 种情况(一定要理解记忆)](#1.3.3 平衡因子更新的 3 种情况(一定要理解记忆))
[情况 1:更新后 BF = 0](#情况 1:更新后 BF = 0)
[情况 2:更新后 BF = 1 或 -1](#情况 2:更新后 BF = 1 或 -1)
[情况 3:更新后 BF = 2 或 -2(失衡)](#情况 3:更新后 BF = 2 或 -2(失衡))
[1.3.4 最终停止更新的条件](#1.3.4 最终停止更新的条件)
[1.3.5 总结](#1.3.5 总结)
[1.3.6 插入功能代码的实现](#1.3.6 插入功能代码的实现)
[1.3.6.1 空树处理](#1.3.6.1 空树处理)
[1.3.6.2 BST 查找插入位置](#1.3.6.2 BST 查找插入位置)
[1.3.6.3 挂新节点并维护父指针](#1.3.6.3 挂新节点并维护父指针)
[1.3.6.4 核心:更新平衡因子的循环](#1.3.6.4 核心:更新平衡因子的循环)
[1.3.7 AVL树的旋转的功能的实现](#1.3.7 AVL树的旋转的功能的实现)
[1.3.7.1 旋转的核心原则](#1.3.7.1 旋转的核心原则)
[1. 右单旋(解决 LL 型失衡)](#1. 右单旋(解决 LL 型失衡))
[2. 左单旋(解决 RR 型失衡)](#2. 左单旋(解决 RR 型失衡))
[3. 左右双旋(解决 LR 型失衡)](#3. 左右双旋(解决 LR 型失衡))
[4. 右左双旋(解决 RL 型失衡)](#4. 右左双旋(解决 RL 型失衡))
[1.3.8 AVL树查找功能的实现](#1.3.8 AVL树查找功能的实现)
[1.3.9 AVL树平衡的检测(这一点了解思路即可)](#1.3.9 AVL树平衡的检测(这一点了解思路即可))
[1.3.10 AVL树的删除](#1.3.10 AVL树的删除)
前言
提示:这里可以添加本文要记录的大概内容:
这是一篇面向C++ 数据结构入门学习者的保姆级教程,承接《搜索二叉树(BST)全解析》,聚焦自平衡二叉搜索树的经典实现 ------AVL 树。目标是帮读者彻底搞懂 AVL 树的核心原理、平衡机制与工程实现,解决普通 BST 在极端场景下的性能退化问题,为后续学习红黑树、数据库索引等进阶内容打下基础
提示:以下是本篇文章正文内容
一、AVL 树的核心概念
1.1 什么是 AVL 树?
AVL树是最先发明的自平衡二叉查找树,AVL是一颗空树,或者具备下列性质的二叉搜索树:它的左右子树都是AVL树,且左右子树的高度差的绝对值不超过1。AVL树是一颗高度平衡搜索二叉树,通过控制高度差去控制平衡
AVL树得名于它的发明者G.M.Adelson-Velsky和E.M.Landis是两个前苏联的科学家,他们在1962年的论文《An algorithm for the organization of information》中发表了它

以上面这张图为例,我们来看"8"这个结点,他的左子树,也就是左箭头连接的全部节点,从上往下数,高度最高为3,右子树高度最高为2,两遍的高度绝对值为1,没有超过1,并且以任意一个结点来看都是这样的
1.2 平衡因子
每个结点都有一个平衡因子,任何结点的平衡因子=
右子树高度 - 左子树高度,范围∈[-1, 0, 1],AVL树并不是必须要平衡因子,但是有了平衡因子可以更方便我们去进行观察和控制树是否平衡,就像一个风向标一样,也就是说当平衡因子绝对值 > 1时,则会失衡
思考一下为什么AVL树是高度平衡搜索二叉树,要求高度差不超过1,而不是高度差是0呢?0不是更好的平衡吗?画画图分析我们发现,不是不想这样设计,而是有些情况是做不到高度差是0的。比如一棵树是2个结点,4个结点等情况下,高度差最好就是1,无法做到高度差是0


上面这张图红色的节点"10",就是失衡的节点,因为它的平衡因子的绝对值已经超过1了,因此需要进行一定的处理才能使其平衡因子的绝对值降为1或者0,后续我们会讲到旋转,就可以解决这种情况
1.3 AVL 树的性质
1.3.1 AVL树的时间复杂度
查找 / 插入 / 删除的时间复杂度:稳定为 O(logN)
对比普通 BST:普通 BST 最坏会退化成链表,时间复杂度掉到 O(N),而 AVL 树通过平衡约束,彻底避免了这种情况
1.3.2 为什么 AVL 树的时间复杂度能稳定在 O (logN)?
核心原因: 
这个约束,让 AVL 树的高度被牢牢限制住了。我们可以用数学推导来证明它的高度上限: 设 AVL 树高度为 h,节点数最少为 N(h),那么:
一、先搞懂一个核心问题:我们为什么要做这个推导?
这里的推理,本质上是在回答:AVL 树最多能 "瘦" 到什么程度?(也就是高度最高能到多少?) 只要我们能算出 "节点数为 N 时,AVL 树的最大高度 h 是多少",就能知道它的时间复杂度上限了
二、第一步:定义「节点数最少的 AVL 树」
我们先定义一个函数: N(h) = 高度为 h 的 AVL 树,最少需要多少个节点?
为什么要找 "最少节点数"? 因为节点越少,树就越 "瘦",高度就越高。(这样我们就能花更多的节点专注于树的高度上,而不是丰满度上)只要我们能算出这个 "最瘦" 的 AVL 树的高度上限,那所有 AVL 树的高度都不会超过它
三、第二步:用平衡规则,写出递推公式
AVL 树的核心规则:任意节点的左右子树高度差 ≤ 1 。 我们看一棵高度为 h 的 AVL 树,它的根节点的两个子树,高度只能是这两种情况:
- 一个子树高度是
h-1,另一个也是h-1(完全平衡) - 一个子树高度是
h-1,另一个是h-2(刚好不失衡的临界状态)
我们要找的是节点最少 的情况,所以会选第二种情况(h-1 和 h-2),因为这样代表花了更多节点在堆高度上,而不是保持平衡上,所以高度为 h 的 AVL 树,最少节点数满足: N(h) = N(h-1) + N(h-2) + 1(这里的1为根节点)
这里的三个部分分别是:
N(h-1):高度为h-1的子树,最少节点数N(h-2):高度为h-2的子树,最少节点数+1:加上根节点本身
四、第三步:算几个小例子,验证一下
我们先算几个简单的情况:
h=0(空树):N(0) = 0h=1(只有根节点):N(1) = 1h=2:N(2) = N(1) + N(0) + 1 = 1 + 0 + 1 = 2(一个根节点 + 一个子节点,刚好平衡)h=3:N(3) = N(2) + N(1) + 1 = 2 + 1 + 1 = 4(根节点 + 一个 h=2 的子树 + 一个 h=1 的子树,刚好不失衡)h=4:N(4) = N(3) + N(2) + 1 = 4 + 2 + 1 = 7
这个数列 0, 1, 2, 4, 7, 12, ... 是不是和斐波那契数列长得很像?它的增长速度,就是斐波那契数列的增长速度
五、第四步:从递推式,推导出 h 和 N 的关系
这一步有点复杂,我们不过多解释,直接来看结论,大家主要记得是用什么推过来的就行了**,** 我们的 N(h) 数列,本质上是斐波那契数列的变形
六、第五步:用这个结论,解释时间复杂度
我们推导出来的结论是: 节点数为 N 的 AVL 树,最大高度
h ≤ 1.44 * log₂(N)
这意味着:
- 不管数据怎么分布,AVL 树的高度,始终是
O(logN)级别的 - 而查找、插入、删除这些操作,都只需要从根走到叶子,最多走
h步 - 所以,AVL 树的所有操作,时间复杂度都是稳定的
O(logN),绝不会像普通 BST 那样退化成O(N)
二、AVL树的实现
1.1 AVL树节点的实现
cpp
// 这是C++的模板语法,K代表键(key)的类型,V代表值(value)的类型
// 这样写,你的AVL树就可以存任意类型的键值对,比如int-int、string-int等等
template<class K, class V>
// 定义AVL树的节点结构体,名字叫AVLTreeNode
struct AVLTreeNode
{
// ---------------------- 节点存储的数据 ----------------------
// pair是C++标准库的键值对类型,_kv用来存这个节点的键和值
// 比如你插入(5, "apple"),K就是int,V就是string,_kv就存这两个值
pair<K, V> _kv;
// ---------------------- 二叉树的基础指针 ----------------------
// 指向左孩子节点的指针
// 所有值比当前节点小的键,都会存在左子树里
AVLTreeNode<K, V>* _left;
// 指向右孩子节点的指针
// 所有值比当前节点大的键,都会存在右子树里
AVLTreeNode<K, V>* _right;
// 指向父节点的指针
// 这是AVL树实现的关键!因为插入/删除节点后,我们要沿着父节点一路往上更新平衡因子,没有这个指针会非常麻烦
AVLTreeNode<K, V>* _parent;
// ---------------------- AVL树的核心字段:平衡因子 ----------------------
// _bf 全称是 balance factor,也就是平衡因子
// 它的定义是:左子树的高度 - 右子树的高度
// AVL树的规则:所有节点的_bf绝对值必须≤1,否则树就失衡了,需要旋转调整
// 新节点刚创建时,没有子树,所以_bf初始化为0
int _bf;
// ---------------------- 节点的构造函数 ----------------------
// 当我们创建一个新节点时,会调用这个构造函数
// 参数kv就是要存的键值对,比如make_pair(3, "banana")
AVLTreeNode(const pair<K, V>& kv)
: _kv(kv) // 把传入的键值对存到节点里
, _left(nullptr) // 左孩子初始化为空指针(没有左孩子)
, _right(nullptr) // 右孩子初始化为空指针(没有右孩子)
, _parent(nullptr) // 父节点初始化为空指针(还没挂到树上)
, _bf(0) // 平衡因子初始化为0
{
// 构造函数体是空的,所有初始化都在上面的初始化列表里完成了
}
};
上面备注了详细的注释,不懂的可以仔细看看,这里还是列个表总结一下,并且讲解一下pair这个类型,这里的节点里的数据类型也可以不是一个pair类型,可以是我们常见的单int类型
| 字段 | 作用 | 理解 |
| _kv | 存储键值对数据 | 节点的 "内容",比如你要存的数字和对应的字符串 |
| _left | 指向左孩子 | 所有比当前节点小的数据,都存在左边 |
| _right | 指向右孩子 | 所有比当前节点大的数据,都存在右边 |
| _parent | 指向父节点 | 插入 / 删除后,要沿着父节点往上检查平衡,这是 AVL 树的关键 |
| _bf | 平衡因子 | 用来判断树有没有歪掉,歪了就要旋转调整 |
|---|

std::pair 是一个"值对"模板类 template <class T1, class T2> struct pair,它可以把两个不同类型(T1 和 T2)的值打包成一个整体, 这两个值可以通过它的公有成员 `first` 和 `second` 来访问
std::pair 就是一个 "打包工具",能把两个东西绑在一起变成一个整体。 比如你想存 "学号 + 姓名",就可以用 pair<int, string>,把 2025001 和 "张三" 打包成一个变量
cpp
#include <utility> // 必须包含这个头文件才能用pair
#include <iostream>
#include <string>
int main() {
// 1. 创建一个pair:int + string
std::pair<int, std::string> student;
// 2. 给它的两个成员赋值
student.first = 2025001; // 第一个值,对应模板里的T1
student.second = "张三"; // 第二个值,对应模板里的T2
// 3. 访问它的值
std::cout << "学号:" << student.first << std::endl;
std::cout << "姓名:" << student.second << std::endl;
// 也可以用make_pair快速创建
auto p = std::make_pair(3.14, "圆周率");
std::cout << p.first << " = " << p.second << std::endl;
return 0;
}
为什么 AVL 树里要用它?
之前写的 AVL 树节点里,用了 pair<K, V> _kv,就是把 "键(key)和值(value)" 打包在一起: K 是键的类型(比如 int,用来排序和查找) V 是值的类型(比如 string,用来存数据) 这样一个节点就能同时存 "用来排序的 key" 和 "要保存的数据 value",非常方便
1.2 AVL树的实现
cpp
// 这是C++的模板声明,K代表键(key)的类型,V代表值(value)的类型
// 这样写,你的AVL树就可以支持任意类型的键值对,比如int-int、string-int等
template<class K, class V>
// 定义AVL树的主类,名字叫AVLTree
class AVLTree
{
// typedef 是给类型起别名
// 这里把 AVLTreeNode<K, V> 这个节点类型,起了个短别名叫 Node
// 后面代码里写 Node* 就等价于写 AVLTreeNode<K, V>*,简化代码书写
typedef AVLTreeNode<K, V> Node;
public:
// 这里是类的公有成员,后面我们会在这里写插入、删除、查找等对外接口
// 现在先用//...占位,后续可以继续补充实现
//...
private:
// 这里是类的私有成员,外部不能直接访问,只能通过类的成员函数操作
// _root 是AVL树的根节点指针,初始化为nullptr(空树)
// 整个AVL树的所有节点,都是通过根节点来访问的
Node* _root = nullptr;
};
由于注释很详细,这里不展开赘述
1.3 AVL树的插入的实现
1.3.1 AVL 树插入一个值的整体流程
-
按二叉搜索树(BST)规则插入新节点 比当前节点小往左走,比当前节点大往右走,找到空位置插入
-
更新平衡因子 插入新节点后,只会影响从新节点到根节点这条路径上的祖先节点 。 我们需要沿着这条路径向上更新所有祖先的平衡因子
-
判断是否失衡
- 如果更新过程中没有出现不平衡,插入直接结束。
- 如果更新过程中出现不平衡,对不平衡的子树进行旋转。
- 旋转后子树恢复平衡,高度也会恢复到插入前,不会再影响上层,插入结束
1.3.2 平衡因子更新规则
1. 平衡因子定义

2. 更新原则
- 只有子树高度发生变化,才会影响当前节点的平衡因子
- 插入节点会导致子树高度增加:
- 新节点插在 parent 左子树 → parent 的平衡因子 −−
- 新节点插在 parent 右子树 → parent 的平衡因子 ++
1.3.3 平衡因子更新的 3 种情况(一定要理解记忆)
情况 1:更新后 BF = 0
- 变化:
-1 → 0或1 → 0 - 含义: 之前子树一边高一边低, 新节点插在了矮的一边, 现在两边一样高了。
- 结果: 子树高度不变 → 不会影响上层 → 更新停止,插入结束

更新到中间结点,3为根的子树高度不变,不会影响上一层,更新结束
情况 2:更新后 BF = 1 或 -1
- 变化:
0 → 1或0 → -1 - 含义: 之前两边一样高, 新节点插在某一边, 现在一边高一边低,但仍然合法。
- 结果: 子树高度变高了 → 会影响上层 → 必须继续向上更新

情况 3:更新后 BF = 2 或 -2(失衡)
- 变化:
1 → 2或-1 → -2 - 含义: 之前一边已经更高, 新节点又插在了更高的一边, 导致平衡被破坏。
- 结果: 必须旋转调整 旋转目标:
- 让子树恢复平衡
- 让子树高度恢复到插入之前 旋转完成后 → 不再影响上层 → 更新停止,插入结束

更新到10结点,平衡因子为2,10所在的子树已经不平衡,需要旋转处理
1.3.4 最终停止更新的条件
满足任意一个就停止向上更新:
- 更新到 根节点
- 更新后 BF = 0
- 更新后 BF = ±2(旋转后停止)
1.3.5 总结
插入按 BST 规则走,插入后向上更新平衡因子:
- BF=0 → 停
- BF=±1 → 继续向上
- BF=±2 → 旋转 → 停
接下来是插入代码的实现,如果上面你搞懂了,下面也不是很难,看一下就懂了
1.3.6 插入功能代码的实现
----------------------------------------------接下来是插入功能的实现------------------------------------------------
cpp
// 2.2.3 插入结点及更新平衡因子的代码实现
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. 按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;
}
// 等于当前节点 → 不允许重复插入,直接返回false
else
{
return false;
}
}
// 3. 创建新节点,挂到父节点上
cur = new Node(kv);
if (parent->_kv.first < kv.first)
{
// 新节点比父节点大 → 挂在父节点的右边
parent->_right = cur;
}
else
{
// 新节点比父节点小 → 挂在父节点的左边
parent->_left = cur;
}
cur->_parent = parent; // 新节点的父指针指向parent
// --------------------------
// 4. 关键:更新平衡因子
// 从cur的父节点开始,一路向上更新到根
// --------------------------
while (parent)
{
// 4.1 根据新节点的位置,更新父节点的平衡因子
if (cur == parent->_left)
{
// 新节点插在左子树 → 左子树高度+1 → 平衡因子-1
parent->_bf--;
}
else
{
// 新节点插在右子树 → 右子树高度+1 → 平衡因子+1
parent->_bf++;
}
// 4.2 根据更新后的平衡因子,判断是否需要继续向上更新
if (parent->_bf == 0)
{
// 情况1:平衡因子变为0 → 子树高度不变,不会影响上层
// 停止更新,插入结束
break;
}
else if (parent->_bf == 1 || parent->_bf == -1)
{
// 情况2:平衡因子为±1 → 子树高度+1,会影响上层
// 继续向上更新
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
// 情况3:平衡因子为±2 → 树失衡了,需要旋转调整
// 旋转处理逻辑后续实现,这里先break
break;
}
else
{
// 理论上不会走到这里,防止出现异常情况
assert(false);
}
}
return true;
}
1.3.6.1 空树处理

如果树是空的,直接把新节点设为根节点即可,没有父节点,也不需要更新平衡因子
1.3.6.2 BST 查找插入位置

- 这部分和普通 BST 的插入逻辑完全一样,核心是找到第一个空的位置
- 用
parent记录 cur 的父节点,方便后续挂新节点 - 如果遇到相同的 key,直接返回 false(不允许重复 key)
1.3.6.3 挂新节点并维护父指针

- 根据
parent和kv的大小关系,决定新节点挂在左边还是右边 - 关键是要设置
cur->_parent = parent,为后续更新平衡因子做准备
1.3.6.4 核心:更新平衡因子的循环

- 循环的核心逻辑就是你之前整理的三种停止条件
- 循环里用
cur和parent一路向上走,每次更新父节点的平衡因子 - 遇到
±2时 break,接下来就要写旋转的代码了
----------------------------------------------接下来是旋转功能的实现------------------------------------------------
这里才是最有难度的一点,一定要理解,而非死记
1.3.7 AVL树的旋转的功能的实现
1.3.7.1 旋转的核心原则
旋转操作必须同时满足两个条件:
- 保持 BST 的有序性:旋转后树的中序遍历序列不变,仍满足 "左子树 < 根 < 右子树" 的规则
- 恢复平衡并降低高度:让失衡的子树重新满足 AVL 平衡条件(平衡因子绝对值≤1),同时将子树高度恢复到插入前的水平,避免继续影响上层节点
AVL 树的旋转分为四种:右单旋、左单旋、左右双旋、右左双旋,其中单旋是基础,双旋是两次单旋的组合
单旋是解决 "问题出在同侧" 的失衡(LL/RR),也是双旋的基础。
1. 右单旋(解决 LL 型失衡)
触发条件
失衡节点 parent 的平衡因子为 -2,且它的左孩子 subL 的平衡因子为 -1(或 0)。 说明:左子树的左分支太高了,需要向右 "掰直"。
核心逻辑(3 步走)
- 让左孩子上位 :
subL成为新的根节点,替代parent。 - 过继中间子树 :把
subL的右子树subLR,挂到parent的左孩子位置。 - 原根退位 :
parent成为subL的右孩子。
关键效果
- 不破坏 BST 规则:中序遍历顺序不变。
- 平衡因子恢复:
parent和subL的 BF 都会变成 0。 - 高度恢复:子树高度回到插入前,不会再影响上层。
特殊情况:

各种情况:

情况 1 - 插入前 a/b/c 高度 h = 0
这是最简单的情况,所有子树都是空树(高度为 0)。
步骤拆解
-
初始状态
- 节点 10 的 BF=-1,节点 5 的 BF=0,
a/b/c都是空树(h=0) - 结构:10 为根,左孩子是 5,5 的左右子树都是空树,10 的右子树
c是空树。
- 节点 10 的 BF=-1,节点 5 的 BF=0,
-
插入新节点后(失衡)
- 在
a(5 的左子树)插入新节点(值 = 1),a的高度从 0 变成 1 - 更新平衡因子:节点 5 的 BF 从 0→-1,节点 10 的 BF 从 - 1→-2(失衡)
- 在
-
右单旋操作
- 把节点 5 "抬上来" 成为新根,节点 10 变成它的右孩子
- 节点 5 原来的右子树
b(空树),变成节点 10 的左子树
-
最终效果
- 节点 5 的 BF=0,节点 10 的 BF=0,树恢复平衡
- 整体高度回到插入前的
h+2 = 0+2 = 2

情况 2 - 插入前 a/b/c 高度 h = 1
这是高度为 1 的子树,包含叶子节点,更接近真实场景。
步骤拆解
-
初始状态
- 节点 10 的 BF=-1,节点 5 的 BF=0
a是节点 1(h=1),b是节点 8(h=1),c是节点 15(h=1)
-
插入新节点后(失衡)
- 在
a(节点 1 的左子树)插入新节点(值 =-3),a的高度从 1 变成 2 - 更新平衡因子:节点 1 的 BF=0,节点 5 的 BF 从 0→-1,节点 10 的 BF 从 - 1→-2(失衡)
- 在
-
右单旋操作
- 节点 5 成为新根,节点 10 变成它的右孩子
- 节点 5 的右子树
b(节点 8),变成节点 10 的左子树
-
最终效果
- 节点 5 的 BF=0,节点 1 的 BF=-1,节点 10 的 BF=0
- 所有节点的 BF 绝对值≤1,树恢复平衡
- 整体高度回到插入前的
h+2 = 1+2 = 3

情况 3 - 插入前 a/b/c 高度 h = 2(AVL 子树)
这是高度为 2 的 AVL 子树,用来证明 "无论子树是什么形态的 AVL 树,右单旋都能生效"。
关键说明
- 图中
x/y/z代表高度为 2 的 AVL 子树的三种形态(满二叉树、单分支等) - 文字部分解释:
a必须是x型子树,因为只有这种形态插入节点后,才会直接引发上层节点失衡,而不是自身先旋转。 - 所有可能的组合:
b和c可以是x/y/z的任意组合,共 3×3×4=36 种场景,右单旋都能处理。
这里来解释一下3*3*4是怎么来的,由于子树b和c上面三种可以任选一种,因此就是3*3,而我插入一个节点由于只能插入子树a上,因此可以插入左子树的左右两边,或者右子树的左右两边,也就是4种,因此是3*3*4共36种
核心逻辑
- 无论
a/b/c是什么形态的 AVL 子树,右单旋的操作都是固定的:subL(节点 5)上位为新根subL的右子树b过继给parent(节点 10)parent成为subL的右孩子
- 操作后,树必然恢复平衡,且高度回到插入前。

图 4:情况 4 - 插入前 a/b/c 高度 h = 3(AVL 子树)
这是高度为 3 的 AVL 子树,是这组图的终极通用证明。
关键说明
- 图中
x/y-C代表高度为 3 的 AVL 子树的 14 种形态 - 文字部分解释:所有可能的组合有 5400 种,但无论哪种,右单旋的逻辑都不变。
- 这里用抽象的矩形表示子树,说明:我们不需要关心子树内部的细节,只要它是高度为 h 的 AVL 树,右单旋就一定有效。
核心结论
- 无论
h是 0/1/2/3/...,右单旋的操作流程完全一致:subL上位subLR过继parent退位
- 最终结果也一致:树恢复平衡,高度回到
h+2,不会再影响上层节点。

----------------------------------------------接下来右单旋代码的实现------------------------------------------------

cpp
// 右单旋:解决 LL 型失衡(parent 的平衡因子为 -2)
// 参数 parent:失衡的节点(要被旋转下去的节点)
template<class K, class V>
void AVLTree<K, V>::RotateR(Node* parent)
{
// ---------------------- 1. 准备工作:获取关键节点 ----------------------
// subL:parent 的左孩子,也就是要上位成为新根的节点
Node* subL = parent->_left;
// subLR:subL 的右孩子,中间子树,后续要过继给 parent
Node* subLR = subL->_right;
// ---------------------- 2. 过继中间子树 subLR 给 parent ----------------------
// 把 parent 的左孩子,指向 subLR(相当于 subL 把自己的右孩子"过继"给 parent)
parent->_left = subLR;
// 如果 subLR 不为空,要把它的父指针改成 parent(维护双向指针)
if (subLR)
subLR->_parent = parent;
// ---------------------- 3. 让 subL 上位,parent 退位 ----------------------
// 先记录 parent 的父节点(后续要让上层节点指向 subL)
Node* parentParent = parent->_parent;
// 让 subL 的右孩子指向 parent(parent 变成 subL 的右子树)
subL->_right = parent;
// 让 parent 的父指针指向 subL
parent->_parent = subL;
// ---------------------- 4. 处理上层节点的指向 ----------------------
// parent 可能是整棵树的根,也可能是某个局部子树的根
if (parentParent == nullptr)
{
// 情况1:parent 原来是整棵树的根,旋转后 subL 成为新的根
_root = subL;
// 新根的父节点为空
subL->_parent = nullptr;
}
else
{
// 情况2:parent 是局部子树,需要修改 parentParent 的孩子指针
if (parent == parentParent->_left)
{
// 如果 parent 是 parentParent 的左孩子,就把 parentParent 的左孩子改成 subL
parentParent->_left = subL;
}
else
{
// 如果 parent 是 parentParent 的右孩子,就把 parentParent 的右孩子改成 subL
parentParent->_right = subL;
}
// 让 subL 的父指针指向 parentParent
subL->_parent = parentParent;
}
// ---------------------- 5. 更新平衡因子 ----------------------
// 右单旋后,parent 和 subL 的平衡因子都会变成 0
parent->_bf = 0;
subL->_bf = 0;
}
关键细节说明
-
为什么要维护
_parent指针? 因为 AVL 树是双向指针结构,修改了_left/_right之后,必须同步修改_parent,否则会出现 "孤儿节点" 或指针混乱。 -
为什么要处理
parentParent? 如果parent是整棵树的根,旋转后要把_root改成subL;如果是局部子树,要让它的父节点指向新的根subL,否则树会断链。 -
为什么直接把 BF 设为 0? 在 LL 型失衡的右单旋场景下,旋转后
parent和subL的平衡因子一定会变成 0,所以可以直接赋值,不用复杂计算。
2. 左单旋(解决 RR 型失衡)
触发条件
失衡节点 parent 的平衡因子为 +2,且它的右孩子 subR 的平衡因子为 +1(或 0)。 说明:右子树的右分支太高了,需要向左 "掰直"。
核心逻辑(3 步走)
- 让右孩子上位 :
subR成为新的根节点,替代parent。 - 过继中间子树 :把
subR的左子树subRL,挂到parent的右孩子位置。 - 原根退位 :
parent成为subR的左孩子。
关键效果
- 不破坏 BST 规则:中序遍历顺序不变。
- 平衡因子恢复:
parent和subR的 BF 都会变成 0。 - 高度恢复:子树高度回到插入前,不会再影响上层。
特殊情况:

第 1 步:失衡发生(插入后)
- 初始状态:
parent(10)的 BF=1(右子树比左子树高 1)subR(15)的 BF=0(左右子树一样高)
- 插入操作:在
subR的右子树a中插入新节点,a的高度从h变成h+1 - 结果:
subR的 BF 从 0→+1parent的 BF 从 1→+2(失衡,RR 型)
第 2 步:左单旋操作(核心)
- 让
subR上位 :subR(15)成为新的根节点,替代parent(10) - 过继中间子树
subRL:把subR的左子树subRL,挂到parent(10)的右孩子位置 - 让
parent退位 :parent(10)变成subR(15)的左孩子
第 3 步:旋转后效果
- 新的根是
subR(15),左孩子是parent(10),右孩子是a parent(10)的右孩子是subRL,BF 恢复为 0subR(15)的 BF 恢复为 0- 树的整体高度回到插入前的
h+2,不会再影响上层节点
一般情况和右单旋的一样,图是对称的,如果你懂右单旋,一定就懂左单旋
----------------------------------------------接下来左单旋代码的实现------------------------------------------------
cpp
// 左单旋:解决 RR 型失衡(失衡节点 parent 的平衡因子为 +2)
// 核心作用:把失衡的"右右型"子树,向左旋转,恢复平衡
template<class K, class V>
void AVLTree<K, V>::RotateL(Node* parent)
{
// ---------------------- 1. 准备关键节点 ----------------------
// subR:失衡节点 parent 的右孩子,旋转后会成为新的根节点
Node* subR = parent->_right;
// subRL:subR 的左孩子(中间子树,后续要过继给 parent)
// 这部分子树的值介于 parent 和 subR 之间,旋转后必须保持在中间
Node* subRL = subR->_left;
// ---------------------- 2. 过继中间子树 subRL 给 parent ----------------------
// 让 parent 的右孩子,指向 subRL(相当于 subR 把自己的左孩子"过继"给 parent)
parent->_right = subRL;
// 如果 subRL 不为空,要同步修改它的父指针,指向 parent
if (subRL)
subRL->_parent = parent;
// ---------------------- 3. 让 subR 上位,parent 退位 ----------------------
// 先记录 parent 的父节点(后续要让上层节点指向新根 subR)
Node* parentParent = parent->_parent;
// 让 subR 的左孩子指向 parent(parent 变成 subR 的左子树)
subR->_left = parent;
// 让 parent 的父指针指向 subR,维护双向指针关系
parent->_parent = subR;
// ---------------------- 4. 处理上层节点的指向(关键细节) ----------------------
// parent 可能是整棵树的根,也可能是某个局部子树的根,需要分情况处理
if (parentParent == nullptr)
{
// 情况1:parent 原来是整棵树的根,旋转后 subR 成为新的根
_root = subR;
// 新根的父节点必须为空
subR->_parent = nullptr;
}
else
{
// 情况2:parent 是局部子树,需要修改 parentParent 的孩子指针,让它指向 subR
if (parent == parentParent->_left)
{
// 如果 parent 是 parentParent 的左孩子,就把 parentParent 的左孩子改成 subR
parentParent->_left = subR;
}
else
{
// 如果 parent 是 parentParent 的右孩子,就把 parentParent 的右孩子改成 subR
parentParent->_right = subR;
}
// 让 subR 的父指针指向 parentParent,维护双向指针关系
subR->_parent = parentParent;
}
// ---------------------- 5. 更新平衡因子 ----------------------
// RR 型失衡经过左单旋后,parent 和 subR 的平衡因子都会变成 0
// 因为子树高度恢复到了插入前的水平,不再失衡
parent->_bf = 0;
subR->_bf = 0;
}
双旋是解决 "问题出在异侧" 的失衡(LR/RL),本质是两次单旋的组合。
3. 左右双旋(解决 LR 型失衡)
触发条件
失衡节点 parent 的平衡因子为 -2,且它的左孩子 subL 的平衡因子为 +1。 说明:左子树的右分支太高了,直接右单旋会破坏结构,需要先把它变成 LL 型,再右单旋。
核心逻辑(两步走)
- 先对 subL 做左单旋:把 "左子树的右分支过高",变成 "左子树的左分支过高"(即把 LR 型转成 LL 型)。
- 再对 parent 做右单旋:用右单旋解决 LL 型失衡。
关键效果
- 两次旋转后,树恢复平衡,且高度回到插入前。
- 中间节点的平衡因子需要特殊处理,不一定都是 0

情况 1:h=0(插入前子树高度为 0)
- 初始状态
- 节点 10(parent)的 BF=-1,节点 5(subL)的 BF=0
- 5 的右子树是空(h=0),10 的右子树是空
- 插入后失衡
- 在 5 的右子树插入节点 8,5 的 BF 从 0→+1,10 的 BF 从 - 1→-2(LR 型失衡)
- 第一步:对 5 做左单旋
- 把节点 8 抬上来,节点 5 变成它的左孩子,节点 8 成为新的 subL
- 此时结构变成了典型的 LL 型失衡
- 第二步:对 10 做右单旋
- 节点 8 成为新根,节点 10 变成它的右孩子,恢复平衡
- 最终节点 5、8、10 的 BF 都为 0

情况 2:h=1(插入前子树高度为 1)
- 初始状态
- 节点 10(parent)的 BF=-1,节点 5(subL)的 BF=0
- 5 的左右子树是 1 和 8(h=1),10 的右子树是 15(h=1)
- 插入后失衡
- 在 8 的右子树插入节点 9,8 的 BF 从 0→+1,5 的 BF 从 0→+1,10 的 BF 从 - 1→-2(LR 型失衡)
- 第一步:对 5 做左单旋
- 把节点 8 抬上来,节点 5 变成它的左孩子,节点 8 成为新的 subL
- 此时结构变成了典型的 LL 型失衡
- 第二步:对 10 做右单旋
- 节点 8 成为新根,节点 10 变成它的右孩子,恢复平衡
- 最终节点 5 的 BF=-1,节点 8 和 10 的 BF 为 0

这张图是左右双旋的终极通用模型,证明无论子树的初始高度 h 是多少,也无论新节点插在 subLR 的左还是右分支,两次旋转都能完美恢复平衡。
分场景拆解(3 种插入位置)
场景 1:新节点插在 subLR 的左子树 e 中
- 插入后,subLR 的 BF 从 0→-1,subL 的 BF 从 0→+1,parent 的 BF 从 - 1→-2
- 对 subL 做左单旋:subLR 上位为新 subL,subL 变成它的左孩子
- 对 parent 做右单旋:subLR 上位为新根,parent 变成它的右孩子
- 最终 BF:subL (5)=0,subLR (8)=0,parent (10)=-1
场景 2:新节点插在 subLR 的右子树 f 中
- 插入后,subLR 的 BF 从 0→+1,subL 的 BF 从 0→+1,parent 的 BF 从 - 1→-2
- 对 subL 做左单旋:subLR 上位为新 subL,subL 变成它的左孩子
- 对 parent 做右单旋:subLR 上位为新根,parent 变成它的右孩子
- 最终 BF:subL (5)=-1,subLR (8)=0,parent (10)=0
场景 3:subLR 本身就是新节点(h=0)
- 插入后,subL 的 BF 从 0→+1,parent 的 BF 从 - 1→-2
- 对 subL 做左单旋:subLR 上位为新 subL,subL 变成它的左孩子
- 对 parent 做右单旋:subLR 上位为新根,parent 变成它的右孩子
- 最终 BF:subL (5)=0,subLR (8)=0,parent (10)=0
----------------------------------------------接下来左右单旋代码实现------------------------------------------------
cpp
// 左右双旋:解决 LR 型失衡(失衡节点 parent 的平衡因子为 -2,且其左孩子 subL 的平衡因子为 +1)
// 核心逻辑:先对 subL 做左单旋(把 LR 型转成 LL 型),再对 parent 做右单旋
template<class K, class V>
void AVLTree<K, V>::RotateLR(Node* parent)
{
// ---------------------- 1. 准备关键节点 ----------------------
// subL:失衡节点 parent 的左孩子(第一次旋转的对象)
Node* subL = parent->_left;
// subLR:subL 的右孩子(两次旋转后会成为新的根节点)
Node* subLR = subL->_right;
// 提前保存 subLR 旋转前的平衡因子,用于后续更新
// 因为两次旋转会修改 subLR 的值,所以必须提前存下来
int bf = subLR->_bf;
// ---------------------- 2. 第一次旋转:对 subL 做左单旋 ----------------------
// 把 LR 型失衡(左子树的右分支过高),转成 LL 型失衡(左子树的左分支过高)
RotateL(parent->_left);
// ---------------------- 3. 第二次旋转:对 parent 做右单旋 ----------------------
// 用右单旋解决 LL 型失衡,恢复整棵树的平衡
RotateR(parent);
// ---------------------- 4. 根据 bf 的值,更新三个关键节点的平衡因子 ----------------------
// bf 是 subLR 旋转前的平衡因子,它的值决定了三个节点最终的 BF 状态
if (bf == 0)
{
// 情况1:新节点就是 subLR 本身(h=0的场景)
// 旋转后 subL、subLR、parent 的 BF 都为 0
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 0;
}
else if (bf == -1)
{
// 情况2:新节点插在 subLR 的左子树中
// 旋转后 parent 的右子树更高,BF=1;subL 和 subLR 的 BF 为 0
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 1;
}
else if (bf == 1)
{
// 情况3:新节点插在 subLR 的右子树中
// 旋转后 subL 的左子树更高,BF=-1;subLR 和 parent 的 BF 为 0
subL->_bf = -1;
subLR->_bf = 0;
parent->_bf = 0;
}
else
{
// 理论上不会走到这里,防止异常情况
assert(false);
}
}
4. 右左双旋(解决 RL 型失衡)
触发条件
失衡节点 parent 的平衡因子为 +2,且它的右孩子 subR 的平衡因子为 -1
说明:右子树的左分支太高了,直接左单旋会破坏结构,需要先把它变成 RR 型,再左单旋。
核心逻辑(两步走)
- 先对
subR做右单旋:把 "右子树的左分支过高",变成 "右子树的右分支过高"(即把 RL 型转成 RR 型) - 再对
parent做左单旋:用左单旋解决 RR 型失衡
关键效果
- 两次旋转后,树恢复平衡,且高度回到插入前。
- 中间节点的平衡因子需要特殊处理,不一定都是 0。

1. 场景 1:新节点插在 subRL 的左子树 e 中
- 失衡发生
- 初始:
parent(10)BF=1,subR(15)BF=0,subRL(12)BF=0 - 在
e中插入节点,subRL的 BF 从 0→-1,subR的 BF 从 0→-1,parent的 BF 从 1→+2(RL 型失衡)
- 初始:
- 第一步:对
subR做右单旋subRL(12)上位成为新的subR,subR(15)变成它的右孩子- 此时结构变成典型的 RR 型失衡
- 第二步:对
parent做左单旋subRL(12)上位成为整棵树的新根,parent(10)变成它的左孩子,subR(15)变成它的右孩子
- 最终 BF 状态 :
parent(10)BF=1,subRL(12)BF=0,subR(15)BF=0
2. 场景 2:新节点插在 subRL 的右子树 f 中
- 失衡发生
- 在
f中插入节点,subRL的 BF 从 0→+1,subR的 BF 从 0→-1,parent的 BF 从 1→+2(RL 型失衡)
- 在
- 第一步:对
subR做右单旋subRL(12)上位成为新的subR,subR(15)变成它的右孩子
- 第二步:对
parent做左单旋subRL(12)上位成为新根,parent(10)变成它的左孩子,subR(15)变成它的右孩子
- 最终 BF 状态 :
parent(10)BF=0,subRL(12)BF=0,subR(15)BF=-1
3. 场景 3:subRL 本身就是新节点(h=0 的情况)
- 失衡发生
- 直接插入
subRL(12),subR的 BF 从 0→-1,parent的 BF 从 1→+2(RL 型失衡)
- 直接插入
- 第一步:对
subR做右单旋subRL(12)上位成为新的subR,subR(15)变成它的右孩子
- 第二步:对
parent做左单旋subRL(12)上位成为新根,parent(10)变成它的左孩子,subR(15)变成它的右孩子
- 最终 BF 状态 :
parent(10)BF=0,subRL(12)BF=0,subR(15)BF=0
----------------------------------------------接下来右左单旋代码实现------------------------------------------------
cpp
// 右左双旋:解决 RL 型失衡(parent BF=+2,subR BF=-1)
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. 先对 subR 做右单旋,把 RL 型转成 RR 型
RotateR(parent->_right);
// 2. 再对 parent 做左单旋,解决 RR 型失衡
RotateL(parent);
// 3. 根据 bf 的值更新平衡因子
if (bf == 0)
{
// 新节点就是 subRL 本身
parent->_bf = 0;
subRL->_bf = 0;
subR->_bf = 0;
}
else if (bf == 1)
{
// 新节点插在 subRL 的右子树 f 中
parent->_bf = 0;
subRL->_bf = 0;
subR->_bf = -1;
}
else if (bf == -1)
{
// 新节点插在 subRL 的左子树 e 中
parent->_bf = 1;
subRL->_bf = 0;
subR->_bf = 0;
}
else
{
assert(false);
}
}
1.3.8 AVL树查找功能的实现
太简单了,不过多解释
cpp
// 查找函数:根据 key 在 AVL 树中查找对应的节点
// 参数 key:要查找的键
// 返回值:找到的节点指针;没找到返回 nullptr
template<class K, class V>
Node* AVLTree<K, V>::Find(const K& key)
{
// cur:从根节点开始遍历
Node* cur = _root;
// 只要当前节点不为空,就继续查找
while (cur)
{
// 如果当前节点的 key 小于目标 key,说明目标在右子树
if (cur->_kv.first < key)
{
cur = cur->_right;
}
// 如果当前节点的 key 大于目标 key,说明目标在左子树
else if (cur->_kv.first > key)
{
cur = cur->_left;
}
// 如果相等,说明找到了目标节点,直接返回
else
{
return cur;
}
}
// 遍历结束仍未找到,返回空指针
return nullptr;
}
1.3.9 AVL树平衡的检测(这一点了解思路即可)
我们实现的AVAVL树是否合格,我们通过检查左右子树高度差的的程序进行反向验证,同时检查一下结点的平衡因子更新是否出现了问题
功能说明
这个函数会检查三件事:
- 是否是二叉搜索树 BST(左 < 根 < 右)
- 每个节点的平衡因子是否正确(= 右高 - 左高)
- 是否满足 AVL 平衡条件(所有节点 |BF| ≤ 1)
cpp
// 检查以 cur 为根的子树是否满足 AVL 树规则
// 返回值:子树的高度(用于递归计算)
// 如果不平衡,直接返回 -1 或 断言报错
template<class K, class V>
int AVLTree<K, V>::_IsBalance(Node* cur)
{
// 1. 递归终止条件:空树高度为 0,一定平衡
if (cur == nullptr)
{
return 0;
}
// 2. 递归计算 左子树高度 和 右子树高度
int leftH = _IsBalance(cur->_left); // 左子树高度
int rightH = _IsBalance(cur->_right); // 右子树高度
// ------------------------------------------------------------------
// 3. 检查:当前节点的平衡因子 是否等于 右高 - 左高
// 这一步是为了验证我们插入/旋转时更新的 BF 是否正确
// ------------------------------------------------------------------
if (cur->_bf != rightH - leftH)
{
cout << "平衡因子错误!" << endl;
}
// ------------------------------------------------------------------
// 4. 核心:检查 AVL 平衡条件
// 平衡因子绝对值 必须 ≤ 1
// ------------------------------------------------------------------
if (abs(cur->_bf) > 1)
{
cout << "节点失衡!key = " << cur->_kv.first << endl;
}
// ------------------------------------------------------------------
// 5. 检查 BST 性质(左子树 < 根,右子树 > 根)
// ------------------------------------------------------------------
// 左孩子存在 && 左孩子值 >= 当前节点值 → 不是BST
if (cur->_left && cur->_left->_kv.first >= cur->_kv.first)
{
cout << "BST 规则破坏(左孩子大于根)" << endl;
}
// 右孩子存在 && 右孩子值 <= 当前节点值 → 不是BST
if (cur->_right && cur->_right->_kv.first <= cur->_kv.first)
{
cout << "BST 规则破坏(右孩子小于根)" << endl;
}
// 6. 返回当前子树的高度 = max(左高, 右高) + 1
return max(leftH, rightH) + 1;
}
// 对外接口:检查整棵树是否平衡
template<class K, class V>
bool AVLTree<K, V>::IsBalance()
{
_IsBalance(_root);
return true;
}
1. 函数作用
_IsBalance(cur) 输入 :一个节点 输出 :以这个节点为根的子树高度 副作用:发现不平衡 / BST 错误直接打印提示
2. 执行流程(递归)
- 走到空节点 → 返回高度 0
- 先算左子树高度 、右子树高度
- 检查
cur->_bf是否等于右高 - 左高 - 检查
|BF| ≤ 1 - 检查是否满足
左 < 根 < 右 - 返回当前树高度:
max(左,右) + 1
3. 为什么这么写?
- 递归后序遍历:先算孩子,再算父亲,才能拿到高度
- 一边算高度,一边检查平衡,一次遍历完成所有检查
- 能同时检查 平衡因子错误、失衡、BST 错误
1.3.10 AVL树的删除
由于AVL树的删除不常考感兴趣的同学可以自行了解,它也不算是很难,但就是很麻烦