✨前言:这篇文章会对
AVL树这个较复杂的数据结构进行讲解,重点讲解了对AVL树的四种旋转操作,对于这四种旋转都做了非常详细的画图分析,并且对代码进行了实现,还有对于AVL树的验证代码及AVL树的性能分析也做了介绍.
AVL树详解
🏞️1.AVL树的概念
在前面,我们学习过二叉搜索树,虽然二叉搜索树可以缩短查找效率,但如果数据有序或接近有序二叉搜索树将退化为单边树,查找元素相当于在顺序表中查找,效率低下. 因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度**.
基于上述概念,我们可以得出:一棵AVL树,要么是空树,要么是具有以下性质的二叉搜索树:
- 它的左右子树均为
AVL树 - 左右子树高度之差(简称平衡因子)的绝对值不超过1

上述图中计算平衡因子时采用公式:平衡因子 = 右子树高度 - 左子树高度
如果一棵二叉搜索树是高度平衡的,它就是AVL树. 如果它有n个节点,其高度可保持在 O ( l o g 2 n ) O(log_2n) O(log2n),搜索时间复杂度 O ( l o g 2 n ) O(log_2n) O(log2n).
🌁2. AVL树节点的定义
AVL树节点定义:
template<class K, class V>
struct AVLTreeNode
{
AVLTreeNode(const pair<K, V>& val = pair<K, V>())
: _kv(val)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
{
}
pair<K, V> _kv;
int _bf;
AVLTreeNode* _left;
AVLTreeNode* _right;
AVLTreeNode* _parent;
};
在节点定义中,我们使用kv模型来存储值.
🌠3. AVL树的操作
📖3.1 AVL树的插入
注意:对于AVL树的插入,因为它是要结合AVL树的旋转的,所以在本文中,AVL树的插入和AVL树的旋转合起来才是完整的插入过程,所以这里的3.1 主要讲一下插入的大体的一个过程,具体插入的细节及代码实现都在3.2AVL树的旋转中.
AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树. 那么AVL树的插入可以分为两步:
- 按照二叉搜索树的方式插入新节点
- 调整平衡因子
对于平衡因子的调整,在插入之前,所有节点的平衡因子分为三种情况:0,1,-1插入后,新插入节点可能会使它的父节点的平衡因子发生变化,有这么三种情况:
1.新插入节点的父节点(parent)的平衡因子变成0(一定是由1或-1变成0)

2.新插入节点的父节点(parent)的平衡因子变成-1/1(由0变成1或-1)

3.新插入节点的父节点的平衡因子变成2/-2,此时已经违反了平衡树的性质,需要对其进行旋转处理.
📖3.2 AVL树的旋转
- 新节点插入到较高左子树的左侧:右单旋

对于图中的右单旋,它的规则如下:

对于图中的a,b,c均为抽象节点,可能不太好理解,所以我们也可以将它们设置成实际的节点来进行分析,会更加直观:
对于,图中的抽象模型,为了方便分析,我们可以将它替换成实际节点来看:


当我们通过将h设置为不同的值时,实际的AVL树就会改变,通过画出h = 0和 h = 1的图我们就已经可以分析清楚这种旋转的情况了,对于h=2、3、4........,由于光是h=2时这棵树的样子就可能有36种情况,所以这里便不再一一画出.如果读者感兴趣,可以试着自己画一画,但根据以上h=0和h=1的情况我们就已经可以分析清楚了
最终,根据我们图上所画的这种右单选的情况,我们可以按照上图写出右旋转的代码:
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
{
subLR->_parent = parent;
}
//有可能parent是一个节点的子节点
Node* ppNode = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (ppNode)
{
//parent为ppNode的子节点
if (parent == ppNode->_left)
{
ppNode->_left = subL;
}
else
{
ppNode->_right = subL;
}
subL->_parent = ppNode;
}
else
{
//parent为根结点,旋转后将subL作新的根节点
_root = subL;
subL->_parent == nullptr;
}
//调整平衡因子
subL->_bf = parent->_bf = 0;
}
在写上述的代码时,我们有一个需要注意的地方,当我们发现当前节点的平衡因子发生错误,我们就需要将当前节点传入到RotateR右旋函数进行右单选,但是当前的节点也就是parent节点,它有可能是根结点,也可能是一个节点(在代码中我们用ppNode表示)的子节点,所以我们需要分情况讨论.
- 新节点插入到较高右子树的右侧:左单旋

左单旋的旋转规则如下:

对于左单选的图示中,将抽象节点转换为实际节点进行分析在右单选中已经演示过,两者非常类似,所以这里不再花费篇幅去讲解.
我们根据左单选的旋转规则就可以写出它的代码:
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)
{
if (parent == ppNode->_left)
{
ppNode->_left = subR;
}
else
{
ppNode->_right = subR;
}
subR->_parent = ppNode;
}
else
{
_root = subR;
subR->_parent = nullptr;
}
subR->_bf = parent->_bf = 0;
}
- 新插入节点在较高左子树的右侧:先左单选再右单旋


对于这种左右双旋,它的旋转规则如下:

对于规则中所述的左单旋和右单旋,在文章上面均已讲解,可以参考上面.
在上图所画的节点均为抽象节点,对于这种左右双旋的情况,我们也可以将抽象节点代替成成实际节点来分析一下:

当h=0时:

当h=1时:在这里要注意,当h=1时,我们在插入新节点的时候,25的左子树和右子树均可以插入,所以就有两种情况我们先来看第一种:新节点插入在25的左子树

新节点插入在25的右子树:

我们分别分析了h=0、h=1时的情况,对左右双旋的这种情况进一步的加深理解,对于分析过程中,我们应该还会发现一个问题,那就是最终调整完之后的平衡因子调整问题:
我们发现,对上面的情况,每一棵树在插入新节点后,它们的subLR的平衡因子都各不相同,而且对应最终平衡因子需要调整的节点(parent、subL、subLR),它们调整后的值也是分为了三种情况的:

所以,由此,我们可以总结出最终的平衡因子调整规则:
- 当
subLR = 0时:调整为subLR = 0,subL = 0,parent = 0 - 当
subLR = -1时:调整为subLR = 0,subL = 0,parent = 1 - 当
subLR = 1时:调整为subLR = 0,subL = -1,parent = 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)
{
parent->_bf = 0;
subL->_bf = 0;
subLR->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 1;
subL->_bf = 0;
subLR->_bf = 0;
}
else if (bf == 1)
{
parent->_bf = 0;
subL->_bf = -1;
subLR->_bf = 0;
}
else
{
//如果走到这里,说明在旋转之前就已经有错,直接断言
assert(false);
}
}
- 新节点插入到较高右子树的左侧:先右单旋再左单旋
同样的,对于右左双旋的这种情况,在插入新节点的时,也会有两个插入位置,所以也要分情况来看:
新节点插入在25的左边:


新节点插入在25的右边:


对于平衡因子的调整,上述讨论已经展现出了两种调整情况,但它和左右双旋一样,也是有三种的调整情况,所以我们需要再分析一下当h=0</

本文详细介绍了AVL树的概念、节点定义以及插入、删除操作,重点讨论了四种旋转操作:右单旋、左单旋、左右双旋和右左双旋,同时分析了插入和删除时的平衡因子调整。此外,还涵盖了AVL树的性能优势和验证方法,展示了其实现代码。
最低0.47元/天 解锁文章
1131





