AVL 树是历史上第一个自平衡二叉搜索树,解决普通二叉搜索树有序插入退化为链表、最坏 O (N) 的致命缺陷,保证增删查稳定(O(logN))。本文基于
AVLTree.h与Test.cpp完整源码,逐行解析节点结构、插入逻辑、四种旋转、平衡因子更新、校验工具,同时补充底层细节、删除难点、AVL 与红黑树对比,覆盖全部代码出现的知识点。
一、AVL 树基础概念
1.1 定义
AVL 树由前苏联科学家 Adelson‑Velsky 和 Landis 于 1962 年提出,它要么是空树,要么满足两个条件:
- 左右子树本身也都是 AVL 树;
- 任意节点左右子树高度差绝对值不超过 1。
为什么不要求高度差必须等于 0? 部分节点数量无法做到完全等高,例如 2 个节点、4 个节点,最优高度差只能是 1,强制 0 是做不到的。
1.2 平衡因子(balance factor,bf)
定义:bf = 右子树高度 − 左子树高度 合法取值只能是 -1、0、1。
bf = 0:左右子树高度相等;bf = 1:右子树比左子树高 1;bf = -1:左子树比右子树高 1;bf = 2 / -2:树失衡,必须旋转修复平衡。
注意:部分教材定义
bf=左−右,符号会完全相反,做题要看定义。本套源码统一使用右减左。
补充:平衡因子不是 AVL 树必需,也可以每个节点存子树高度;平衡因子只是传感器,不需要递归计算高度,O (1) 读取,提升效率。
1.3 性能对比普通 BST
- 普通 BST:最优(O(logN)),有序插入退化链表最坏(O(N));
- AVL 树:严格限制高度,树高上界约(1.44*log_2N),增删查最坏稳定(O(logN))。
二、AVL 树节点与类框架源码解析
cpp
template<class K, class V>
struct AVLTreeNode
{
pair<K, V> _kv; // key‑value键值对
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); // 插入
Node* Find(const K& key); // 查找
void InOrder(); // 中序遍历
bool IsBalanceTree(); // 校验是否AVL树
int Height(); // 获取树高度
int Size(); // 统计节点数量
private:
// 四大旋转私有函数
void RotateR(Node* parent); // 右单旋 LL
void RotateL(Node* parent); // 左单旋 RR
void RotateLR(Node* parent); // 左右双旋 LR
void RotateRL(Node* parent); // 右左双旋 RL
// 递归辅助函数
int _Height(Node* root);
int _Size(Node* root);
bool _IsBalanceTree(Node* root);
void _InOrder(Node* root);
Node* _root = nullptr; // 根节点
};
重点知识点(代码细节)
_parent父指针的意义 : 普通 BST 递归插入依靠参数引用回溯;AVL 插入完成后,需要从新节点自底向上回溯所有祖先更新平衡因子,父指针可以直接向上走,不需要递归保存路径。旋转操作也需要父指针完成上层子树的链接。_bf初始化 0:新插入节点是叶子,左右子树都是空,平衡因子初始为 0。- 存储
pair<K,V>:和 map 一样,key 做排序,value 存储附属业务数据。 - 所有旋转函数是私有成员,对外不暴露,封装内部实现。
三、AVL 插入完整流程
插入分为三大阶段:
- 普通 BST 规则找到插入位置,创建新节点;
- 自底向上更新祖先平衡因子;
- 判断是否失衡,如果 bf 等于 2/-2,执行对应旋转;旋转之后该子树高度恢复插入前状态,插入场景旋转完成直接终止循环,不需要继续向上更新。
3.1 平衡因子更新规则
新节点插入parent的右孩子:parent->_bf ++; 新节点插入parent的左孩子:parent->_bf --。
三种更新停止条件
- 更新后
parent->_bf == 0: 更新前 bf 是1→0或者-1→0;新增节点填补矮的一侧,子树整体高度不变,不会继续影响上层祖先,循环 break 结束更新。 - 更新后
parent->_bf == 1 || parent->_bf == -1: 更新前 bf 是0→1或者0→-1;子树高度 + 1,会继续影响上层祖先;cur = parent; parent = parent->_parent;继续向上循环。 - 更新后
parent->_bf == 2 || parent->_bf == -2: 子树失衡,执行旋转;插入场景旋转之后子树高度恢复插入之前,不会再向上传播,直接 break。
⭐重要区分:插入最多触发 1 次旋转(单旋或者双旋);但是删除会出现连锁失衡,可能一路旋转到根,课件没有实现删除,后面补充拓展。
3.2 Insert 完整源码与逐行注释
cpp
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
// BST查找插入位置
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重复,不允许插入
}
}
// 创建新节点,建立父子关系
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->_right)
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); // LL:右单旋
}
else if (parent->_bf == 2 && cur->_bf == 1)
{
RotateL(parent); // RR:左单旋
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
RotateLR(parent); // LR:左右双旋
}
else if (parent->_bf == 2 && cur->_bf == -1)
{
RotateRL(parent); // RL:右左双旋
}
else
{
assert(false); // 原树本身就非法
}
break; // 插入旋转完毕,不再向上
}
else
{
assert(false);
}
}
return true;
}
3.3 四种失衡场景对照表
| 失衡类型 | 失衡节点 bf | 孩子节点 bf | 旋转方案 |
|---|---|---|---|
| LL 左左 | -2 |
-1 |
右单旋 RotateR |
| RR 右右 | 2 |
1 |
左单旋 RotateL |
| LR 左右 | -2 |
1 |
左右双旋 RotateLR:先左旋子节点,再右旋失衡点 |
| RL 右左 | 2 |
-1 |
右左双旋 RotateRL:先右旋子节点,再左旋失衡点 |
LL 是左子树的左子树长高;RR 是右子树的右子树长高;LR 左子树的右子树长高;RL 右子树的左子树长高。
四、四大旋转操作深度解析
旋转两个硬性原则:
- 保持二叉搜索树有序性质;
- 修复平衡,并且插入场景旋转之后,子树高度等于插入之前,不再向上传播失衡。
4.1 右单旋 RotateR(处理 LL 场景)
原理图:失衡点 parent,subL 是 parent 的左孩子,subLR 是 subL 的右子树。
subLR脱离 subL,挂载为 parent 的左孩子;注意必须维护subLR->_parent;- subL 顶替 parent 的位置,parent 变为 subL 的右孩子;
- 处理
parentParent:如果 parent 是整棵树的根,更新_root;否则把上层父节点指向 subL; - 更新 parent、subL 的
_bf全部置 0。
cpp
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
Node* parentParent = 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;
}
parent->_bf = subL->_bf = 0;
}
高频易错:很多初学者旋转只修改孩子指针,忘记同步修改
_parent父指针,会导致树逻辑彻底错乱!
4.2 左单旋 RotateL(处理 RR 场景)
和右单旋镜像:
cpp
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if(subRL)
subRL->_parent = parent;
Node* parentParent = parent->_parent;
subR->_left = parent;
parent->_parent = subR;
if (parentParent == nullptr)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (parentParent->_left == parent)
parentParent->_left = subR;
else
parentParent->_right = subR;
subR->_parent = parentParent;
}
subR->_bf = parent->_bf = 0;
}
4.3 左右双旋 RotateLR(LR 场景)
场景:失衡点 parent bf=-2;它的左孩子 subL bf=1;也就是长高发生在左孩子的右子树,单纯右单旋无法修复,必须双旋。
- 先对
subL做左单旋 RotateL; - 再对失衡点
parent做右单旋 RotateR; - ⭐重点:双旋之后平衡因子不一定全部为 0 ,取决于
subLR(中间节点)原来的 bf 值,分三种情况:
bf == 0:subLR 本身就是新插入节点 → subL、parent、subLR 全部置 0;bf == 1:新节点插入 subLR 的右子树 →subL->_bf=0,parent->_bf=1,subLR->_bf=0;bf == -1:新节点插入 subLR 的左子树 →subL->_bf=-1,parent->_bf=0,subLR->_bf=0。
cpp
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(subL);
RotateR(parent);
if (bf == 0)
{
subL->_bf = 0;
parent->_bf = 0;
subLR->_bf = 0;
}
else if (bf == -1)
{
subL->_bf = 0;
parent->_bf = 1;
subLR->_bf = 0;
}
else if (bf == 1)
{
subL->_bf = -1;
parent->_bf = 0;
subLR->_bf = 0;
}
else
{
assert(false);
}
}
4.4 右左双旋 RotateRL(RL 场景)
镜像 LR 双旋:失衡点 bf=2,长高发生在右孩子的左子树。
- 先对
subR做右单旋 RotateR; - 再对失衡点
parent做左单旋 RotateL; - 根据
subRL原始 bf 三种分支更新平衡因子。
cpp
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(subR);
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);
}
}
双旋最容易踩坑:直接把三个节点 bf 全部赋值 0,忽略三种不同 bf 分支,测试双旋用例直接破坏 AVL 性质。
五、查找、遍历、校验工具函数源码解析
5.1 Find 查找
复用普通 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;
}
5.2 中序遍历_InOrder
AVL 本质是 BST,中序遍历输出有序序列。
cpp
void InOrder()
{
_InOrder(_root);
cout << endl;
}
private:
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_kv.first << " ";
_InOrder(root->_right);
}
5.3 校验函数 IsBalanceTree【非常重要测试工具】
校验函数,用来反向验证我们写的 AVL 树是否正确,两个校验条件同时满足:
- 当前节点实际计算得到的高度差绝对值必须小于 2;
- 当前节点存储的
_bf必须等于右子树高度 − 左子树高度;如果不一致说明平衡因子更新逻辑出错。
cpp
bool IsBalanceTree()
{
return _IsBalanceTree(_root);
}
private:
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;
}
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 (root->_bf != diff)
{
cout << root->_kv.first << "平衡因子异常" << endl;
return false;
}
return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}
测试代码
TestAVLTree1()使用数组{4,2,6,1,3,5,15,7,16,14},里面包含 LL/RR/LR/RL 全部四种旋转场景,IsBalanceTree()返回 true 代表代码正确。
注意:_Height是递归统计高度,仅测试校验使用,业务代码不要频繁调用,复杂度 O (N);生产 AVL 节点一般直接存 height 成员变量 O (1) 读取。
六、拓展知识
6.1 AVL 删除
插入最多一次旋转;删除会造成连锁失衡:删除节点之后,父节点子树高度可能减少,旋转修复当前节点之后,上层祖先又会失衡,最坏需要从删除点一路旋转到根,最坏(O(logN))次旋转。
- 插入:旋转之后子树高度复原,循环 break;
- 删除:旋转之后子树高度可能继续降低,不能 break,要继续向上回溯直到 root。
这是 AVL 树最大短板,写删除逻辑难度远大于插入。
6.2 AVL vs 红黑树(面试高频)
| 对比维度 | AVL 树 | 红黑树(set/map 底层) |
|---|---|---|
| 平衡约束 | 严格平衡,高度差≤1 | 弱平衡,最长路径≤2 倍最短路径 |
| 查找性能 | 更好,树更矮 | 略差,同一个数量级 O (logN) |
| 插入旋转 | 最多 1‑2 次旋转 | 最多 2 次旋转 |
| 删除旋转 | 最坏 O (logN) 次连锁旋转 | 最多 3 次旋转 |
| 额外开销 | 每个节点存平衡因子 int | 每个节点仅需要 1bit 颜色位 |
| 适用场景 | 查询远远多于增删 | 插入删除频繁,工业标准选择 |
结论:C++ STL 没有采用 AVL,使用红黑树,就是因为删除场景 AVL 旋转开销太大。
6.3 AVL 两种实现流派
- 存平衡因子 bf,插入更新 bf;
- 存 height 高度:每个节点保存子树高度,旋转之后更新 height,不需要计算 bf; 两种都可以,height 版本做删除逻辑更方便。
6.4 易错坑点汇总
- 旋转忘记更新
_parent父指针,树内部逻辑断裂,校验直接失败; - 双旋直接把三个节点 bf 全部置 0,忽略
subLR三种 bf 分支; - 混淆 bf 定义:有的教材 bf = 左‑右,直接抄课件旋转代码就会全部反向;
- 插入旋转之后继续向上循环:插入旋转之后子树高度复原,必须 break;
- 把校验用递归
_Height用到业务逻辑,造成性能灾难。
七、面试高频问答
AVL 树性质?平衡因子定义?
AVL 是自平衡 BST,任意节点左右子树高度差绝对值不大于 1;本课件 bf = 右子树高度 − 左子树高度,合法取值 - 1,0,1。
AVL 四种失衡类型分别对应什么旋转?
LL 右单旋;RR 左单旋;LR 先左后右双旋;RL 先右后左双旋。
AVL 插入更新平衡因子,循环停止三个条件?
bf=0 停止;bf=±1 继续向上;bf=±2 旋转后停止。
为什么旋转的时候必须维护_parent 指针?
AVL 插入需要自底向上回溯祖先更新 bf;旋转要修改上层父节点的孩子指向,如果父指针错误,树结构完全破坏。
AVL 插入和删除旋转次数差异?
插入最多一次旋转;删除会连锁失衡,最坏 O (logN) 次旋转,这也是 STL 选用红黑树的重要原因。
IsBalanceTree 校验函数做了哪两件校验?
第一校验实际高度差绝对值 < 2;第二校验节点存储 bf 等于真实计算的右减左高度差,排查 bf 更新逻辑 bug。
AVL 树中序遍历结果有什么特点?
和 BST 一样,中序遍历 key 有序。