前言
在二叉搜索树(BST)的使用中,我们会遇到一个致命问题:当数据有序插入时,普通二叉搜索树会退化成单链表,增删查改的时间复杂度从理想的O(logN)骤降至O(N),完全失去了搜索树的性能优势。
为了解决这个问题,前苏联科学家 G. M. Adelson-Velsky 和 E. M. Landis 在 1962 年的论文中提出了AVL 树 ------ 这是世界上第一个自平衡二叉搜索树,通过严格控制树的左右子树高度差,保证树的高度始终稳定在O(logN)级别,让所有操作的时间复杂度都能稳定在O(logN),从根本上解决了普通 BST 的退化问题。
一、AVL 树的核心概念
1.1 AVL 树的定义
AVL 树本质是一颗高度平衡的二叉搜索树,它要么是空树,要么必须同时满足以下两个性质:
- 它的左右子树都是 AVL 树;
- 左右子树的高度差的绝对值不超过 1。
1.2 平衡因子(Balance Factor)
为了方便监控和控制树的平衡状态,我们引入 ** 平衡因子(BF)** 的概念:
结点的平衡因子 = 该结点右子树的高度 - 左子树的高度
根据 AVL 树的定义,任何结点的平衡因子只能是 0、1、-1,一旦平衡因子的绝对值超过 1,就说明该结点所在的子树已经不平衡,需要进行旋转调整。平衡因子就像树平衡状态的 "风向标",让我们无需每次都递归计算子树高度,就能快速判断平衡状态。
1.3 设计细节:为什么高度差不超过 1,而不是 0?
很多初学者会有疑问:左右子树高度完全相等(高度差 0)不是更平衡吗?答案是:理想很美好,但现实中无法实现。比如当树只有 2 个结点、4 个结点时,无论怎么排列,都无法做到左右子树高度差为 0,最优情况就是高度差为 1。因此 AVL 树设计为 "高度差绝对值不超过 1",在保证平衡的同时,兼顾了实现的可行性。
1.4 性能优势
AVL 树的结点分布和完全二叉树高度相似,整棵树的高度可以严格控制在logN级别,因此无论是插入、删除还是查找操作,时间复杂度都能稳定在O(logN),相比普通二叉搜索树有了本质的性能提升。
二、AVL 树的结点结构设计
AVL 树的结点需要存储键值对、孩子指针、父指针和平衡因子,其中父指针是关键------ 插入新结点后,我们需要从新增结点向上遍历祖先结点更新平衡因子,父指针能让这个过程变得非常便捷。
完整的结点与 AVL 树类框架代码如下:
cpp
#include <iostream>
using namespace std;
// AVL树结点结构
template<class K, class V>
struct AVLTreeNode
{
pair<K, V> _kv; // 键值对,保证key的唯一性
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:
// 成员函数声明,后续逐步实现
bool Insert(const pair<K, V>& kv);
Node* Find(const K& key);
int Height();
bool IsBalanceTree();
private:
// 内部私有成员函数
void RotateR(Node* parent);
void RotateL(Node* parent);
void RotateLR(Node* parent);
void RotateRL(Node* parent);
int _Height(Node* root);
bool _IsBalanceTree(Node* root);
// 根结点
Node* _root = nullptr;
};
三、AVL 树的插入操作
插入是 AVL 树最核心的操作,分为两大核心阶段:按照二叉搜索树规则插入新结点 、向上更新平衡因子并处理不平衡。
3.1 插入的整体流程
AVL 树的插入分为 4 个核心步骤:
- 按照二叉搜索树的规则,找到合适的插入位置,创建并插入新结点;
- 新增结点只会影响其祖先结点的高度,因此从新增结点的父结点开始,向上更新路径上所有祖先结点的平衡因子;
- 更新过程中,若所有结点的平衡因子始终合法(0/1/-1),没有出现不平衡,插入直接结束;
- 若更新中出现平衡因子为 2 或 - 2,说明该结点所在子树已不平衡,需要对该子树进行旋转调平衡;旋转后子树高度会恢复到插入前的水平,不会影响上层结点,插入结束。
3.2 平衡因子的更新规则
平衡因子的更新是插入操作的核心,我们需要严格遵循以下规则:
更新原则
- 平衡因子 = 右子树高度 - 左子树高度;
- 只有子树高度发生变化,才会影响父结点的平衡因子;
- 新增结点在父结点的左子树,父结点的平衡因子
--;新增结点在父结点的右子树,父结点的平衡因子++。
更新停止条件
更新过程中,根据父结点更新后的平衡因子,分为三种情况决定是否继续向上更新:
- 更新后父结点平衡因子 = 0 :说明更新前父结点的平衡因子是 1 或 - 1,新增结点补在了矮的那一侧,插入后父结点所在子树的整体高度不变,不会影响上层祖先的平衡因子,直接停止更新。
- 更新后父结点平衡因子 = 1 或 - 1 :说明更新前父结点的平衡因子是 0,新增结点让子树一侧变高,父结点所在子树的整体高度 + 1,会影响上层祖先的平衡因子,需要继续向上更新。
- 更新后父结点平衡因子 = 2 或 - 2 :说明父结点所在子树已经违反平衡规则,出现了不平衡,需要立即进行旋转调平衡,旋转后停止更新。
3.3 插入操作的完整代码实现
结合上述流程和规则,我们实现完整的 Insert 函数,其中旋转调平衡的逻辑会在后续章节详细实现:
cpp
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. 按照二叉搜索树规则,找到插入位置
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. 创建新结点,链接到父结点
cur = new Node(kv);
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;
// 4. 向上更新平衡因子,并处理不平衡
while (parent)
{
// 更新当前父结点的平衡因子
if (cur == parent->_left)
parent->_bf--;
else
parent->_bf++;
// 根据平衡因子判断后续操作
if (parent->_bf == 0)
{
// 子树高度不变,停止更新
break;
}
else if (parent->_bf == 1 || parent->_bf == -1)
{
// 子树高度+1,继续向上更新
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
// 子树不平衡,旋转调平衡
if (parent->_bf == 2)
{
if (parent->_right->_bf == 1)
{
// 右右场景,左单旋
RotateL(parent);
}
else if (parent->_right->_bf == -1)
{
// 右左场景,右左双旋
RotateRL(parent);
}
}
else // parent->_bf == -2
{
if (parent->_left->_bf == -1)
{
// 左左场景,右单旋
RotateR(parent);
}
else if (parent->_left->_bf == 1)
{
// 左右场景,左右双旋
RotateLR(parent);
}
}
// 旋转后子树高度恢复,无需继续向上更新
break;
}
else
{
// 平衡因子异常,触发断言
assert(false);
}
}
return true;
}
四、AVL 树的核心:旋转调平衡
当插入结点导致子树不平衡时,我们需要通过旋转来恢复平衡,旋转必须遵循两个核心原则:
- 旋转后必须保持二叉搜索树的性质(左子树所有结点 < 根结点 < 右子树所有结点);
- 让不平衡的子树恢复平衡,同时将子树的高度恢复到插入前的水平,彻底消除对上层结点的影响。
根据不平衡的场景,旋转分为四种:右单旋、左单旋、左右双旋、右左双旋,下面我们逐一拆解。
4.1 右单旋(左左场景)
触发场景
当不平衡结点的平衡因子为-2,且其左孩子的平衡因子为-1时触发,也就是新结点插入在不平衡结点左孩子的左子树中,属于纯粹的 "左边高",我们称之为左左场景。
旋转原理
我们用抽象模型来讲解:parent是不平衡的根结点,subL是parent的左孩子,subLR是subL的右子树,a/b/c是高度为h的 AVL 子树。
- 二叉搜索树规则保证:
subL < subLR < parent; - 旋转核心步骤:将
subLR变成parent的左子树,将parent变成subL的右子树,最终subL成为这棵子树新的根结点; - 旋转后,子树高度恢复到插入前的水平,
parent和subR的平衡因子都置为 0。
代码实现
cpp
template<class K, class V>
void AVLTree<K, V>::RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
// 1. 修改孩子指针,保持二叉搜索树规则
parent->_left = subLR;
subL->_right = parent;
// 2. 处理subLR的父指针
if (subLR)
subLR->_parent = parent;
// 3. 保存parent的父结点,处理上层链接
Node* parentParent = parent->_parent;
// 4. 更新parent和subL的父指针
parent->_parent = subL;
// 5. 处理子树与上层的链接
if (parentParent == nullptr)
{
// parent是整棵树的根,更新_root
_root = subL;
subL->_parent = nullptr;
}
else
{
// parent是局部子树,链接到父结点
if (parent == parentParent->_left)
parentParent->_left = subL;
else
parentParent->_right = subL;
subL->_parent = parentParent;
}
// 6. 更新平衡因子
parent->_bf = subL->_bf = 0;
}
4.2 左单旋(右右场景)
触发场景
当不平衡结点的平衡因子为2,且其右孩子的平衡因子为1时触发,也就是新结点插入在不平衡结点右孩子的右子树中,属于纯粹的 "右边高",我们称之为右右场景。
旋转原理
左单旋是右单旋的完全对称操作:parent是不平衡的根结点,subR是parent的右孩子,subRL是subR的左子树。
- 二叉搜索树规则保证:
parent < subRL < subR; - 旋转核心步骤:将
subRL变成parent的右子树,将parent变成subR的左子树,最终subR成为这棵子树新的根结点; - 旋转后,子树高度恢复到插入前的水平,
parent和subR的平衡因子都置为 0。
代码实现
cpp
template<class K, class V>
void AVLTree<K, V>::RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
// 1. 修改孩子指针
parent->_right = subRL;
subR->_left = parent;
// 2. 处理subRL的父指针
if (subRL)
subRL->_parent = parent;
// 3. 保存parent的父结点
Node* parentParent = parent->_parent;
// 4. 更新parent和subR的父指针
parent->_parent = subR;
// 5. 处理子树与上层的链接
if (parentParent == nullptr)
{
// parent是整棵树的根
_root = subR;
subR->_parent = nullptr;
}
else
{
// parent是局部子树
if (parent == parentParent->_left)
parentParent->_left = subR;
else
parentParent->_right = subR;
subR->_parent = parentParent;
}
// 6. 更新平衡因子
parent->_bf = subR->_bf = 0;
}
4.3 左右双旋(左右场景)
触发场景
当不平衡结点的平衡因子为-2,且其左孩子的平衡因子为1时触发,也就是新结点插入在不平衡结点左孩子的右子树中,我们称之为左右场景。
这种场景下,单纯的右单旋无法解决问题,旋转后树依然不平衡,需要分两步完成:
- 先对不平衡结点的左孩子,执行一次左单旋;
- 再对不平衡结点本身,执行一次右单旋。
平衡因子更新规则
双旋的核心难点在于平衡因子的更新,需要根据中间结点subLR(左孩子的右子树根)的平衡因子,分三种场景处理:
subLR->_bf == 0:旋转后,subL、subLR、parent的平衡因子全部置为 0;subLR->_bf == -1:旋转后,subL->_bf=0,subLR->_bf=0,parent->_bf=1;subLR->_bf == 1:旋转后,subL->_bf=-1,subLR->_bf=0,parent->_bf=0。
代码实现
cpp
template<class K, class V>
void AVLTree<K, V>::RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
// 保存subLR的平衡因子,用于后续更新
int bf = subLR->_bf;
// 1. 先对左孩子执行左单旋
RotateL(parent->_left);
// 2. 再对不平衡结点执行右单旋
RotateR(parent);
// 3. 根据subLR的原始平衡因子,更新各结点平衡因子
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);
}
}
4.4 右左双旋(右左场景)
触发场景
当不平衡结点的平衡因子为2,且其右孩子的平衡因子为-1时触发,也就是新结点插入在不平衡结点右孩子的左子树中,我们称之为右左场景。
这是左右双旋的对称场景,同样需要两步旋转:
- 先对不平衡结点的右孩子,执行一次右单旋;
- 再对不平衡结点本身,执行一次左单旋。
平衡因子更新规则
根据中间结点subRL(右孩子的左子树根)的平衡因子,分三种场景处理:
subRL->_bf == 0:旋转后,subR、subRL、parent的平衡因子全部置为 0;subRL->_bf == 1:旋转后,subR->_bf=0,subRL->_bf=0,parent->_bf=-1;subRL->_bf == -1:旋转后,subR->_bf=1,subRL->_bf=0,parent->_bf=0。
代码实现
cpp
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. 先对右孩子执行右单旋
RotateR(parent->_right);
// 2. 再对不平衡结点执行左单旋
RotateL(parent);
// 3. 更新平衡因子
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);
}
}
五、AVL 树的查找操作
AVL 树的查找完全遵循二叉搜索树的规则,从根结点开始,比当前结点 key 大就走右子树,比当前结点 key 小就走左子树,相等则找到,遍历到空则说明 key 不存在。
得益于 AVL 树的高度平衡特性,查找的时间复杂度稳定在O(logN),不会出现普通 BST 的退化问题。
代码实现
cpp
template<class K, class V>
typename AVLTree<K, V>::Node* AVLTree<K, V>::Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_kv.first < key)
{
// key更大,走右子树
cur = cur->_right;
}
else if (cur->_kv.first > key)
{
// key更小,走左子树
cur = cur->_left;
}
else
{
// 找到目标结点
return cur;
}
}
// 未找到
return nullptr;
}
六、AVL 树的平衡校验
写完 AVL 树的核心逻辑后,我们需要通过程序反向验证树的平衡性,确保代码实现的正确性,同时通过测试用例验证功能和性能。
6.1 树高度计算与平衡校验
我们需要实现两个核心工具函数:
_Height:递归计算子树的高度;_IsBalanceTree:递归校验整棵树是否符合 AVL 树的规则,检查两点:- 每个结点的左右子树高度差绝对值是否小于 2;
- 结点存储的平衡因子,是否等于实际计算的 "右子树高度 - 左子树高度";
- 左右子树是否都是合法的 AVL 树。
代码实现
cpp
// 计算子树高度
template<class K, class V>
int AVLTree<K, V>::_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;
}
// 对外暴露的高度计算接口
template<class K, class V>
int AVLTree<K, V>::Height()
{
return _Height(_root);
}
// 内部平衡校验函数
template<class K, class V>
bool AVLTree<K, V>::_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 << " 结点高度差异常,高度差:" << diff << endl;
return false;
}
// 校验2:存储的平衡因子是否与实际高度差一致
if (root->_bf != diff)
{
cout << root->_kv.first << " 结点平衡因子异常,存储BF:" << root->_bf
<< ",实际BF:" << diff << endl;
return false;
}
// 递归校验左右子树
return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}
// 对外暴露的平衡校验接口
template<class K, class V>
bool AVLTree<K, V>::IsBalanceTree()
{
return _IsBalanceTree(_root);
}
七、总结
AVL 树作为最早的自平衡二叉搜索树,通过严格的高度差控制,彻底解决了普通二叉搜索树的退化问题,实现了稳定的O(logN)级别的增删查改操作。
本文完整覆盖了 AVL 树的核心知识点:
- 核心概念:平衡定义、平衡因子的设计与作用;
- 结点设计:带父指针的三叉链结构,方便平衡因子更新;
- 插入操作:BST 插入 + 平衡因子更新的完整流程;
- 旋转调平衡:四种旋转场景的原理与代码实现,这是 AVL 树的核心;
- 查找与校验:查找逻辑与平衡校验方法,确保实现的正确性。
关于 AVL 树的删除操作,其核心逻辑是先按照二叉搜索树规则删除结点,再从删除位置向上更新平衡因子,遇到不平衡时同样通过旋转调平衡,实现复杂度较高,有兴趣的同学可以参考《殷人昆 数据结构:用面向对象方法与 C++ 语言描述》中的详细讲解。
在实际工程中,AVL 树因为严格的平衡要求,插入删除时的旋转次数较多,更适合查询频繁、插入删除较少的场景;而后续出现的红黑树,通过更宽松的平衡规则,减少了旋转次数,成为了工业界更主流的选择。但 AVL 树作为自平衡搜索树的基础,是理解红黑树、B + 树等高级数据结构的必经之路。