Hello,大家好,这一篇博客我们来讲解一下数据结构中的AVL树这一部分的内容,AVL树属于是数据结构的一部分,顾名思义,AVL树是一棵特殊的搜索二叉树,我们接下来要讲的这篇博客是建立在了解搜索二叉树这个知识点的基础之上的,因此,我在这里建议大家可以先去看看我之前写过的那片有关搜索二叉树内容的博客,为了方便大家寻找,链接就放到下面了:
目录
[1 AVL树的概念](#1 AVL树的概念)
[2 AVL树的实现](#2 AVL树的实现)
[2.1 AVL树的结构](#2.1 AVL树的结构)
[2.2 AVL树的插入](#2.2 AVL树的插入)
[2.2.1 AVL树插入值的具体过程](#2.2.1 AVL树插入值的具体过程)
[2.2.2 平衡因子的更新](#2.2.2 平衡因子的更新)
[2.2.2.1 更新原则](#2.2.2.1 更新原则)
[2.2.2.2 更新停止条件](#2.2.2.2 更新停止条件)
[2.2.2.3 插入节点及更换平衡因子的代码实现](#2.2.2.3 插入节点及更换平衡因子的代码实现)
[2.3 右旋转](#2.3 右旋转)
[2.3.1 旋转的原则](#2.3.1 旋转的原则)
[2.3.2 右单旋](#2.3.2 右单旋)
[2.3.3 代码实现右单旋](#2.3.3 代码实现右单旋)
[2.4 左旋转(左单旋)](#2.4 左旋转(左单旋))
[2.4.1 代码实现左单旋](#2.4.1 代码实现左单旋)
[2.5 左右双旋](#2.5 左右双旋)
[2.5.1 前情提要](#2.5.1 前情提要)
[2.5.2 深度解析](#2.5.2 深度解析)
[2.5.3 代码实现左右双旋](#2.5.3 代码实现左右双旋)
[2.6 右左双旋](#2.6 右左双旋)
[2.6.1 旋转方法](#2.6.1 旋转方法)
[2.6.2 代码实现右左双旋](#2.6.2 代码实现右左双旋)
[2.7 AVL树的查找](#2.7 AVL树的查找)
[2.8 AVL树的删除](#2.8 AVL树的删除)
1 AVL树的概念
1>.AVL树是最先发明的自平衡二叉查找树,AVL是一棵空树,或者是具备下列性质的二叉搜索树:它的左右子树均为AVL树,且左右子树的高度差的绝对值不超过1。AVL树是一棵高度平衡的二叉搜索树。
2>.AVL树得名于它的发明者:G.M.Adelson-Velsky和E.M.Landis是两个前苏联的科学家,他们在1962年的论文中发表了它。
3>.为了更好地去了解AVL树的实现,我们这里还需引入一个平衡因子的概念,每个节点都有一个平衡因子,任何节点的平衡因子等于右子树的高度减去左子树的高度(右 - 左),也就是说任何节点的平衡因子等于 0 / -1 / 1 ,AVL树并不是需要平衡因子,但是这里有了平衡因子的话就可以更方便我们去观察和控制树是否平衡,就像一个风向标一样。
4>.思考一下为什么AVL树是高度平衡的搜索二叉树,要求高度不超过1,而不是高度差是0呢?0不是更好地平衡吗?其实,通过下面地两幅图我们就不难发现,不是我们不想这样设计,而是在有些情况下是做不到高度差为0的,比如说当有一棵树是2个节点,4个节点等情况下,高度差最好就是1,无法作为高度差是0的这种情况。
5>.AVL树整体节点的数量和分布和完全二叉树类似,高度可以控制在log2 ( N ) ,相比于二叉搜索树有了本质的提升。
2 AVL树的实现
2.1 AVL树的结构
通过我们前面的学习可知AVL树它其实就是一棵二叉搜索树,只不过相较于一棵普通的二叉搜索树而言,AVL树它的左右子树高度差的绝对值不会超过1,是一棵自平衡二叉树,且它是一个个独立的节点构成的,各个节点之间是由指针连接起来的,所以,我们首先来看AVL树各个节点的结构:
cpp
template<class K,class V>
struct AVLTreeNode//这里我们之所以选用struct类,是因为我们在实现后面的各个操作时(如插入,删除,查找等)都要通过节点内部的各个指针才可以完成,因此这里选用struct,因为它默认的访问权限是public,当然,也可以使用class,但是必须加"public"才可以。
{
pair<K, V> _kv;//AVL树中的每个节点中都是要存放元素的,_kv变量就是用来存放元素的,AVL树它也是一棵二叉搜索树,通过我们前面所学的二叉搜索树的两种key和key/value类型,这里AVL树就相当于是key/value类型,因为这个类型更好,key/value可以充当key类型去用,而key不可以充当key/value去用,因此AVL树的元素为pair<K,V>类型的变量。
AVLTreeNode<K, v>* _left;
AVLTreeNode<K, v>* _right;//因为是二叉树,所以每个节点中都必须含有两个指针,一个指向左子树,而另一个指向右子树。
AVLTreeNode<K, v>* _parent;//AVL树中还需要有一个_parent指针,指向当前节点的父节点,后续进行更新平衡因子这一操作时我们就会用到,这个指针的用法我们后面再说,这里先跳过。
int _bf;//balance factor(平衡因子),用来判断以这个节点为根节点的那棵树是否平衡了。
AVLTreeNode(const pair<K, V>& kv)//拷贝构造函数
:_kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
{ }//这里的默认构造和析构函数我们使用编译器自己生成的就足够我们使用了。
};
AVL树的框架:
cpp
template<class K,class V>
class AVLTree//AVL树的结构(部分)
{
typedef AVLTreeNode<K, v> node;
public:
//......;主体函数部分,这里先不写,后面会慢慢地去进行补充。
private:
node* root = nullptr;//指向根节点地指针。
};
2.2 AVL树的插入
2.2.1 AVL树插入值的具体过程
1>.插入一个值按二叉搜索树的规则去进行插入操作。
2>.新增节点以后,只会影响祖先节点的高度,也就是可能会影响部分祖先节点的平衡因子,所以更新的是从当前这个新增节点到根节点路径上的平衡因子,实际中最坏的情况下是必须要更新到根,有些情况就只需要更新到中间就可以停止了,具体情况我们下面再做详细的分析。
3>.更新平衡因子后,平衡因子符合0/-1/1,就说明插入结束。
4>.更新平衡因子过程中出现不平衡,对不平衡子树进行旋转操作,旋转这一步操作本质上是降低了子树的高度,不会再影响上一层,所以插入结束。(后面会讲)
2.2.2 平衡因子的更新
2.2.2.1 更新原则
1>.平衡因子=右子树的高度-左子树的高度。
2>.只有子树高度变化才会影响当前节点的平衡因子。
3>.插入一个节点,会增加高度,所以新增节点在parent的右子树上时,parent的平衡因子++,新增节点在parent的左子树上时,parent的平衡因子--。
4>.parent所在子树的高度是否变化决定了是否会继续往上更新。
2.2.2.2 更新停止条件
1>.更新后如果parent的平衡因子等于0,更新中parent的平衡因子变化为1->0或者-1->0,通过这个平衡因子的变化,我们就可以知道更新前parent的子树是一边高一边低,新增的节点被插入在了低的那边,插入后parent所在的子树的高度不会变,不会影响parent的父亲节点的平衡因子,因此,更新结束。
2>.更新后parent节点的平衡因子等于1或者是-1,更新时parent节点的平衡因子变化为0->-1或者是0->-1(这里我们需要注意的是,这个变化它是有一个前提的,那就是必须要保证要插入的这棵树它必须是一棵AVL树,既然是AVL树,那么就说明这棵树中的所有的节点中的平衡因子的绝对值都是<2的,因此只有我们这里所说的这两种情况),说明在插入之前,以parent节点为根节点的那棵树的子树就会一边高一边低,虽然子树一边高一边低,但是以parent为根节点的这棵树它也符合AVL树的要求,且高度增加了1,但是以parent节点的父亲节点为根节点的这棵子树就不符合AVL树的要求了,因为高度增加了1,会影响parent节点的父亲节点的平衡因子,因此要继续向上更新祖宗节点的平衡因子才可以。
3>.更新后parent的平衡因子等于2或-2,更新时平衡因子的变化为1->2或者是-1->-2,说明更新前以parent为根节点的树的子树一边高一边低,新插入的那个节点被插在了高的那一边,这样就会使parent所在的子树高的那边会变得更高,进而破坏了平衡,因而parent所在的子树就会不符合平衡的要求,需要进行旋转处理,旋转的目标有2个:1.把parent子树旋转平衡。2.降低parent子树的高度。所以旋转后也不需要进行往上更新,插入结束。
2.2.2.3 插入节点及更换平衡因子的代码实现
cpp
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new node(kv);
return true;
}
node* parent = nullptr;//指向cur节点的父亲节点的指针。
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;//通过我们前面对AVL树的节点结构的仔细剖析,我们可以得知其中的一个成员变量_parent指向的是当前这个节点的父亲节点,是为了更新平衡因子所准备的,而parent指针恰好就指向当前节点(要插入节点)的父亲节点,因此让插入的新节点的_parent指针指向parent节点即可。
//当程序进行到这里时,就说明插入成功了,接下来就该更新这个节点的各个指针节点的平衡因子了。
while (parent != nullptr)//要想更新这个节点的各个祖宗节点的平衡因子,就只能按照来时的路一步步往回走(也就是通过节点中的_parent指针往回走),按照"更新原则"和"更新停止条件"走,我们在进行更新平衡因子的操作时,最坏的一个情况就是会一直更新到整棵AVL树的根节点(也就是_root指针指向的那个节点),按照我们的逻辑走的话,更新完某一个节点的平衡因子后,就要让parent指向这个节点中_parent成员变量所指向的那个节点,......当parent指向_root时,我们更新_root的平衡因子后,parent就指向为空了,此时就足以证明我们将从刚刚插入的那个节点开始的所有祖宗节点的平衡因子全部都更新完了,相当于是插入完成了,可以结束了,因此这里以parent!=nullptr这一条件结束更新平衡因子。
{
//更新平衡因子
if (cur == parent->_left)//如果新插入的节点在左边的话,那么就说明新插入的这个节点的父亲节点的平衡因子要-1。
{
parent->_bf--;
}
else//(cur == parent->_right);新插入的节点在右边,那么就说明新插入的这个节点的父亲节点的平衡因子要+1。
{
parent->_bf++;
}//根据我们前面所讲到的更新原则和更新停止条件的相关知识,并不是插入的新节点的所有祖宗都需要更新平衡因子,当更新后的平衡因子达到某个条件后就不需要再去更新父亲节点的平衡因子了,更新就可以结束了,那接下来我们就来看一下。
if (parent->_bf == 0)//就说明插入节点后,让以parent这个节点为根节点的这棵树的左右子树平衡了(原来是一高一低不平衡),因此它的高度时没有变化的,通过更新停止条件可知,当parent->_bf为0时,更新就可以结束了。
{
break;
}
else if (parent->_bf == -1 || parent->_bf == 1)//通过更新停止条件2可知,这种情况下是导致了以parent为根的那棵树的左右子树高度发生了变化,需要继续向上更新祖宗节点的平衡因子。
{
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == -2 || parent->_bf == 2)//通过更新停止条件3可知,这种情况下就需要旋转来解决了,且旋转过后会恢复到插入节点以前的高度,就和更新停止条件1差不多了,因此操作过后就不用继续向上更新了,插入结束。
{
//旋转处理,还没学,后面会讲。(通过我们后面的讲解学习之后,我们可以知道旋转分为4种方式,不同的插入方式对应不同的旋转方式,因此我们应该还要判断一下它应用哪种旋转方式。)
//......
break;
}
else//防止要进行插入节点的这棵树本身就不是AVL树的情况
{
assert(false);//直接终止程序,报错。
}
}//出了循环之后,就说明要么是parent指向nullptr了,要么是平衡因子更新完成了,也就是插入完成了。
return true;
}
2.3 右旋转
2.3.1 旋转的原则
1>.保持二叉搜索树的规则。
2>.让旋转的树从不平衡变为平衡,其次会降低树的高度,旋转分为4种,左单旋 / 右单旋 / 左右双旋 / 右左双旋(在我们接下来对旋转的讲解中会画图进行讲解,在下面的图中,有些节点我们这里给的是具体的值,比如10和5等等一些节点,用10和5是为了在这里能够更加方便地去进行讲解,实际中是什么值其实都可以,只要大小关系符合二叉搜索树的规则就可以了)。
2.3.2 右单旋
1>.下图中的第一幅图展示的是10为根节点的树,由a/b/c抽象为三棵高度为h的子树(h>=0),a/b/c这三棵子树且均符合AVL树的要求。10这个节点可能是整棵树的根,也可能是以整棵树中某个局部的子树的根。这里a/b/c是高度为h的子树,是一种概括且抽象的表示,它代表了所有右单旋的场景,实际单旋形态由很多,具体后面会进行一个详细的描述。
2>.在a子树中插入一个新的节点,就会导致a子树的高度从 h 变成了 h+1 ,就要不断地到上面去更新平衡因子,进而导致10的平衡因子从-1变成了-1,10为根节点的树左右高度差超过了1,违反了平衡规则。10为根节点的树的左子树的高度太高了,需要往右边旋转,从而控制两棵树的平衡。
3>.旋转的核心步骤:因为5<b子树的根节点的值<10,将b变成10的左子树,10变成5的右子树,而5这个节点则变成这棵树新的根,这样一来,新形成的子树也就符合了搜索树的规则,控制了平衡,同时还将这棵树的高度恢复到了插入之前的h+2,符合旋转规则。如果插入之前的10这棵树是一个局部子树的话,旋转过后就不会再影响上一层了,因此插入就结束了。
OK,以上图中的过程就是我们旋转的比较具体的一个过程,接下来我们来对a,b,c这三棵子树是什么来做一个简单的解答。
情况1:插入前a/b/c的高度h==0(图中包含10和5这两个节点)
情况2:插入前a/b/c的高度h==1
情况3:插入前a/b/c高度h==2
通过我们前面所学习的知识可知,高度h==2的子树分为了3种情况:
上述所讲的过程中所说的b和c子树可以是x/y/z中的任意一种,但是a只能是x,假如a是y/z的话,那么在插入一个新的节点后,按照要求,它y/z自己就要进行旋转操作(大家在这里可以自已去画画图),这样就有回到情况1中去了(我们这里是想让10这个节点为旋转点进行右单旋操作),由此观之,只有a是x时,插入后节点高度为h+1,a不用旋转,此刻会引发以10这个节点旋转,既然a是x,那么插入的方式就有4种,而b/c又是x/y/z,将他们组合一下,得出的结论是若插入前a/b/c的高度h==2时,总共有3*3*4=36种场景。
情况4:插入前a/b/c高度h==3AVL子树
通过情况4我们可以得知,b和c可以是x/y-c中任意的一种情况,一共有15*15种组合,a的情况在这里和情况3中比较相似,要确保在插入新的节点后,a自身不会旋转,这样的话,a的最后一层叶节点的个数必须要大于等于3,小于等于4,这里要分情况考虑,若为3,最后一列有4种组合情况,总计有4*4种插入情况(大家可以自行画图,这里就不做过多的解释了),若是4,总计有8种插入情况,将它们全部组合一下,得出的结论是,若插入前a/b/c的高度h==3时,总计有15*15(8+4*4)=5400种场景。
综上所述,a/b/c的情况是什么其实有无数种情况,还有许多场景(h==4,h==5...),它们的解释说到底其实还是和上面的解释是差不多的,因此,就不多解释了,如果大家有兴趣的话可以自行尝试去画一画。
2.3.3 代码实现右单旋
cpp
void RotateR(node* parent)//parent指针指向的是更新后的平衡因子为-2的那个节点,并且parent的左子树的那个根节点的平衡因子为-1,否则的话,无法保证是左边高,要进行右单旋操作,若parent的左子树的那个根节点的平衡因子为1,那么就是b高a低,应该是去进行左单旋操作。
{
//通过我们前面的讲解可知,我们可以知道右单旋主要变换的就是parent(更新后平衡因子为-2的节点),parent的左子树的那个根节点(更新后平衡因子为-1的节点),以及b子树,既让如此,那么首先要先得到指向它们的指针。
node* subL = parent->_left;//parent的左子树的那个根节点。
node* subLR = subL->_right;//b子树的根节点。
node* pParent = parent->_parent;//这里我们暂时先对这句话代码做一个标记,后面的代码分析会用到这句代码。
parent->_left = subLR;//先让parent的左指针指向b子树,我们前面有分析到,连接好后,还要注意要连接一下_parent成员变量。
if (subLR != nullptr)//如果b子树为空的话,就不需要连接_parent成员变量了,因为空树没有节点。
{
subLR->_parent = parent;//parent节点成为了b子树的新的父亲节点,这里之所以要再判断一下subLR是否为空,是为了让b子树重新认父亲,因此需要访问subLR中的_parent成员变量,若不判断的话,如果在b子树是空树进行这一步操作时,系统就会报错。
}
subL->_right = parent;//根据前面的分析我们可知,连接完b子树后接下来该subL了,让subL节点为parent新的父亲节点。
parent->_parent = subL;//调整parent中的_parent成员变量。
//OK,按道理说,当程序走到这里时,就已经旋转完成了,但是事实上并没有完,通过我们前面的分析可知,parent节点可能是这棵AVL树的根节点,但是也有极大的可能它是一棵局部的子树,如果它是整棵AVL树的根节点的话,让_root指向subL就可以结束了,假如是一棵局部子树,还要连接一下subL节点,让它成为这棵AVL树新的根,并且与旋转前parent的父节点连接。
if (pParent != nullptr)//在第6行代码中,我们保留了旋转前parent的父节点的指针,它的作用就体现在了这里。
{
_root = subL;
subL->_parent = nullptr;//整棵AVL树最终的那个根节点的_parent成员变量是空的。
}
else//说明旋转之前parent树只是一个局部子树,要和上一层连接。
{
if (pParent->_left == parent)//因为我们这里暂时还未将subL作为这颗子树新的根,因此parent此时还是和上一层连接着,由此观之,还是要靠parent指针去判断subL应该插在上一层的左边还是右边。
{
pParent->_left = subL;
}
else
{
pParent->_right = subL;
}
subL->_parent = pParent;//更新一下subL的_parent成员变量。
}
subL->_bf = parent->_bf = 0;//通过我们前面的解析可知,无论是那种情况,最后subL和parent的平衡因子都会变为0,因为高度恢复且两边最终都会回归平衡。
}
2.4 左旋转(左单旋)
1>.下图中展示的是10为根的AVL树,有a/b/c抽象为三颗高度为h的子树(h>=0),a/b/c均符合AVL树的要求。10可能会是整棵树的根,也可能是一个整棵树中一棵局部子树的根。这里a/b/c指的是高度为h的子树,是一种概括抽象的表示,他代表了所有的左单旋的场景,实际左单旋形态有很多种,具体跟上面的右单旋的4中情况是类似的。
2>.在a子树中插入一个新的节点,导致a子树的高度由h变成了h+1,不断向上更新平衡因子,就导致了10这个节点的平衡因子从1变成了2,以10为根节点的这棵树的左右高度差超过了1,因此需要往左边旋转,从而控制两棵树的平衡。
3>.旋转核心步骤,因为10<b子树的根的值<15,将b子树变成10节点的右子树,10变成15的左子树,15变成这棵树新的根,符合搜索树的规则,控制了平衡,并同时将这棵树的高度恢复到了插入之前的h+2,符合旋转的原则,如果插入之前10是整棵树的一个局部子树的话,那么旋转之后就不会再影响上一层了,这样插入就结束了。
2.4.1 代码实现左单旋
cpp
void RotateL(node* parent)//parent在这里的解释其实和右单旋中的解释一样,它也指向的是更新后平衡因子为2的那个节点,这里有一点我们需要注意一下,就是只有当parent指向的那个节点的平衡因子为2且cur指向的那个节点的平衡因子为1才可以进行左单旋操作。
{
node* subR = parent->_right;
node* subRL = subR->_left;
node* pParent = parent->_parent;//上面这三行代码的解释和在有单旋中的解释相同,方便起见,这里就不再多写了。
if (subRL != nullptr)//与右单旋中的解释一样。
{
subRL->_parent = parent;
}
subR->_left = parent;//将parent连接到subR的左子树上。
parent->_parent = subR;//subR节点成为parent节点新的父亲节点。
if (pParent != nullptr)
{
_root = subR;//如果pParent为空的话,就说明在旋转之前,parent节点就是整棵树的根,因此根据我们前面所讲的逻辑可知,需要更换subR为新的根,让_root指向subR就可以了。
subR->_parent = nullptr;
}
else
{
if (pParent->_left == parent)
{
pParent->_left = subR;
}
else
{
pParent->_right = subR;
}
subR->_parent = pParent;
}
parent->_bf = subR->_bf = 0;
}
2.5 左右双旋
2.5.1 前情提要
我们在前面讲解的2种单旋方式它们的场景全部都是纯粹的一边高,也就是说,只有插入节点在5这个节点的左子树上或者插入节点在15这个节点的右子树上时,我们使用单旋就可以使子树平衡,其余情况下,若只是使用单旋就不会平衡,就如上图所示那样。
2.5.2 深度解析
1>.通过上图我们可以看到,在左边高时,如果插入的位置不是在a子树,而是在b子树中插入,b子树的高度就会从h变成h+1,从而引发旋转,按照我们的前面所讲的来看的话,左边太高,应该往右边旋转,可是我们通过上图来看的话,右旋之后无法解决问题,我们的树它依然是不平衡的,右单旋解决的是纯粹的左边高这种情况(也就是说,插入的新节点不仅属于以5这个节点为根节点的子树的左子树,更是属于以10这个节点为根节点的左子树,这才是纯粹的一边高),但是插入在b子树中,以10这个节点为根节点的子树不再是单纯的左边高,对于以10这个节点为根节点的子树来说是左边高,而对于以5这个节点为根节点的子树来说则是右边高,对于这样的插入情况,我们就需要用两次旋转才能解决,首先以5节点为旋转点进行一个左单旋操作,旋转完后,再以10这个节点为旋转点进行一个右单旋操作,这样这棵树就会平衡了。
(从10这个节点的角度来看的话,新插入的这个节点相当于是插入在b子树中,但是以5这个节点为旋转点来看的话,8这个节点就不算是在b子树中)
2>.上图为左右双旋中h==0场景的分析,下面我们将a/b/c子树抽象为高度h的AVL子树进行分析另外画图,另外我们还需要把b子树的细节再进一步展开为8和左子树高度为h-1的e和f子树,因为我们要以b的父亲节点5为旋转点进行左单旋操作,而左单旋需要动b子树中的左子树。b子树中新增节点的位置又有不同,平衡因子更新的细节也因为这个新增节点的位置不同而不同,通过观察8这个节点的平衡因子的不同,这里我们还要分为三个场景去分析:
场景1:h>=1时,新增节点在e子树中
h>=1时,新增节点插入在8节点的左子树(e子树),e子树的高度从h-1变为h并不断更新,8->5->10的各个节点的平衡因子,会引发旋转,其中8的平衡因子为-1,旋转后8和5的平衡因子变为0,10的平衡因子为-1。
场景2:h>=1时,新增节点在f子树中
h>=1时,新增节点插入在f子树中,使得f子树的高度从h-1变为了h,并且会不断更新8->5->10节点的平衡因子,引发了旋转,其中8的平衡因子为1,旋转后8和10的平衡因子为0,5的平衡因子为-1。
场景3:h==0时,a/b/c三棵子树均为空树
h==0时,a//b/c三棵子树均为空树,b子树自己就是一个新增节点,不断更新5->10节点的平衡因子,引发旋转。
OK,通过我们对上述3种情况的分析我们可以得知,当我们将节点新插入到b子树上时,我们此时若使用单旋的话,就无法产生我们想要的结果,无法使子树平衡,因此在这里我们要进行的使双旋操作。通过我们双旋具体的旋转方法可知,我们这里要将b子树展开,要用到b子树的根节点,由于将b子树展开之后,形成e和f两棵子树,而e和f这两棵子树上都可以用来插入新的节点,这样就会生出情况1和情况2两种插入方式,通过图中展示出来的旋转后的结果来看,虽然最后的结果都是让子树重新归于平衡了,但是子树中不同位置插入的节点的平衡因子更新后的结果也是不同的(当然了,还有情况3,之所以会出现情况3这种情况,是因为旋转发生的前提一定使插入节点之后如果子树不平衡的话才会发生的,而情况3的8节点刚好插入在了要进行双旋的位置上),这样的话就会使得无法精准地判断处平衡因子的大小,基于这种情况,需要我们分开讨论。
2.5.3 代码实现左右双旋
cpp
void RotateLR(node* parent)//parent指向更新后平衡因子变为-2的那个节点。
{
node* subL = parent->_left;//通过去前面的解释可知,我们进行左右双旋的1时候是嫌疑parent的_left节点为旋转点进行一次左单旋操作,因此,我们这里定义一个指针让它去指向parent的_left指向的节点。
node* subLR = subL->_right;//旋转完后,要修改此时的这个subL的_right指针指向的那个节点的平衡因子,由于进行一次左右双旋之后,它里面所有的节点的位置会进行一次大整改,旋转完后的subL节点的右子树可能就不是我们想要的了,因此这里在旋转之前必须要定义一个指针来指向那个节点。
int bf = subLR->_bf;//这里我们需要定义一个int类型的变量bf来存放subLR这个指针指向的那个节点的平衡因子(插入节点之后),这里之所以要这么做,是因为通过前面我们在这里所分析的那3种情况,如果我们仔细观察的话不难发现,之所以会出现这3种情况,因为就在于新插入的节点是在e子树还是f子树上,或者插入的那个节点就是b子树的根,由于插入的位置不同才导致了最后我们要进行重新修整一下平衡因子的操作,而插入的节点的位置最直接影响的就是subLR节点的平衡因子,三个位置->三种情况->三个平衡因子,因此我们就可以通过旋转前b子树的根节点(也就是subLR指向的那个节点的平衡因子来判断它属于哪一种情况,由此精准对应并修改它们旋转之后的平衡因子。)
RotateL(subL);//以subL这个节点为旋转点(也就是上图中的5这个节点)进行左单旋操作。
RotateR(parent);//进行完左单旋操作后,我们接下来就要以parent(也就是上图中的10这个节点)为旋转点去进行右单旋操作。
//我们此时再去看一下我们前面写的那两个单旋的代码,我们可以得知在进行完前面的两步代码之后,所有参与旋转的节点的平衡因子全部变成了0,但是前面的3种情况中个别节点的平衡因子明显不是0,由此需要我们去分情况讨论修改各个节点的平衡因子。
if (bf == 0)//对应的是情况3
{
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 0;
}
else if (bf == -1)//对应的是情况1
{
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 1;
}
else if (bf == 1)//对应的是情况2
{
subL->_bf = -1;
subLR->_bf = 0;
parent->_bf = 0;
}
else//就说明旋转之前这棵树它本身就不是一棵AVL树。
{
assert(false);//报错
}
}
2.6 右左双旋
2.6.1 旋转方法
1>.跟左右双旋类似,下面我们将a/b/c子树抽象为高度为h的AVL子树进行分析,另外我们还需要把b子树的细节进一步展开为12节点和高度为h-1的e/f这两棵子树,因为我们要对b节点的父亲节点15为旋转点进行右单旋操作,右单旋需要动b子树中的右子树。由于插入的位置不确定,插入的位置可能是e子树/f子树或者插入的节点它就是b子树的根节点,从而使得平衡因子更新的细节也不同,通过观察12的平衡因子的不同,这里也要分3种情况讨论。
场景1:h>=1时,新增节点在e子树中
h>=1时,新增节点插入在e子树上,e子树的高度从h-1变为h并不断更新12->15->10这些节点的平衡因子,引发旋转,其中12的平衡因子为-1,旋转后12和10的平衡因子为0,15的平衡因子为1。
场景2:h>=1,新增节点在f子树中
h>=1时,新插入的节点插入在f子树上,f子树的高度从h-1变成h,并会不断更新12->15->10这些节点的平衡因子,引发旋转,其中12的平衡因子为1,旋转后12和15的平衡因子均为0,10的平衡因子为1。
场景3:h==0时,a/b/c三棵子树均为空树
h==0时,a/b/c三棵子树均为空树,b自己就是一个新增节点,不断更新15->10这些节点的平衡因子,引发旋转,其中12的平衡因子为0,旋转后12和10和15节点的平衡因子均为0。
2.6.2 代码实现右左双旋
cpp
void RotateRL(node* parent)//parent指向的是更新后平衡因子因为2的那个节点
{
node* subR = parent->_right;//要以parent->_right节点为旋转点去进行一次右单旋操作。
node* subRL = subR->_left;//通过左右双旋的相关代码的解释可知,这里我们可以知道,后面要以旋转之前subR->_left的这个节点的平衡因子为条件,去判断插入节点是属于上面3种情况的哪一种,而且在旋转后要去修改此时的subR->_left这个节点的平衡因子,由于旋转后它们的关系全部都错乱了,如果旋转后再通过subR->_left去找节点的话,就找不到我们想要的那个节点了,因此这里我们必须在旋转前指定一个指针指向旋转前的subR->_left这个指针指向的那个节点。以方便后续去更新平衡因子。
int bf = subRL->_bf;//通过我们前面所学的可以得知,我们在正式旋转之前(也就是插入节点之后,更新节点的平衡因子遇到2时),三种情况的平衡因子唯一不同的就是subRL这个节点的平衡因子,旋转前这个节点的3种情况分别对应3个不同的平衡因子,由于是在旋转前才有的,因此需要提前将它存储起来。
RotateR(subR);//以subR这个节点为旋转点进行右旋操作。
RotateL(parent);//以parent这个节点为旋转点进行左旋操作。
if (bf == 0)//对应的是情况3
{
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = 0;
}
else if (bf == -1)//对应的是情况1
{
subL->_bf = 0;
subRL->_bf = 0;
subR->_bf = 1;
}
else if (bf == 1)//对应的是情况2
{
parent->_bf = -1;
subR->_bf = 0;
subRL->_bf = 0;
}
else//就说明旋转之前这棵树它本身就不是一棵AVL树。
{
assert(false);//报错
}
}
2.7 AVL树的查找
用二叉搜索树的逻辑实现即可,搜索效率为O(log2N)。
cpp
node* Find(const K& key)
{
node* cur = _root;//让cur指针指向_root节点,cur去遍历二叉搜索树,去找到相对应的节点。
while (cur)//若cur为空,则说明没找到。
{
if (cur->_kv.first < key)//在右子树
{
cur = cur->_right;
}
else if (cur->_kv.first < key)//在左子树
{
cur = cur->_left;
}
else//cur->_kv.first == key;找到了
{
return cur;
}
}
return nullptr;//若处循环的话,则说明cur指向空,未找到。
}
2.8 AVL树的删除
AVL树的删除这一部分有点麻烦,本章节不做讲解,如果大家对这部分感兴趣的话,可以直接去尝试着去实现。
OK,今天我们就先讲到这里了,那么,我们下一篇再见,谢谢大家的支持!