✍️ 前言
🎯 一句话总结:AVL树是二叉搜索树的"进化版",它通过旋转操作自动保持平衡,让增删查的效率永远稳定在 O(logN)。
为什么要学 AVL 树?
在上一篇博客中,我们学习了二叉搜索树(BST)。BST 有一个致命缺陷:当插入的数据有序时,它会退化成链表。比如依次插入 1、2、3、4、5,BST 就变成了一条向右倾斜的"一字长蛇阵",查找效率从 O(logN) 暴跌到 O(N)。
AVL 树就是为了解决这个问题而诞生的。它在每次插入/删除后都会检查平衡性,一旦发现左右子树高度差超过 1,就立刻通过旋转操作恢复平衡。无论你怎么插入数据,它都能保持树的高度在 logN 级别。
本篇你将学到什么?
| 学习目标 | 具体内容 |
|---|---|
| 🧩 AVL 概念 | 自平衡原理、平衡因子的定义与作用 |
| 📦 树的结构 | 结点定义、parent 指针的必要性 |
| 🔧 插入操作 | BST 插入 + 平衡因子更新 + 旋转触发 |
| 🔄 四种旋转 | 右单旋、左单旋、左右双旋、右左双旋 |
| 🔍 查找与验证 | O(logN) 查找、平衡性检测代码 |
| 🐛 经典 Bug | 旋转中容易踩的坑 |
💡 前置知识 :建议先阅读本系列第十六篇:二叉搜索树,理解 BST 的插入、查找和删除操作,因为 AVL 树就是在 BST 的基础上增加了平衡控制。
准备好了吗?让我们开始吧!🚀
🧠 全文思维导图
以下思维导图总结了本文所有核心知识点,建议先浏览一遍建立整体印象,再逐节深入学习。

📋 目录
- 一、AVL树的概念
- 二、AVL树的结点定义与结构
- 三、AVL树的插入操作
- 四、平衡因子的更新规则
- 五、四种旋转详解
- [5.1 右单旋(RotateR)](#5.1 右单旋(RotateR))
- [5.2 左单旋(RotateL)](#5.2 左单旋(RotateL))
- [5.3 左右双旋(RotateLR)](#5.3 左右双旋(RotateLR))
- [5.4 右左双旋(RotateRL)](#5.4 右左双旋(RotateRL))
- 六、AVL树的查找
- 七、AVL树的平衡验证
- 八、性能分析
- 九、完整代码汇总
- 十、总结
一、AVL树的概念
1. 简介
AVL 树是最先发明的自平衡二叉搜索树,由两位前苏联科学家 G. M. Adelson-Velsky 和 E. M. Landis 在 1962 年的论文中提出,AVL 就得名于他们名字的缩写。
AVL 树是一棵空树,或者具备以下性质的二叉搜索树:
- 它的左右子树都是 AVL 树
- 左右子树的高度差的绝对值不超过 1
通过控制高度差,AVL 树确保整棵树始终保持"矮胖"形态,高度被控制在 logN 级别,增删查的效率都能稳定在 O(logN)。
平衡因子(Balance Factor)
为了方便观察和控制树的平衡性,AVL 树引入了平衡因子的概念:
平衡因子 = 右子树高度 - 左子树高度
在 AVL 树中,任何结点的平衡因子只能是 0、1 或 -1。如果某个结点的平衡因子变成了 2 或 -2,就说明树失衡了,需要旋转调整。
💡 思考:为什么 AVL 树要求高度差不超过 1,而不是高度差为 0?因为有些情况下做不到高度差为 0!比如只有 2 个结点的树,左右子树高度差必然是 1。
BST 退化问题演示
普通BST(退化成链表): AVL树(自动平衡):
1 2
\ / \
2 1 3
\ \
3 4
\ \
4 5
\
5
高度 = 5, 查找O(N) 高度 = 3, 查找O(logN)
二、AVL树的结点定义与结构
1. 简介
AVL 树的结点与普通 BST 结点相比,多了两个关键成员:
_parent指针 :指向父结点。因为插入新结点后,需要从新结点开始向上更新祖先结点的平衡因子,没有 parent 指针就无法回溯。_bf平衡因子:记录当前结点的平衡状态。
2. 代码示例
AVL 树结点定义------三叉链 + 平衡因子
cpp
#include <iostream>
#include <assert.h>
using namespace std;
// AVL树结点定义(三叉链)
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)
{}
};
// AVL树主体
template<class K, class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
// 插入、查找、旋转等接口...
private:
Node* _root = nullptr;
};
💡 为什么用
struct而不是class? 因为结点需要直接访问成员,struct 默认公有更方便。树主体用 class 封装,对外只暴露公共接口。
3. 经典Bug:忘记 parent 指针
问题描述 :新手定义结点时只写了 _left 和 _right(二叉链),忘了加 _parent。结果插入新结点后,不知道怎么向上更新祖先的平衡因子。
错误写法:
cpp
// ❌ 错误:没有parent指针,无法向上回溯
template<class K, class V>
struct AVLTreeNode
{
pair<K, V> _kv;
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
int _bf; // 有了平衡因子,但没法向上更新!
};
正确做法 :必须使用三叉链(_left + _right + _parent),这样才能从新插入的结点一路向上更新平衡因子直到根结点。
三、AVL树的插入操作
1. 简介
AVL 树的插入分为两大步骤:
- 按 BST 规则插入:比当前结点小往左走,大往右走,找到空位置插入。
- 更新平衡因子 + 必要时旋转:从新结点的 parent 开始向上更新平衡因子,如果发现某个结点的平衡因子变成 ±2,就进行旋转处理。
插入的完整流程
1. BST规则找到插入位置
2. 插入新结点,链接parent
3. 从parent开始向上更新平衡因子:
- 新结点在parent左边 → parent->_bf--
- 新结点在parent右边 → parent->_bf++
4. 根据更新后的平衡因子决定后续操作:
- bf == 0 → 子树高度没变,停止更新
- bf == ±1 → 子树高度变了,继续向上更新
- bf == ±2 → 子树失衡,旋转处理,然后停止

2. 代码示例
插入操作完整实现------BST插入 + 平衡因子更新
cpp
bool 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;
}
else
{
// key已存在,插入失败
return false;
}
}
// 3. 创建新结点并链接到parent
cur = new Node(kv);
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent; // 别忘了设置parent指针!
// 4. 从parent开始向上更新平衡因子
while (parent)
{
if (cur == parent->_left)
parent->_bf--; // 插在左边,平衡因子减1
else
parent->_bf++; // 插在右边,平衡因子加1
if (parent->_bf == 0)
{
// 平衡因子从±1变成0,说明矮的一边被填上了
// 子树整体高度没变,不需要继续向上更新
break;
}
else if (parent->_bf == 1 || parent->_bf == -1)
{
// 平衡因子从0变成±1,说明子树高度增加了
// 需要继续向上更新祖先的平衡因子
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
// 平衡因子变成±2,子树失衡!需要旋转处理
// 旋转后子树恢复平衡且高度降低,不需要继续向上更新
if (parent->_bf == 2 && cur->_bf == 1)
{
// 右边高的右边高 → 左单旋
RotateL(parent);
}
else if (parent->_bf == -2 && cur->_bf == -1)
{
// 左边高的左边高 → 右单旋
RotateR(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1)
{
// 右边高的左边高 → 右左双旋
RotateRL(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
// 左边高的右边高 → 左右双旋
RotateLR(parent);
}
break;
}
else
{
// 理论上不应该走到这里,走到了说明逻辑有bug
assert(false);
}
}
return true;
}
中序遍历验证------验证BST性质是否保持
cpp
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << " ";
_InOrder(root->_right);
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
💡 验证方式:中序遍历如果输出有序序列,说明 BST 性质没有被旋转破坏。AVL 树旋转后仍然是一棵合法的二叉搜索树。
3. 经典Bug:旋转条件判断错误
问题描述:新手容易只看 parent 的平衡因子来决定旋转类型,忘了看 cur 的平衡因子。
错误写法:
cpp
// ❌ 错误:只看parent,没看cur
if (parent->_bf == 2)
RotateL(parent); // 万一是右左双旋的情况呢?
else if (parent->_bf == -2)
RotateR(parent); // 万一是左右双旋的情况呢?
正确做法 :必须同时看 parent 和 cur 的平衡因子,四种组合对应四种旋转:
| parent->_bf | cur->_bf | 旋转类型 | 含义 |
|---|---|---|---|
| -2 | -1 | 右单旋 | 左边的左边高 |
| 2 | 1 | 左单旋 | 右边的右边高 |
| -2 | 1 | 左右双旋 | 左边的右边高 |
| 2 | -1 | 右左双旋 | 右边的左边高 |
四、平衡因子的更新规则
1. 简介
平衡因子的更新是 AVL 树最核心的逻辑之一。理解了它,就理解了 AVL 树"什么时候需要旋转"。
更新原则
- 平衡因子 = 右子树高度 - 左子树高度
- 插入结点在 parent 的左子树 → parent 的平衡因子 --
- 插入结点在 parent 的右子树 → parent 的平衡因子 ++
三种停止条件
| 更新后 parent->_bf | 含义 | 操作 |
|---|---|---|
| 0 | 原来±1,矮的一边被填上,子树高度不变 | 停止更新 |
| ±1 | 原来0,两边变成一高一低,子树高度+1 | 继续向上更新 |
| ±2 | 原来±1,高的一边更高了,子树失衡 | 旋转处理,然后停止 |
2. 代码示例
三种停止条件的场景演示
cpp
// 场景1:bf从-1变成0 → 停止
// 插入前:左子树高1,右子树高0,bf=-1
// 在左子树插入后:左子树高1,右子树高0... 不对
// 实际是:插入前左低右高bf=1,插在左边后两边等高bf=0
//
// 5 5
// \ → / \
// 8 3 8 bf: 1→0,高度不变,停止!
// /
// 3 ← 新插入的结点
// 场景2:bf从0变成1 → 继续向上
//
// 5 5
// / \ → / \
// 3 8 3 8
// \
// 9 ← 新插入
// 5的bf: 0→1,子树高度变了,需要继续向上更新!
// 场景3:bf从1变成2 → 旋转!
//
// 5 8
// \ → / \
// 8 5 9 5的bf从1变2,左单旋!
// \
// 9 ← 新插入
💡 记忆口诀 :0停、1传、2转。bf变成0就停,变成±1就往上传,变成±2就旋转。
五、四种旋转详解
旋转原则
- 保持搜索树的规则(中序遍历仍然有序)
- 让旋转的树从不平衡变平衡,同时降低高度
旋转分为四种:右单旋、左单旋、左右双旋、右左双旋。
5.1 右单旋(RotateR)
简介
适用场景:左边太高(parent->_bf == -2 且 cur->_bf == -1),即"左边的左边高"。
核心思路:把 parent 往右下方"压",让左孩子 subL 上来当新的根。subL 的右子树 subLR 变成 parent 的左子树。
图解:
旋转前: 旋转后:
10 5
/ \ / \
5 c → a 10
/ \ / \
a b b c
a/b/c是高度为h的子树
在a中插入导致10的bf从-1变-2
旋转后:5成为新根,b变成10的左子树
高度恢复到插入前的h+2
代码示例
右单旋实现------parent左子树太高时右旋
cpp
void RotateR(Node* parent)
{
Node* subL = parent->_left; // parent的左孩子
Node* subLR = subL->_right; // subL的右子树(需要转移给parent)
// 1. subLR变成parent的左子树
parent->_left = subLR;
if (subLR) // subLR可能为空!必须判断
subLR->_parent = parent;
// 2. parent变成subL的右子树
Node* parentParent = parent->_parent; // 先保存parent的父亲
subL->_right = parent;
parent->_parent = subL;
// 3. 处理subL与上层(parentParent)的链接
if (parentParent == nullptr)
{
// parent原来是整棵树的根
_root = subL;
subL->_parent = nullptr;
}
else
{
// parent原来是局部子树,需要和上层链接
if (parent == parentParent->_left)
{
parentParent->_left = subL;
}
else
{
parentParent->_right = subL;
}
subL->_parent = parentParent;
}
// 4. 更新平衡因子
parent->_bf = subL->_bf = 0;
}
经典Bug:subLR为空未判断
cpp
// ❌ 错误写法:直接解引用subLR,可能空指针崩溃
parent->_left = subLR;
subLR->_parent = parent; // 如果subLR是nullptr,直接崩!
// ✅ 正确写法:先判断是否为空
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
5.2 左单旋(RotateL)
简介
适用场景:右边太高(parent->_bf == 2 且 cur->_bf == 1),即"右边的右边高"。
核心思路:和右单旋对称。把 parent 往左下方"压",让右孩子 subR 上来当新的根。subR 的左子树 subRL 变成 parent 的右子树。
图解:
旋转前: 旋转后:
10 15
/ \ / \
a 15 → 10 c
/ \ / \
b c a b
在c中插入导致10的bf从1变2
旋转后:15成为新根,b变成10的右子树
代码示例
左单旋实现------parent右子树太高时左旋
cpp
void RotateL(Node* parent)
{
Node* subR = parent->_right; // parent的右孩子
Node* subRL = subR->_left; // subR的左子树(需要转移给parent)
// 1. subRL变成parent的右子树
parent->_right = subRL;
if (subRL) // subRL可能为空!
subRL->_parent = parent;
// 2. parent变成subR的左子树
Node* parentParent = parent->_parent;
subR->_left = parent;
parent->_parent = subR;
// 3. 处理subR与上层的链接
if (parentParent == nullptr)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (parent == parentParent->_left)
{
parentParent->_left = subR;
}
else
{
parentParent->_right = subR;
}
subR->_parent = parentParent;
}
// 4. 更新平衡因子
parent->_bf = subR->_bf = 0;
}
💡 规律:左单旋和右单旋完全对称。左单旋操作右孩子,右单旋操作左孩子。代码结构一模一样,只是左右互换。
5.3 左右双旋(RotateLR)
简介
适用场景:左边高,但插入在左孩子的右子树上(parent->_bf == -2 且 cur->_bf == 1),即"左边的右边高"。
核心思路:右单旋解决不了这个问题!因为问题不在"纯粹的左边高",而是左孩子 subL 的右边高。需要两步:
- 先以 subL 为旋转点做左单旋(把右边的"折"拉直)
- 再以 parent 为旋转点做右单旋
图解:
旋转前: 第一步:左旋subL 第二步:右旋parent
10 10 8
/ \ / \ / \
5 c → 8 c → 5 10
\ / \ / \ / \
8 5 b a b b' c
/ \ /
a b' a
插入在b'位置(左孩子的右子树)
单次右旋无法解决,需要先左旋拉直,再右旋平衡
三种子场景
双旋后平衡因子的更新取决于新结点插入在哪个位置:
| 场景 | subLR->_bf(旋转前) | 旋转后 subL->_bf | 旋转后 parent->_bf | 含义 |
|---|---|---|---|---|
| 场景1 | -1 | 0 | 1 | 插入在 subLR 的左子树 |
| 场景2 | 1 | -1 | 0 | 插入在 subLR 的右子树 |
| 场景3 | 0 | 0 | 0 | subLR 本身就是新插入的结点 |
代码示例
左右双旋实现------先左旋再右旋 + 分场景更新平衡因子
cpp
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right; // subLR是双旋的关键结点
int bf = subLR->_bf; // 旋转前先保存subLR的平衡因子
// 第一步:以subL为旋转点左单旋
RotateL(parent->_left);
// 第二步:以parent为旋转点右单旋
RotateR(parent);
// 第三步:根据subLR旋转前的平衡因子,分场景更新
if (bf == 0)
{
// 场景3:subLR本身就是新增结点
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 0;
}
else if (bf == -1)
{
// 场景1:新增结点在subLR的左子树
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 1;
}
else if (bf == 1)
{
// 场景2:新增结点在subLR的右子树
subL->_bf = -1;
subLR->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false); // 理论上不可能
}
}
💡 关键点 :双旋调用了两次单旋,但单旋内部会把平衡因子都设为0,所以双旋需要在调用单旋之前 保存 subLR 的平衡因子,然后在单旋之后手动修正三个结点的平衡因子。
经典Bug:双旋后平衡因子全设为0
cpp
// ❌ 错误写法:双旋后直接全设为0
void RotateLR(Node* parent)
{
RotateL(parent->_left);
RotateR(parent);
// 单旋内部已经把bf设为0了,但双旋场景下不全都是0!
}
// ✅ 正确做法:保存subLR的bf,旋转后分场景更新
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf; // 先保存!
RotateL(parent->_left);
RotateR(parent);
// 根据bf分三种情况更新...
}
5.4 右左双旋(RotateRL)
简介
适用场景:右边高,但插入在右孩子的左子树上(parent->_bf == 2 且 cur->_bf == -1),即"右边的左边高"。
核心思路:与左右双旋完全对称。两步:
- 先以 subR 为旋转点做右单旋(把左边的"折"拉直)
- 再以 parent 为旋转点做左单旋
图解:
旋转前: 第一步:右旋subR 第二步:左旋parent
10 10 12
/ \ / \ / \
a 15 → a 12 → 10 15
/ / \ / \ \
12 b 15 a b c
\
c
插入在c位置(右孩子的左子树的右子树)
先右旋拉直,再左旋平衡
三种子场景
| 场景 | subRL->_bf(旋转前) | 旋转后 parent->_bf | 旋转后 subR->_bf | 含义 |
|---|---|---|---|---|
| 场景1 | -1 | 0 | 1 | 插入在 subRL 的左子树 |
| 场景2 | 1 | -1 | 0 | 插入在 subRL 的右子树 |
| 场景3 | 0 | 0 | 0 | subRL 本身就是新结点 |
代码示例
右左双旋实现------先右旋再左旋 + 分场景更新平衡因子
cpp
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left; // subRL是双旋的关键结点
int bf = subRL->_bf; // 旋转前先保存
// 第一步:以subR为旋转点右单旋
RotateR(parent->_right);
// 第二步:以parent为旋转点左单旋
RotateL(parent);
// 第三步:根据subRL旋转前的平衡因子,分场景更新
if (bf == 0)
{
// 场景3:subRL本身就是新增结点
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = 0;
}
else if (bf == 1)
{
// 场景2:新增结点在subRL的右子树
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1)
{
// 场景1:新增结点在subRL的左子树
subR->_bf = 1;
subRL->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
四种旋转总结对比
| 旋转类型 | 触发条件 (parent, cur) | 核心操作 | 适用场景 |
|---|---|---|---|
| 右单旋 RotateR | (-2, -1) | parent右旋,subL上位 | 左边的左边高 |
| 左单旋 RotateL | (2, 1) | parent左旋,subR上位 | 右边的右边高 |
| 左右双旋 RotateLR | (-2, 1) | 先左旋subL,再右旋parent | 左边的右边高 |
| 右左双旋 RotateRL | (2, -1) | 先右旋subR,再左旋parent | 右边的左边高 |
💡 记忆技巧:
- 单旋:直线型失衡 → 一次旋转解决
- 双旋:折线型失衡 → 先把折线拉直(单旋一次),再平衡(单旋一次)
六、AVL树的查找
1. 简介
AVL 树的查找逻辑与普通 BST 完全相同:从根结点开始,比目标值小往右走,大往左走,相等就找到了。由于 AVL 树始终保持平衡,查找效率稳定在 O(logN)。
2. 代码示例
查找操作------与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; // 没找到
}
查找测试
cpp
void TestFind()
{
AVLTree<int, int> t;
int a[] = {16, 3, 7, 11, 9, 26, 18, 14, 15};
for (auto e : a)
t.Insert({e, e});
// 测试查找存在的值
auto node = t.Find(11);
if (node)
cout << "找到: " << node->_kv.first << endl;
else
cout << "没找到" << endl;
// 测试查找不存在的值
node = t.Find(100);
if (node)
cout << "找到: " << node->_kv.first << endl;
else
cout << "没找到100" << endl;
}
// 输出:
// 找到: 11
// 没找到100
七、AVL树的平衡验证
1. 简介
我们实现了 AVL 树,但怎么知道写得对不对?最可靠的方法是:递归计算每个结点左右子树的实际高度差,验证高度差不超过 1,并且和结点存储的平衡因子一致。
2. 代码示例
高度计算函数------递归求子树高度
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;
}
平衡验证函数------检查高度差 + 平衡因子一致性
cpp
bool _IsBalanceTree(Node* root)
{
// 空树也是AVL树
if (nullptr == root)
return true;
// 计算当前结点左右子树的实际高度
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
int diff = rightHeight - leftHeight;
// 检查1:高度差绝对值是否超过1
if (abs(diff) >= 2)
{
cout << root->_kv.first << "高度差异常" << endl;
return false;
}
// 检查2:实际高度差是否与存储的平衡因子一致
if (root->_bf != diff)
{
cout << root->_kv.first << "平衡因子异常" << endl;
return false;
}
// 递归检查左右子树
return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}
// 对外接口
bool IsBalanceTree()
{
return _IsBalanceTree(_root);
}
综合测试------常规用例 + 大量随机值
cpp
void TestAVLTree1()
{
AVLTree<int, int> t;
// 包含双旋场景的测试用例
int a[] = {4, 2, 6, 1, 3, 5, 15, 7, 16, 14};
for (auto e : a)
{
t.Insert({e, e});
}
t.InOrder(); // 中序遍历验证有序性
cout << "是否平衡: " << t.IsBalanceTree() << endl;
}
// 输出:
// 1:1 2:2 3:3 4:4 5:5 6:6 7:7 14:14 15:15 16:16
// 是否平衡: 1
void TestAVLTree2()
{
const int N = 100000;
vector<int> v;
v.reserve(N);
srand(time(0));
for (size_t i = 0; i < N; i++)
{
v.push_back(rand() + i);
}
AVLTree<int, int> t;
for (auto e : v)
{
t.Insert(make_pair(e, e));
}
cout << "是否平衡: " << t.IsBalanceTree() << endl;
cout << "树高度: " << t.Height() << endl;
cout << "结点数: " << t.Size() << endl;
}
// 输出示例:
// 是否平衡: 1
// 树高度: 19 ← 10万结点高度才19,logN级别!
// 结点数: 100000
💡 10 万个结点,树高才 19 层! 这就是 AVL 树的威力------log₂(100000) ≈ 16.6,加上平衡因子的容差,高度控制在 19 左右,查找最多比较 19 次就能找到目标。
八、性能分析
1. 简介
AVL 树的性能优势在于:无论数据以什么顺序插入,它都能保持平衡,增删查的时间复杂度都稳定在 O(logN)。
性能对比
| 对比项 | 普通BST(最差) | 普通BST(最优) | AVL树 |
|---|---|---|---|
| 树高度 | O(N) | O(logN) | O(logN) |
| 查找效率 | O(N) | O(logN) | O(logN) |
| 插入效率 | O(N) | O(logN) | O(logN) |
| 删除效率 | O(N) | O(logN) | O(logN) |
| 是否自平衡 | 否 | 否 | 是 |
| 数据敏感 | 是(有序数据退化) | 是 | 否 |
实际数据验证
| 结点数量 | AVL树高度 | 理论log₂(N) | 查找最大比较次数 |
|---|---|---|---|
| 100 | ~8 | 6.6 | 8 |
| 1,000 | ~11 | 10.0 | 11 |
| 10,000 | ~15 | 13.3 | 15 |
| 100,000 | ~19 | 16.6 | 19 |
| 1,000,000 | ~23 | 19.9 | 23 |
💡 关键结论:AVL 树的高度始终控制在 logN 附近,即使百万级数据,查找也只需要 23 次比较。相比之下,退化的 BST 可能需要比较 100 万次!
AVL 树的适用场景
- 适合:查找密集型场景(查多改少),因为 AVL 树是严格平衡的,查找效率极高
- 不太适合:插入/删除密集型场景,因为严格平衡要求频繁旋转,维护成本较高。这种场景更适合红黑树(放宽平衡条件,减少旋转次数)
💡 延伸 :C++ STL 中的
set和map底层用的是红黑树而不是 AVL 树。红黑树是 AVL 树的"宽松版",它不要求高度差不超过 1,而是通过颜色约束保证最长路径不超过最短路径的 2 倍,从而减少了插入/删除时的旋转次数。我们将在下一篇博客中学习红黑树。
九、完整代码汇总
以下是 AVL 树的完整实现代码,可以直接编译运行:
cpp
#include <iostream>
#include <assert.h>
#include <vector>
#include <ctime>
#include <cstdlib>
using namespace std;
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;
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)
{
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)
RotateL(parent);
else if (parent->_bf == -2 && cur->_bf == -1)
RotateR(parent);
else if (parent->_bf == 2 && cur->_bf == -1)
RotateRL(parent);
else if (parent->_bf == -2 && cur->_bf == 1)
RotateLR(parent);
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;
}
void InOrder() { _InOrder(_root); cout << endl; }
bool IsBalanceTree() { return _IsBalanceTree(_root); }
int Height() { return _Height(_root); }
private:
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR) subLR->_parent = parent;
Node* ppnode = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (ppnode == nullptr) { _root = subL; subL->_parent = nullptr; }
else { if (ppnode->_left == parent) ppnode->_left = subL; else ppnode->_right = subL; subL->_parent = ppnode; }
parent->_bf = subL->_bf = 0;
}
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL) subRL->_parent = parent;
Node* ppnode = parent->_parent;
subR->_left = parent;
parent->_parent = subR;
if (ppnode == nullptr) { _root = subR; subR->_parent = nullptr; }
else { if (ppnode->_left == parent) ppnode->_left = subR; else ppnode->_right = subR; subR->_parent = ppnode; }
parent->_bf = subR->_bf = 0;
}
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(parent->_left);
RotateR(parent);
if (bf == 0) { subL->_bf = 0; subLR->_bf = 0; parent->_bf = 0; }
else if (bf == -1) { subL->_bf = 0; subLR->_bf = 0; parent->_bf = 1; }
else if (bf == 1) { subL->_bf = -1; subLR->_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(parent->_right);
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);
}
void _InOrder(Node* root)
{
if (root == nullptr) return;
_InOrder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << " ";
_InOrder(root->_right);
}
int _Height(Node* root)
{
if (root == nullptr) return 0;
int lh = _Height(root->_left);
int rh = _Height(root->_right);
return lh > rh ? lh + 1 : rh + 1;
}
bool _IsBalanceTree(Node* root)
{
if (root == nullptr) return true;
int lh = _Height(root->_left);
int rh = _Height(root->_right);
int diff = rh - lh;
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);
}
Node* _root = nullptr;
};
// ============ 测试代码 ============
int main()
{
// 测试1:常规用例(包含双旋场景)
AVLTree<int, int> t;
int a[] = {4, 2, 6, 1, 3, 5, 15, 7, 16, 14};
for (auto e : a)
t.Insert({e, e});
cout << "中序遍历: ";
t.InOrder();
cout << "是否平衡: " << t.IsBalanceTree() << endl;
cout << "树高度: " << t.Height() << endl;
// 测试2:大量随机值
const int N = 100000;
vector<int> v;
v.reserve(N);
srand(time(0));
for (size_t i = 0; i < N; i++)
v.push_back(rand() + i);
AVLTree<int, int> t2;
for (auto e : v)
t2.Insert(make_pair(e, e));
cout << "\n=== 10万随机值测试 ===" << endl;
cout << "是否平衡: " << t2.IsBalanceTree() << endl;
cout << "树高度: " << t2.Height() << endl;
return 0;
}
十、总结
AVL 树核心知识点回顾
| 知识点 | 核心内容 |
|---|---|
| 定义 | 自平衡二叉搜索树,左右子树高度差 ≤ 1 |
| 平衡因子 | bf = 右子树高度 - 左子树高度,取值 0/±1 |
| 结点结构 | 三叉链(left/right/parent)+ 平衡因子 |
| 插入 | BST插入 → 向上更新bf → 失衡则旋转 |
| 右单旋 | 左边高(-2, -1),parent右旋 |
| 左单旋 | 右边高(2, 1),parent左旋 |
| 左右双旋 | 左边的右边高(-2, 1),先左旋subL再右旋parent |
| 右左双旋 | 右边的左边高(2, -1),先右旋subR再左旋parent |
| 查找 | 与BST相同,O(logN) |
| 验证 | 递归检查高度差 ≤ 1 且 bf 与实际一致 |
| 性能 | 增删查均 O(logN),高度控制在 logN |
新手容易踩的坑
| Bug | 原因 | 解决方案 |
|---|---|---|
| 空指针崩溃 | subLR/subRL 可能为空 | 旋转时判断 if (subLR) |
| 平衡因子错误 | 双旋后全设为0 | 保存 subLR/subRL 的 bf,分场景更新 |
| 旋转条件错误 | 只看 parent 的 bf | 同时看 parent 和 cur 的 bf |
| 无法向上更新 | 结点没有 parent 指针 | 使用三叉链 |
| parent 链接错误 | 忘记判断 parent 是根还是子树 | 检查 parentParent 是否为 nullptr |
💡 下一篇预告 :我们将学习红黑树 ------AVL 树的"宽松版",也是 C++ STL 中
set和map的真正底层结构。红黑树通过颜色约束代替严格的高度差控制,在保持 O(logN) 效率的同时减少了旋转次数,是工程实践中更常用的自平衡树。
如果觉得本文有帮助,别忘了 点赞收藏 + 关注 哦!有问题欢迎评论区交流~ 🌸