📌 读者定位与阅读收益
前置知识:已掌握二叉搜索树(BST)的基本概念和操作
读完本文你将获得:
-
✅ 理解 AVL 树核心思想 与平衡因子底层含义
-
✅ 吃透 四种旋转场景,掌握可直接落地的统一决策表
-
✅ 能手写完整 AVL 树插入代码,规避所有高频 Bug
-
✅ 理解删除操作核心逻辑与插入的本质差异
-
✅ 掌握复杂度分析、AVL与红黑树工程选型逻辑
-
💡 附赠可截图保存的 旋转速查卡,面试刷题直接用
一、AVL树是什么------从 BST 的缺陷说起
1.1 BST 的退化问题
普通二叉搜索树(BST)有序性优异,但存在致命缺陷:数据有序/逆序插入时会退化为单向链表。
示例:顺序插入 [1,2,3,4,5]
-
正常平衡 BST:树高
O(logn),查找效率 O(logn) -
退化后链表结构:树高
O(n),查找效率降级为 O(n)
🔴 【配图1】:平衡BST vs 退化链表的对比示意图

1.2 AVL 的核心思想
为了解决 BST 退化问题,AVL 树引入强制高度平衡机制,是严格平衡的二叉搜索树。
核心定义:
-
满足 BST 性质:左子树值 < 根值 < 右子树值;
-
引入平衡因子(Balance Factor) :bf(node) = height(left) - height(right);
-
AVL 强制约束:任意节点 的左右子树的高度差的绝对值 <= 2
-
节点失衡时,通过 树旋转 调整结构,恢复平衡且不破坏BST有序性。
二、核心概念:高度与平衡因子
2.1 节点结构定义(C++)
AVL 树节点相比普通 BST,仅多一个 height 高度字段,用于 O(1) 计算平衡因子:
cpp
typedef int ElemType;
//AVL有效节点
// 二叉链表
typedef struct AVLNode
{
ElemType data; //1.数据域
struct AVLNode* leftchild; //2.左孩子指针域
struct AVLNode* rightchild; //3.右孩子指针域
int height; //4.节点的高度
}AVLNode;
//AVL辅助节点
typedef struct AVLTree
{
AVLNode* root; //用来指向AVL树的根节点
}AVLTree;
2.2 高度计算与空节点约定
本文统一工程/笔试通用标准(彻底解决计算歧义):
-
空节点高度 = -1
-
叶子节点高度 = 0
-
通用高度公式:height(node) = 1 + max( height(leftchild), height(rightchild) )
关键答疑:为什么存高度、不存平衡因子?
旋转、增删操作会改变子树结构,缓存高度可实现 O(1) 时间复杂度获取子树高度,动态计算BF,比缓存BF更灵活、不易出错。
🔴 【配图2】:标注一棵 AVL 树上每个节点的高度值(叶子为0,空为-1)

2.3 高度更新传递链
AVL 树高度更新有且仅有一条传递路径:
插入/删除节点 → 仅该节点到根节点的路径节点高度会变化
因此必须遵循:自底向上回溯更新高度、校验平衡,无法自上而下更新。
🔴 【配图3】:插入节点后,用高亮标记出哪些节点需要重新计算高度

三、贯穿案例:一个插入序列的完整演化
本文全程使用同一套测试序列,一次性覆盖 LL、RR、LR、RL 全部四种旋转场景,所有理论均有实操落地。
3.1 测试序列
插入序列:[20,10,5,3,12,15,30,40,6,9,8]
3.2 分步场景预览
|----------|-------------|-------|
| 插入步骤 | 触发类型 | 失衡节点 |
| 插入 20、10 | 无旋转(完全平衡) | --- |
| 插入 5 | LL(右旋) | 节点 20 |
| 插入 3 | 无旋转(完全平衡) | --- |
| 插入 12 | 无旋转(完全平衡) | --- |
| 插入 15 | LR(左右旋) | 节点 20 |
| 插入 30 | 无旋转(完全平衡) | --- |
| 插入 40 | RR(左旋) | 节点 20 |
| 插入 6 | 无旋转(完全平衡) | --- |
| 插入 9 | 无旋转(完全平衡) | --- |
| 插入 8 | RL(右左旋) | 节点 6 |
🔴 【配图4】:展示最终平衡树形,并用箭头标注每个旋转发生的位置

四、四种失衡场景与旋转(⭐ 核心章节,全文重点)
4.1 如何定位失衡节点?
插入/删除节点后,自底向上回溯所有祖先节点,第一个出现 |bf| > 1 的节点,即为失衡根节点。
递归实现中,只需在递归回溯阶段逐层校验,即可精准捕获失衡节点。
4.2 LL型(右旋)------左子树的左子树过深
触发条件 :bf(node) = 2 && bf(node.left) = 1
场景本质:失衡节点的左孩子、左子树偏高,同侧失衡,单次右旋即可修复。
核心操作步骤:
-
将失衡节点的左孩子提升为新根;
-
将新根的原有右子树,挂载到失衡节点的左子树;
-
原失衡节点下沉为新根的右子树;
-
自下而上更新两颗节点高度。
cpp
//单左旋
AVLNode* LeftRotate(AVLNode* node) {
if (node == NULL)return NULL;
//1.先找到node节点的儿子与孙子
AVLNode* child = node->rightchild;
AVLNode* grandchild = child->leftchild;
//2.让node左旋下来
//先处理冲突的节点:冲突的左孩变右孩
child->leftchild = node;
node->rightchild = grandchild;
//3.更新一下节点高度
Update_Height(node);
Update_Height(child);
return child;
}
🔴 【配图5-1】:LL旋转前树形(标注 a、b、c 三个节点的平衡因子关系)

🔴 【配图5-2】:LL旋转后树形(显示指针变化)

🔴 【配图5-3】 :用案例演示------插入 [10,5,3] 后,节点10失衡,右旋过程

4.3 RR型(左旋)------右子树的右子树过深
触发条件 :bf(node) = -2 && bf(node.right) = -1
场景本质:与LL对称,失衡节点右孩子、右子树偏高,同侧失衡,单次左旋修复。
核心操作步骤:
-
将失衡节点的右孩子提升为新根;
-
将新根的原有左子树,挂载到失衡节点的右子树;
-
原失衡节点下沉为新根的左子树;
-
自下而上更新高度。
cpp
//单右旋
AVLNode* RightRotate(AVLNode* node) {
if (node == NULL)return NULL;
//1.先找到node节点的儿子与孙子
AVLNode* child = node->leftchild;
AVLNode* grandchild = child->rightchild;
//2.让node右旋下来
//先处理冲突的节点:冲突的右孩变左孩
node->leftchild = grandchild;
child->rightchild = node;
//3.更新一下节点高度
Update_Height(node);
Update_Height(child);
return child;
}
🔴 【配图6-1】:RR旋转前树形(对称于LL)

🔴 【配图6-2】:RR旋转后树形

🔴 【配图6-3】 :用案例演示------插入 [12,15,17] 后,节点12失衡,左旋过程

4.4 LR型(左右旋)------左子树的右子树过深
触发条件 :bf(node) = 2 && bf(node.left) = 1
场景本质 :异侧失衡,左子树整体偏高,但左孩子的右子树过深,单次右旋无法修复,必须双旋。
双旋逻辑 :先转为LL场景,再右旋修复
cpp
//先左旋(对node左孩子说的)
node->leftchild = LeftRotate(node->leftchild);
//再右旋(对node自身说的)
return RigthRotate(node);
🔴 【配图7-1】:LR旋转前树形(标注失衡节点 node 和其左孩子的右子树)

🔴 【配图7-2】:第一步左旋后的中间状态

🔴【配图7-3】:第二步右旋后的最终状态

🔴 【配图7-4】 :用案例演示------插入 [10,5,7] 后,节点10失衡,左右旋全过程

4.5 RL型(右左旋)------右子树的左子树过深
触发条件 :bf(node) = -2 && bf(node.right) = -1
场景本质:与LR对称,异侧失衡,右子树整体偏高、右孩子左子树过深,需要双旋修复。
双旋逻辑 :先转为RR场景,再左旋修复
cpp
//先右旋(对node右孩子说的)
node->rightchild = RigthRotate(node->rightchild);
//再左旋(对node自身说的)
return LeftRotate(node);
🔴 【配图8-1】:RL旋转前树形(对称于LR)

🔴 【配图8-2】:第一步右旋后的中间状态

🔴 【配图8-3】:第二步左旋后的最终状态

🔴 【配图8-4】 :用案例演示------插入 [10,15,12] 后,节点10失衡,右左旋全过程

4.6 统一决策表(⭐ 直接翻译代码,零修改落地)
所有失衡场景可通过以下代码一次性判断,面试手撕直接套用:
cpp
//平衡旋转函数(通用) //插入造成的失衡可以调整 删除造成的也行
AVLNode* Rotate(AVLNode* node)
{
//平衡因子:左子树高度 - 右子树高度
int bal = Get_BalanceFactor(node);
if (bal == 2)//L
{
int bal_lc = Get_BalanceFactor(node->leftchild);
if (bal_lc == 1)//🔴 LL 单次右旋
{
//单右旋
return RigthRotate(node);
}
else if (bal_lc == -1) // 🔴 LR 先左后右双旋
{
node->leftchild = LeftRotate(node->leftchild);//先左旋(对node左孩子说的)
return RigthRotate(node);//再右旋(对node自身说的)
}
}
if (bal == -2)//R
{
int bal_rc = Get_BalanceFactor(node->rightchild);
if (bal_rc == -1) // 🔴 RR 单次左旋
{
//单左旋
return LeftRotate(node);
}
else if (bal_rc == 1) // 🔴 RL 先右后左双旋
{
//先右旋(对node右孩子说的)
node->rightchild = RigthRotate(node->rightchild);
//再左旋(对node自身说的)
return LeftRotate(node);
}
}
return node; // 无失衡,直接返回
}
🔴 【配图9】:四种旋转的决策流程图(从 bf 判断到选择旋转类型的完整路径)

4.7 旋转黄金法则:高度更新顺序(必考易错点)
核心铁律 :旋转后必须 先更新原子子树根高度,再更新新根高度(自下而上)
以右旋为例:
cpp
//更新当前节点的高度
void Update_Height(AVLNode* node) {
assert(node != NULL);
int hl = Get_Height(node->leftchild);
int hr = Get_Height(node->rightchild);
node->height = hl > hr ? hl + 1 : hr + 1;
}
❌ 顺序颠倒后果:新根高度基于旧数据计算,平衡因子全部错乱,隐性Bug极难排查
五、插入实现的完整流程(⭐ 可直接手撕代码)
5.1 插入流程总览
-
标准 BST 递归插入,找到空位挂载新节点;
-
回溯阶段更新当前节点高度(必须在平衡判断之前);
-
计算当前节点平衡因子,判断是否失衡;
-
根据统一决策表执行对应旋转;
-
返回当前节点(旋转后为新根,上层递归承接)。
🔴 【配图10】:平衡二叉树完整构建流程图,包含具体的旋转过程
插入序列:[20,10,5,3,12,15,30,40,6,9,8]

5.2 完整插入代码
cpp
//插入(启动插入递归函数)
bool Insert_AVL(AVLTree* pTree, ElemType val)
{
assert(NULL != pTree);
Insert_Helper(pTree->root, val);
return true;
}
//插入的递归函数
AVLNode* Insert_Helper(AVLNode* root, ElemType val) {
if (root == NULL)
return BuyNode(val);
if (root->data > val) {
root->leftchild = Insert_Helper(root->leftchild, val);
}
else if (root->data < val) {
root->rightchild = Insert_Helper(root->rightchild, val);
}
else {
return root;
}
//更新当前节点的高度
Update_Height(root);//当前root只有更新完高度之后,其回退的父节点获取的平衡因子才是准确的
//再对此时的root节点做一趟平衡处理
return Rotate(root);
}
六、删除操作的核心思路(⭐ 进阶可选)
6.1 删除与插入的核心差异
相同点:删除后同样需要自底向上回溯、更新高度、校验平衡、执行旋转。
本质区别(面试高频):
-
插入 :最多触发 2次旋转,修复后上层必然平衡,回溯终止;
-
删除 :会永久降低子树高度,修复当前节点后,上层仍可能连锁失衡,最坏需要回溯到根节点、多次旋转。
6.2 BST删除三种场景回顾
-
叶子节点:直接删除,无后续替换;
-
单孩子节点:用唯一孩子替代当前节点位置;
-
双孩子节点:用中序后继/前驱节点替换,再删除后继/前驱。
🔴 【配图11】:删除双孩子节点时,用后继替代的指针变化图
6.3 删除后平衡调整策略
-
从实际删除位置向上回溯,逐节点更新高度;
-
复用第四章 balance() 函数做统一旋转修复;
-
关键:无论当前层是否修复平衡,都必须继续向上回溯,不能终止。
6.4 代码入口示意
cpp
AVLNode* remove(AVLNode* node, int key) {
// 1. 完成标准BST三种删除逻辑
// 2. 更新当前节点高度
// 3. 调用balance()修复失衡
// 4. 返回节点,继续上层回溯校验
}
七、AVL树复杂度分析与工程选型
7.1 时间复杂度一览表
| 操作 | 时间复杂度 | 最大旋转次数 | 说明 |
|---|---|---|---|
| 查找 | O(log n) | 0 | 树高严格 ≤ 1.44×log₂(n+2),路径最短 |
| 插入 | O(log n) | ≤ 2 次 | 单/双旋即可永久修复平衡 |
| 删除 | O(log n) | ≤ O(logn) 次 | 存在连锁失衡,需多层修复 |
7.2 AVL树 vs 红黑树(工程选型核心对比)
| 对比维度 | AVL 树 | 红黑树 |
|---|---|---|
| 平衡严格度 | 严格平衡(|bf|≤1) | 宽松近似平衡(黑高平衡) |
| 查找速度 | 更快(树更矮、比较次数少) | 略慢于AVL |
| 插入旋转次数 | ≤ 2 次 | ≤ 2 次 |
| 删除旋转次数 | 最多 O(logn) 次(开销大) | ≤ 3 次(开销极低) |
| 适用场景 | 查询密集型(静态索引、只读数据) | 读写均衡/高频更新(容器、动态索引) |
一句话选型结论:读多写少用 AVL,频繁增删用红黑树。
八、调试指南与高频踩坑清单
8.1 三大最高频Bug与解决方案
| Bug类型 | 现象 | 解决方案 |
|---|---|---|
| 空节点高度不统一 | 叶子节点BF计算错乱、假性失衡 | 全局统一:空节点=-1,叶子=0 |
| 旋转后未更新高度 | 树形正常,后续操作持续失衡 | 严格遵守「先子后父」高度更新顺序 |
| 递归返回值未承接 | 旋转失效、指针丢失、树结构混乱 | 必须写 node->left = insert(...) |
8.2 官方可视化工具推荐
https://www.cs.usfca.edu/~galles/visualization/AVLtree.html:支持AVL插入、删除、旋转逐帧动画,零基础直观看懂结构变化。
九、附录:完整测试代码与旋转速查卡
9.1 通用测试用例
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<stdint.h>
#include<assert.h>
#include"AVLTree.h"
//AVL全部递归实现
//工具函数
//1.购买新结点
AVLNode* BuyNode(ElemType val) {
AVLNode* pnewNode = (AVLNode*)malloc(sizeof(AVLNode));
if (pnewNode == NULL)
exit(EXIT_FAILURE);
pnewNode->data = val;
pnewNode->leftchild = NULL;
pnewNode->rightchild = NULL;
pnewNode->height = 0;
}
//2.获取当前节点的高度(默认当前节点是准确的)
int Get_Height(AVLNode* node) {
if (node == NULL)return -1;
return node->height;
}
//3.更新当前节点的高度
void Update_Height(AVLNode* node) {
assert(node != NULL);
int hl = Get_Height(node->leftchild);
int hr = Get_Height(node->rightchild);
node->height = hl > hr ? hl + 1 : hr + 1;
}
//4.获取当前节点的平衡因子
int Get_BalanceFactor(AVLNode* node) {
if (node == NULL)return -1;
return Get_Height(node->leftchild) - Get_Height(node->rightchild);
}
//5.单左旋
AVLNode* LeftRotate(AVLNode* node) {
if (node == NULL)return NULL;
//1.先找到node节点的儿子与孙子
AVLNode* child = node->rightchild;
AVLNode* grandchild = child->leftchild;
//2.让node左旋下来
//先处理冲突的节点:冲突的左孩变右孩
child->leftchild = node;
node->rightchild = grandchild;
//3.更新一下节点高度
Update_Height(node);
Update_Height(child);
return child;
}
//6.单右旋
AVLNode* RightRotate(AVLNode* node) {
if (node == NULL)return NULL;
//1.先找到node节点的儿子与孙子
AVLNode* child = node->leftchild;
AVLNode* grandchild = child->rightchild;
//2.让node右旋下来
//先处理冲突的节点:冲突的右孩变左孩
node->leftchild = grandchild;
child->rightchild = node;
//3.更新一下节点高度
Update_Height(node);
Update_Height(child);
return child;
}
//普通函数
//1.初始化
void Init_AVL(AVLTree* pTree)
{
assert(pTree != NULL);
pTree->root = NULL;
}
//2.插入(启动插入递归函数)
bool Insert_AVL(AVLTree* pTree, ElemType val)
{
assert(NULL != pTree);
Insert_Helper(pTree->root, val);
return true;
}
//2.5 插入的递归函数
AVLNode* Insert_Helper(AVLNode* root, ElemType val) {
if (root == NULL)
return BuyNode(val);
if (root->data > val) {
root->leftchild = Insert_Helper(root->leftchild, val);
}
else if (root->data < val) {
root->rightchild = Insert_Helper(root->rightchild, val);
}
else {
return root;
}
//更新当前节点的高度
Update_Height(root);//当前root只有更新完高度之后,其回退的父节点获取的平衡因子才是准确的
//再对此时的root节点做一趟平衡处理
return Rotate(root);
}
//5.平衡旋转函数(通用) //插入造成的失衡可以调整 删除造成的也行
AVLNode* Rotate(AVLNode* node)
{
//平衡因子:左子树高度 - 右子树高度
int bal = Get_BalanceFactor(node);
if (bal == 2)//L
{
int bal_lc = Get_BalanceFactor(node->leftchild);
if (bal_lc == 1)//LL型
{
//单右旋
return RigthRotate(node);
}
else if (bal_lc == -1)//LR型
{
node->leftchild = LeftRotate(node->leftchild);//先左旋(对node左孩子说的)
return RigthRotate(node);//再右旋(对node自身说的)
}
}
if (bal == -2)//R
{
int bal_rc = Get_BalanceFactor(node->rightchild);
if (bal_rc == -1)//RR型
{
//单左旋
return LeftRotate(node);
}
else if (bal_rc == 1)//RL型
{
//先右旋(对node右孩子说的)
node->rightchild = RigthRotate(node->rightchild);
//再左旋(对node自身说的)
return LeftRotate(node);
}
}
return node;
}
int main()
{
return 0;
}
9.2 旋转速查卡(可截图保存)
| 类型 | 触发条件 | 操作方式 | 案例插入序列 |
|---|---|---|---|
| LL | bf>1 && bf(left)>=0 | 右旋(node) | 10→5→3 |
| LR | bf>1 && bf(left)<0 | 左旋(left)+右旋(node) | 10→5→7 |
| RR | bf<-1 && bf(right)<=0 | 左旋(node) | 15→12→17 |
| RL | bf<-1 && bf(right)>0 | 右旋(right)+左旋(node) | 10→15→12 |
黄金法则复盘:旋转后先更新原子树根高度,再更新新根高度(自下而上不可逆)
✍️ 结语
AVL 树是自平衡二叉树的基石与入门天花板。吃透 AVL 的平衡思想、旋转本质、高度更新逻辑,后续学习红黑树、Treap、替罪羊树等高级平衡树,只会事半功倍。
本文摒弃纯背诵式理论,以统一标准+贯穿案例+落地代码+避坑指南+面试考点全覆盖,真正实现:看懂原理、写得出代码、答得上面试、刷得对习题。
📊 全文配图清单汇总
| 编号 | 位置 | 内容 | 类型 |
|---|---|---|---|
| 图1 | 第一章 | 平衡BST vs 退化链表对比 | 对比图 |
| 图2 | 第二章 | 节点高度标注示例 | 标注图 |
| 图3 | 第二章 | 插入后高度更新路径 | 高亮路径图 |
| 图4 | 第三章 | 完整案例最终树形+旋转标注 | 总览图 |
| 图5-1~3 | 第四章 | LL旋转全过程+案例 | 过程图×3 |
| 图6-1~3 | 第四章 | RR旋转全过程+案例 | 过程图×3 |
| 图7-1~4 | 第四章 | LR双旋全过程+案例 | 过程图×4 |
| 图8-1~4 | 第四章 | RL双旋全过程+案例 | 过程图×4 |
| 图9 | 第四章 | 旋转决策流程图 | 流程图 |
| 图10 | 第五章 | 插入操作全流程图 | 流程图 |
| 图11 | 第六章 | 双孩子节点删除指针变化 | 过程图 |