前言:
二叉搜索树(BST)的理想时间复杂度为O(log n),但在极端情况下会退化为O(n)的链表结构。为解决这一问题,苏联数学家Adelson-Velsky和Landis于1962年提出了AVL树这一自平衡二叉搜索树。
AVL树通过引入平衡因子和旋转操作,始终保持树的严格平衡状态,确保各项操作稳定在O(log n)时间复杂度。其核心在于:每个节点的左右子树高度差(平衡因子)不超过1,当插入或删除操作破坏平衡时,通过四种基本旋转(左旋、右旋、左右旋、右左旋)自动调整恢复平衡。
本文将系统解析AVL树的平衡原理:从平衡因子的数学定义出发,详细讲解四种旋转操作的触发条件和执行过程,并提供模块化的C++实现。掌握AVL树不仅能深入理解自平衡机制,更为红黑树等高级数据结构的学习奠定基础。
目录

一、AVL树的定义
AVL树:是一种 自平衡二叉搜索树,由苏联数学家 Georgy Adelson-Velsky 和 Evgenii Landis 在 1962 年提出,其名称来源于这两位发明者的名字缩写。
AVL树要么是空树,要么是满足以下性质的二叉搜索树:
其左、右子树也都是 AVL 树
并且左、右子树的高度差的绝对值不超过 1

二、AVL树的特点
- 严格的平衡特性:保证该节点的左右子树平衡因子绝对值控制在【0,1】,避免极端情况
- 旋转平衡机制:如果出现极端情况,会通过旋转来恢复平衡(后面会细说)
- 高效操作复杂度:查找、插入、删除操作均保持O(log n)时间复杂度,平衡因子维护使旋转操作最多影响O(log n)个节点
- 性能趋势:适用于频繁查询,查询效率比红黑树更加的稳定
三、AVL树的节点结构
每个树节点需要涉及到下面几个变量:
数据存储(pair< >)
左右指针(left,right)
父节点指针(parent)
平衡因子(banance)(左右子树的高度差值)
如下
//节点
template<class K,class V>
struct AVL_TreeNode
{
AVL_TreeNode(const pair<K, V>& _date)
:date(_date)
,parent(nullptr)
,left(nullptr)
,right(nullptr)
,balance(0)
{ }
//数据
pair<K, V> date;
//节点指针
AVL_TreeNode<K, V>* parent;
AVL_TreeNode<K, V>* left;
AVL_TreeNode<K, V>* right;
//平衡因子
int balance;
};
四、AVL树的插入操作
AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。
那么AVL树的插入过程可以分为两步:
1. 按照二叉搜索树的方式插入新节点
2. 调整节点的平衡因子
1.判断
我们首先需要判断这棵树是否为空,空则直接创建一个根节点即可。
if (root == nullptr)
{
root = new Node(date);
return true;
}
2.链接
//找插入位置
Node* parent = nullptr;
Node* cur = root;
while (cur)
{
//如果date>cur->date,右子树
parent = cur;
if (date.first > cur->date.first)
{
cur = cur->right;
}
else if (date.first < cur->date.first)
{
cur = cur->left;
}
else
{
//如果值相等,不符合key唯一条件
return false;
}
}
//插入连接节点
cur = new Node(date);
if (parent->date.first > date.first)
{
parent->left = cur;
}
else
{
parent->right = cur;
}
//连接父节点(重点)
cur->parent = parent;
3.旋转
这里我们首先来看一下有哪些旋转方式,再一一来看:
右单旋(RR 旋转):处理 LL 型失衡
左单旋(LL 旋转):处理 RR 型失衡
左右双旋(RL 旋转):处理 RL 型失衡
右左双旋(LR 旋转):处理 LR 型失衡
1.左单旋
仅仅树的右边不平衡,那么想要右边平衡,只能向左调整
调整方法:将右子树往左边调,可以发现,中间节点是处于临界状态,那么可以如下调整:

这里我们可以看到,左单旋的时候,图中的b旋转成为了30的右孩子,但是要是没有b呢?
那就直接令30取代b的位置就可以了。
//单左旋转
void Whirl_L(Node* parent)
{
//标记节点
Node* cur = parent->right;
Node* curleft = cur->left;
Node* pphead = parent->parent;
//连接parent和cur
cur->left = parent;
parent->parent = cur;
//连接curright和parent
parent->right = curleft;
if (curleft)
{
curleft->parent = parent;
}
//如果pphead为空,说明pphead是root的根节点
if (pphead)
{
//确定cur和pphead链接位置
cur->parent = pphead;
if (pphead->left == parent)
{
pphead->left = cur;
}
else
{
pphead->right = cur;
}
}
else
{
root = cur;
cur->parent = nullptr;
}
//更新cur和parent的平衡因子
cur->balance = 0;
parent->balance = 0;
}
2.右单旋
我们再来体会一下右单旋,本质上是镜像的左单旋。

上图在插入前,AVL树是平衡的,新节点插入到30的左子树(注意:此处不是左孩子)中,30左子树增加了一层,导致以60为根的二叉树不平衡,要让60平衡,只能将60左子树的高度减少一层,右子树增加一层,即将左子树往上提,这样60转下来,因为60比30大,只能将其放在30的右子树,而如果30有右子树,右子树根的值一定大于30,小于60,只能将其放在60的左子树,旋转完成后,更新节点的平衡因子即可。
在旋转过程中,有以下几种情况需要考虑:
1. 30节点的右孩子可能存在,也可能不存在
2. 60可能是根节点,也可能是子树
如果是根节点,旋转完成后,要更新根节点
如果是子树,可能是某个节点的左子树,也可能是右子树
//单右旋转
void Whirl_R(Node* parent)
{
//标记节点
Node* cur = parent->left;
Node* curright = cur->right;
Node* pphead = parent->parent;
//连接parent和cur
cur->right = parent;
parent->parent = cur;
//连接curright和parent
parent->left = curright;
if (curright)
{
curright->parent = parent;
}
//连接cur和pphead(注意:pphead可能是root->parent是空
if (pphead)
{
//判断cur的连接位置
cur->parent = pphead;
if (pphead->left == parent)
{
pphead->left = cur;
}
else
{
pphead->right = cur;
}
}
else
{
root = cur;
cur->parent = nullptr;
}
//更新平衡因子
cur->balance = 0;
parent->balance = 0;
}
3.左右双旋
当在ptr的左子树的右子树中插入一个结点后,造成了ptr平衡因子为-2的不平衡,将ptr向下找到当前结点的左孩子的右孩子,先进行左单旋ptr->left = subL,然后将ptr的右子树断开指向subR,此时便完成了旋转,最后将平衡因子进行更新。
//左右双旋
void Whirl_L_R(Node* parent)
{
//标记
Node* cur = parent->right;
Node* curright = cur->right;
int bf = curright->banance;
//左旋
Whirl_L(parent->left);
//右旋
Whirl_R(parent);
//如果在curleft的左右插入节点
if (bf != 0)
{
if (bf == -1)
{
cur->balance = 1;
}
else
{
parent->balance = -1;
}
}
}
4.右左双旋

//右左双旋
void Whirl_R_L(Node* parent)
{
//记录节点
Node* cur = parent->right;
Node* curleft = cur->left;
int bf = curleft->banance;
//右旋
Whirl_R(parent->right);
//左旋
Whirl_L(parent);
//如果在curleft的左右插入节点
if (bf != 0)
{
if (bf == -1)
{
parent->balance = 1;
}
else
{
cur->balance = -1;
}
}
}
5.旋转总结
假如以pParent为根的子树不平衡,即pParent的平衡因子为2或者-2,分以下情况考虑
1. pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR
- 当pSubR的平衡因子为1时,执行左单旋
- 当pSubR的平衡因子为-1时,执行右左双旋
2. pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为pSubL
- 当pSubL的平衡因子为-1是,执行右单旋
- 当pSubL的平衡因子为1时,执行左右双旋
旋转完成后,原pParent为根的子树个高度降低,已经平衡,不需要再向上更新。
五、AVL树的验证
AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:
1. 验证其为二叉搜索树
如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
2. 验证其为平衡树
每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
节点的平衡因子是否计算正确
int _Height(PNode pRoot);
bool _IsBalanceTree(PNode pRoot)
{
// 空树也是AVL树
if (nullptr == pRoot) return true;
// 计算pRoot节点的平衡因子:即pRoot左右子树的高度差
int leftHeight = _Height(pRoot->_pLeft);
int rightHeight = _Height(pRoot->_pRight);
int diff = rightHeight - leftHeight;
// 如果计算出的平衡因子与pRoot的平衡因子不相等,或者
// pRoot平衡因子的绝对值超过1,则一定不是AVL树
if (diff != pRoot->_bf || (diff > 1 || diff < -1))
return false;
// pRoot的左和右如果都是AVL树,则该树一定是AVL树
return _IsBalanceTree(pRoot->_pLeft) && _IsBalanceTree(pRoot-
>_pRight);
}



177万+

被折叠的 条评论
为什么被折叠?



