平衡二叉树(AVL树)深度解析:从原理到实践

📌 读者定位与阅读收益

前置知识:已掌握二叉搜索树(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 树引入强制高度平衡机制,是严格平衡的二叉搜索树。

核心定义:

  1. 满足 BST 性质:左子树值 < 根值 < 右子树值;

  2. 引入平衡因子(Balance Factor) :bf(node) = height(left) - height(right);

  3. AVL 强制约束:任意节点 的左右子树的高度差的绝对值 <= 2

  4. 节点失衡时,通过 树旋转 调整结构,恢复平衡且不破坏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

场景本质:失衡节点的左孩子、左子树偏高,同侧失衡,单次右旋即可修复。

核心操作步骤

  1. 将失衡节点的左孩子提升为新根;

  2. 将新根的原有右子树,挂载到失衡节点的左子树;

  3. 原失衡节点下沉为新根的右子树;

  4. 自下而上更新两颗节点高度。

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对称,失衡节点右孩子、右子树偏高,同侧失衡,单次左旋修复。

核心操作步骤

  1. 将失衡节点的右孩子提升为新根;

  2. 将新根的原有左子树,挂载到失衡节点的右子树;

  3. 原失衡节点下沉为新根的左子树;

  4. 自下而上更新高度。

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 插入流程总览

  1. 标准 BST 递归插入,找到空位挂载新节点;

  2. 回溯阶段更新当前节点高度(必须在平衡判断之前);

  3. 计算当前节点平衡因子,判断是否失衡;

  4. 根据统一决策表执行对应旋转;

  5. 返回当前节点(旋转后为新根,上层递归承接)。

🔴 【配图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删除三种场景回顾

  1. 叶子节点:直接删除,无后续替换;

  2. 单孩子节点:用唯一孩子替代当前节点位置;

  3. 双孩子节点:用中序后继/前驱节点替换,再删除后继/前驱。

🔴 【配图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 第六章 双孩子节点删除指针变化 过程图