目录
模型Ⅱ、根节点的平衡因子为-2,插入节点的子树的平衡因子为-1
模型Ⅲ、根节点的平衡因子为2,插入节点的子树的平衡因子为-1
一、AVL树的概念
AVL树是一种高度平衡的二叉搜索树,由俄罗斯的两位数学家G.M.Adelson-Velskii 和E.M.Landis在1962年提出,用于解决二叉搜索树在极端场景下左、右严重不平衡而导致效率低下的问题。
满足以下条件的二叉树属于AVL树:
1、二叉搜索树
2、左、右子树高度差的绝对值不超过1(高度平衡)
3、左、右子树也是AVL树
二、AVL树的构建
AVL树的构建原理和二叉搜索树大致相同,唯一的不同点及困难点在于:
如何在频繁的插入/删除操作中始终维持二叉搜索树的高度平衡?
因此,本篇文章重点探讨如何实现二叉搜索树的高度平衡,不会着重讲解或验证二叉搜索树的特性,因此需要大家先掌握二叉搜索树的相关知识。
1、节点的定义
与常规二叉搜索树不同的是,我们要在节点中新增一个平衡因子,用于维护左、右子树高度平衡;
另外,还要增加一个指向父节点的指针,方便我们向上层遍历。
节点的定义如下:
//二叉树节点
template <class k,class v>
struct TreeNode
{
TreeNode(const pair<k, v>& kv)
:_kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _fb(0)
{
}
pair<k, v> _kv;//kv结构
TreeNode<k, v>* _left;
TreeNode<k, v>* _right;
TreeNode<k, v>* _parent;
int _fb;//平衡因子(右子树高度-左子树高度)
};
注:这里用右子树高度 - 左子树高度来表示平衡因子,其实用左子树高度 - 右子树高度也是一样的道理。
结合AVL树的特性,得出以下结论:
1、任意一个叶子节点的平衡因子都是0
2、右子树高度变大或左子树高度变小,平衡因子都会变大;左子树高度变大或右子树高度变小,平衡因子都会变小;
3、正常情况下,平衡因子的值只能是-1、0、1三种情况,当平衡因子变成-2或2时,说明其左、右子树不再平衡,需要对结构进行调整
2、AVL树的插入操作
我们通过插入节点来构建AVL树。
首先,通过father和cur两个指针的迭代先找到合适的插入位置。其中father表示要插入的根节点,cur表示插入位置。当cur走到nullptr时,抵达最终的插入位置,创建新节点并赋值给cur,然后将father和cur进行链接。
查找插入位置的代码逻辑如下:
if (_root == nullptr)//当前为空树
{
_root = new TreeNode(kv);
return true;
}
//当前不是空树,先查找插入位置
TreeNode* father = nullptr, * cur = _root;
while (cur)
{
if (cur->_kv.first < kv.first)
{
father = cur;
cur = cur->_right;
}
else if (cur->_kv.first > kv.first)
{
father = cur;
cur = cur->_left;
}
else//节点已存在
return false;
}
//cur为空,找到了插入位置.插入节点
cur = new TreeNode(kv);
cur->_parent = father;
if (father->_kv.first < cur->_kv.first)
{
father->_right = cur;
}
else
{
father->_left = cur;
}
由于cur由之前的空节点变成了叶子节点,则对于father而言,cur这颗子树的高度由0变成了1。其中一棵子树的高度增加了1,father的平衡因子必然要加一或减一。如果cur是father的右子树,则father->_bf++;如果cur是father的左子树,则father->_bf--;
father的平衡因子调整以后有以下三种情况:
1、father->_bf==0。这就意味着之前father->_bf一定是1或者-1。此时的情景是:
新节点插入到了之前较矮的子树中,使得较矮子树的高度增加了1,最后两棵子树的高度变得相同,而father整棵树的高度不变。因此不会再影响father的上级根节点。
2、father->_bf==1或-1。这就意味着之前father->_bf一定是0。此时的情景是:
新节点插入到了原来高度相同的左、右子树的其中一棵,使得其中一棵子树的高度增加了1。同时,也使得father整棵树的高度也增加了1。
由于father本身也是其上级父节点的子树,其高度的增加势必也会影响其上级父节点的平衡因子,因此,需要不断往上迭代处理上级根节点的平衡因子。直到根节点的平衡因子变成0时完成调整。
3、father->_bf==2或 -2。这就意味着,之前father->_bf一定是1或-1;此时的情景是:
新节点插入到原本高度差为1的较高的那棵子树中,使得左、右子树的高度差变成2。
此时,左、右子树的高度变得不再平衡,需要调整father这棵树的结构,使其变得平衡的同时,令其恢复到插入前的高度,从而避免再向上层去调整平衡因子。
调整平衡因子的代码逻辑如下:
while (father)//father为空表示走到根节点
{
//调整平衡因子
if (father->_kv.first < cur->_kv.first)//father的右子树高度增加了
{
father->_fb++;
}
else//father的左子树高度增加了
{
father->_fb--;
}
//查看调整后的father的平衡因子
if (father->_fb == 0)//左右子树高度相同,则必然之前高度较低的子树被补平了,
{ //father的高度不变
braek;//结束调整
}
else if (father->_fb == 1 || father->_fb == -1)
{ //之前father的平衡因子一定为0,此时father的高度一定增加了
//而father作为爷爷节点的子树,其高度增加了必然影响爷爷节点的平衡因子
cur = father;
father = father->father;//往上层迭代
}
else if (father->_fb == 2 || father->_fb == -2)//高度变得不平衡,对结构进行调整
{
//进行旋转
}
else//未知异常
{
assert(false);//报错
}
}
接下来,我们重点分析高度不平衡时如何对子树的结构进行调整,使其恢复平衡。通常,我们将这个调整的操作称为旋转。
3、旋转的四种情况及处理方式
首先,我们先明确旋转需要达到怎样的效果。
旋转需要达到的效果有以下几点:
a、继续维持二叉搜索树的结构
b、使左、右子树的高度变得平衡
c、调整旋转后节点的平衡因子
d、根节点对应的子树恢复到插入之前的高度(不用再调整上层的平衡因子)
其次,我们可以通过根节点的平衡因子来推导旋转的结构模型。
当一个根节点的平衡因子由1变成2或由-1变成-2时,插入节点的那棵子树的高度必然发生了变化。
因此插入节点的子树的平衡因子一定是1或-1。
(如果对应子树的平衡因子为0,则当前根节点的平衡因子不会发生变化;如果对应子树的平衡因子为2或-2,则会先完成当前子树的旋转,当前根节点的平衡因子同样不会发生变化)。
由此又可以推导出对应子树在插入节点之前,其平衡因子一定为0。也就是说,对应子树的左、右两边在插入节点之前高度一定相同。
因此就有4种不同的组合情况,分别为:
Ⅰ:根节点的平衡因子为2,插入节点的子树的平衡因子为1
Ⅱ:根节点的平衡因子为-2,插入节点的子树的平衡因子为-1
Ⅲ:根节点的平衡因子为2,插入节点的子树的平衡因子为-1
Ⅳ:根节点的平衡因子为-2,插入节点的子树的平衡因子为1
接下来,带着以上四个目标分别对四种不同的旋转模型进行分析和处理。
模型Ⅰ、根节点的平衡因子为2,插入节点的子树的平衡因子为1
节点插入前、后的结构模型如下图:
此时右边高于左边,我们采取将根节点及左子树下压的方式进行调整,这个操作称为左旋。
具体操作如下:
① 将cur的左子树b链接到father的右子树(father<b<cur)
② 将father链接到cur的左子树
③将cur取代father变成这棵子树新的根节点,并将其链接到father曾经的父节点下面
旋转过程及最终模型如下图:
观察旋转后的模型结构,依旧是二叉搜索树的结构,且左右两边高度平衡,同时旋转后整棵子树的高度和插入前一样,都是h+2。旋转的四个目标已经完成了3个,接下来只需要调整节点的平衡因子即可。
在上述旋转的过程中,只有father和cur的平衡因子会发生改变,且旋转后father和cur的左、右子树高度都相同,因此只需将father和cur的平衡因子设为0即可。
插入前、插入后、旋转后的结构模型对比如下图:
左旋的代码逻辑如下:
//左旋
void RotateL(TreeNode* father)
{
assert(father);//根节点不能为空
TreeNode* grandpa = father->_parent;//当前根节点的父节点
TreeNode* rightTree = father->_right;//右子树
TreeNode* rightTree_left = rightTree->_left;//右子树的左子树
//先将右子树的左子树链接到根节点右边
father->_right = rightTree_left;
if (rightTree_left != nullptr)
rightTree_left->_parent = father;
//然后将father链接到rightTree的左边
rightTree->_left = father;
father->_parent = rightTree;
//接着将grandpa与rightTree进行链接
rightTree->_parent = grandpa;
if (grandpa == nullptr)//当前parent就是根节点
{
_root = rightTree;
}
else//grandpa不为空
{
if (grandpa->_left == father)
{
grandpa->_left = rightTree;
}
else
{
grandpa->_right = rightTree;
}
}
//最后将father和rightTree的平衡因子改成0
father->_fb = rightTree->_fb = 0;
}
模型Ⅱ、根节点的平衡因子为-2,插入节点的子树的平衡因子为-1
节点插入前、后的结构模型如下图:
此时左边高于右边,我们采取将根节点及右子树下压的方式进行调整,这个操作称为右旋。
具体操作如下:
① 将cur的右子树b链接到father的左子树(cur<b<father)
② 将father链接到cur的右子树
③将cur取代father变成这棵子树新的根节点,并将其链接到father曾经的父节点下面
旋转过程及最终模型如下图:
观察旋转后的模型结构,依旧是二叉搜索树的结构,且左右两边高度平衡,同时旋转后整棵子树的高度和插入前一样,都是h+2。旋转的四个目标已经完成了3个,接下来只需要调整节点的平衡因子即可。
在上述旋转的过程中,只有father和cur的平衡因子会发生改变,且旋转后father和cur的左、右子树高度都相同,因此只需将father和cur的平衡因子设为0即可。
插入前、插入后、旋转后的结构模型对比如下图:
右旋的代码逻辑如下:
//右旋
void RotateR(TreeNode* father)
{
assert(father);//根节点不能为空
TreeNode* grandpa = father->_parent;//father的父节点
TreeNode* leftTree = father->_left;//左子树
TreeNode* leftTree_right = leftTree->_right;//左子树的右子树
//先将左子树的右子树链接到father左边
father->_left = leftTree_right;
if (leftTree_right)
leftTree_right->_parent = father;
//然后将father链接到左子树的右边
leftTree->_right = father;
father->_parent = leftTree;
//最后将leftTree与grandpa进行链接
leftTree->_parent = grandpa;
if (grandpa == nullptr)
{
_root = leftTree;
}
else
{
if (grandpa->_left == father)
{
grandpa->_left = leftTree;
}
else
{
grandpa->_right = leftTree;
}
}
//最后将father和leftTree的平衡因子调整为0
father->_fb = leftTree->_fb = 0;
}
模型Ⅲ、根节点的平衡因子为2,插入节点的子树的平衡因子为-1
节点插入前、后的结构模型如下图:
通过b的高度增加了1,可以推导出节点插入后,b的平衡因子一定是 1或-1。由此又可以推导出,在节点插入前,b的平衡因子一定是0。
将b部分划分成根+左右子树的形态,则结构模型转化成以下形态:
观察插入后的模型,发现具有以下特点:
1、father<cur_L<cur。单从节点值而言,cur_L可以作为father和cur的根节点,father和cur可以分别作为cur_L的左、右子树。如果按照这种方式链接,我们需要对cur_L原来的左子树b和原来的右子树c进行处理。
2、father<b<cur_L,cur_L<c<cur。从大小关系上看,b正好可以作为father的右子树,c正好可以作为cur的左子树。
我们尝试按照上述方式对father整棵子树进行重组,得到以下模型:
观察重组后的模型结构:
如果插入位置是b,则father的左子树高度为h,右子树高度为也是h,father满足AVL树的条件,且father的高度为h+1;而cur的左子树高度为h-1,右子树高度为h,cur满足AVL树的条件,且cur整棵树的高度为h+1。而father和cur分别作为cur_L的左、右子树,其高度都是h+1,则cur_L整棵树满足AVL树的条件,且cur_L的高度为h+2,与节点插入前整棵子树的高度相同。
如果插入位置是c,则father的左子树高度为h,右子树高度为h-1,father满足AVL树的条件,且father的高度为h+1;而cur的左子树高度为h,右子树高度也是h,cur满足AVL树的条件,且cur整棵树的高度为h+1。而father和cur分别作为cur_L的左、右子树,其高度都是h+1,则cur_L整棵树满足AVL树的条件且cur_L的高度为h+2,与节点插入前整棵子树的高度相同。
也就是说,不管插入位置是b还是c,按照以上方式对整棵子树进行重组,都可以形成一个与插入前高度相同的AVL树。
我们将上述整棵子树结构重组的操作拆分成两个步骤来执行:
第一步:将cur_L的右子树c链接到cur的左子树,再将cur链接到cur_L的右子树
我们发现这个步骤和右旋的操作基本相同,唯一的区别在于没有将cur_L链接到cur原来的父节点下面。在此回顾一下右旋的操作:
① 将cur的右子树b链接到father的左子树(cur<b<father)
② 将father链接到cur的右子树
③将cur取代father变成这棵子树新的根节点,并将其链接到father曾经的父节点下面
分别对cur子树进行步骤一操作和右旋操作,分别得到以下模型:
第二步:将cur_L的左子树b链接到father的右子树,将father链接到cur_L的左子树,然后cur_L取代father变成这棵子树新的根节点,将其链接到father曾经的父节点下面。
我们发现这个步骤和左旋的操作完全相同,在此回顾一下左旋的操作:
① 将cur的左子树b链接到father的右子树(father<b<cur)
② 将father链接到cur的左子树
③将cur取代father变成这棵子树新的根节点,并将其链接到father曾经的父节点下面
分别对father进行步骤一操作和右旋操作,得到以下模型:
根据模型结构我们发现,按照步骤一加步骤二的方式进行重组,和先对cur进行右旋,再对father进行左旋的方式重组,最终得到的模型结构都是一样的。
因此,我们可以直接复用之前的代码逻辑进行结构调整,将该操作称为右左双旋。
最后只需要调整father、cur、cur_L的平衡因子即可。
再次回到我们之前对重组后结构的分析:
如果插入位置是b,则father的左子树高度为h,右子树高度为也是h,father满足AVL树的条件,且father的高度为h+1;而cur的左子树高度为h-1,右子树高度为h,cur满足AVL树的条件,且cur整棵树的高度为h+1。而father和cur分别作为cur_L的左、右子树,其高度都是h+1,则cur_L整棵树满足AVL树的条件,且cur_L的高度为h+2,与节点插入前整棵子树的高度相同。
如果插入位置是c,则father的左子树高度为h,右子树高度为h-1,father满足AVL树的条件,且father的高度为h+1;而cur的左子树高度为h,右子树高度也是h,cur满足AVL树的条件,且cur整棵树的高度为h+1。而father和cur分别作为cur_L的左、右子树,其高度都是h+1,则cur_L整棵树满足AVL树的条件且cur_L的高度为h+2,与节点插入前整棵子树的高度相同。
如果插入位置原来是空树,则h==0。father的左、右子树高度都为0,father满足AVL树的条件,且father的高度为1;而cur的左、右子树高度也是0,cur满足AVL树的条件,且cur整棵树的高度为1。而father和cur分别作为cur_L的左、右子树,其高度都是1,则cur_L整棵树满足AVL树的条件且cur_L的高度为2,与节点插入前整棵子树的高度相同。
由此得出结论:cur_L的平衡因子一定为0。father和cur的平衡因子与节点的插入位置有关,而不同的插入位置又会影响节点插入后cur_L的平衡因子,因此,father和cur的平衡因子可以通过节点插入后cur_L的平衡因子来确定。
当插入的子树本身是空树时,h==0。进行旋转前cur_L的平衡因子为0。旋转后cur_L、father、cur的平衡因子都是0。
当插入位置为cur_L的左子树b时,进行旋转前cur_L的平衡因子为-1。旋转后cur_L的平衡因子为0,father的平衡因子也是0,而cur的平衡因子为1。
当插入位置为cur_L的右子树c时,进行旋转前cur_L的平衡因子为1。旋转后cur_L的平衡因子为0,father的平衡因子为-1,cur的平衡因子为0。
右左双旋的代码逻辑如下:
//右左双旋
void RotateRL(TreeNode* father)//插入位置在较高右子树的左侧
{ //father->_fb==2,father->_right->_fb=-1
assert(father);//根节点不能为空
TreeNode* rightTree = father->_right;//右子树
TreeNode* rightTree_left = rightTree->_left;//右子树的左子树
int RL_fb = rightTree_left->_fb;//旋转前右子树的左子树的平衡因子
RotateR(rightTree);//先对右子树进行右旋
RotateL(father);//再对根节点进行左旋
//修改平衡因子
if (RL_fb == 0)//右子树的左子树在插入前是空树
{
rightTree_left->_fb = 0;
father->_fb = 0;
rightTree->_fb = 0;
}
else if (RL_fb == -1)//插入位置在右子树的左子树的左边
{
rightTree_left->_fb = 0;
father->_fb = 0;
rightTree->_fb = 1;
}
else if (RL_fb == 1)//插入位置在右子树的左子树的右边
{
rightTree_left->_fb = 0;
father->_fb = -1;
rightTree->_fb = 0;
}
else//未知异常,报错
{
assert(false);
}
}
模型Ⅳ、根节点的平衡因子为-2,插入节点的子树的平衡因子为1
节点插入前、后的结构模型如下图:
同样的,将b部分划分成根+左右子树的形态,则结构模型转化成以下形态:
我们同样采取father和cur瓜分cur_R左、右子树,再分别链接链接到cur_R的左、右子树的方式对整棵子树进行重组,得到以下模型:
观察重组后的模型结构,同样是一个与插入前高度相同的AVL树。
而上述重组的过程同样可以拆分成cur左旋+father右旋两个步骤。
操作示意图如下:
观察旋转后的结构模型,我们发现通过cur左旋+father右旋两个步骤同样可以将不平衡的子树调整成与插入前高度相同的AVL树,将该操作称为左右双旋。
接下来只需要修改father、cur、cur_L的平衡因子即可。
同样地,cur_R的平衡因子一定为0,而cur和father的平衡因子与节点在cur_R中的插入位置有关,即与节点插入后cur_R的平衡因子有关。
当插入的子树本身是空树时,h==0。进行旋转前cur_R的平衡因子为0。旋转后cur_R、father、cur的平衡因子都是0。
当插入位置为cur_R的左子树b时,进行旋转前cur_R的平衡因子为-1。旋转后cur_R的平衡因子为0,father的平衡因子为1,而cur的平衡因子为0。
当插入位置为cur_R的右子树c时,进行旋转前cur_R的平衡因子为1。旋转后cur_R的平衡因子为0,father的平衡因子为0,cur的平衡因子为-1。
左右双旋的代码逻辑如下:
//左右双旋
void RotateLR(TreeNode* father)//插入位置在较高左子树的右侧
{ //father->_fb==-2 && father->_left->_fb==1
assert(father);//根节点不能为空
TreeNode* leftTree = father->_left;//左子树
TreeNode* leftTree_right = leftTree->_right;//左子树的右子树
int LR_fb = leftTree_right->_fb;//插入后左子树的右子树的平衡因子
RotateL(leftTree);//先将左子树左旋
RotateR(father);//再将father右旋
//调整father、leftTree、leftTree_right的平衡因子
if (LR_fb == 0)//插入位置是空树
{
leftTree_right->_fb = 0;
father->_fb = 0;
leftTree->_fb = 0;
}
else if (LR_fb == -1)//插入位置在左子树的右子树的左侧
{
leftTree_right->_fb = 0;
father->_fb = 1;
leftTree->_fb = 0;
}
else if (LR_fb == 1)//插入位置在左子树的右子树的右侧
{
leftTree_right->_fb = 0;
father->_fb = 0;
leftTree->_fb = -1;
}
else//未知异常
{
assert(false);//报错
}
}
最后,对插入节点构建二叉树的操作做一个小总结,分为以下三个步骤:
1、在AVL树中找到合适位置插入节点
2、从插入的根节点开始,不断向上调整对应根节点的平衡因子
3、如果向上调整的过程中某个根节点的平衡因子变成-2或2,需要调整该根节点以下整棵子树的结构,使其恢复平衡;并使调整后该子树的高度与节点插入前的该子树的高度相同,避免再往上层调整平衡因子
三、AVL树的验证
通过以下操作来验证一棵二叉树是否为AVL树:
1、中序遍历整棵二叉树,检查其中序序列是否是递增序列
2、遍历二叉树,检查每个根节点的左、右子树高度是否平衡,并检查是否存在异常的平衡因子
中序遍历及左右子树平衡验证的代码如下:
//中序遍历
void Inorder()
{
Inorder(_root);
}
//中序遍历(子函数)
void Inorder(TreeNode* root)
{
if (root == nullptr) return;
Inorder(root->_left);
cout << "key:" << root->_kv.first << " val:" << root->_kv.second << endl;
Inorder(root->_right);
}
//判断平衡
bool Isbalance()
{
return Isbalance(_root);
}
//计算高度
int Height(TreeNode* root)
{
if (root == nullptr) return 0;
int height_left = Height(root->_left);//左子树高度
int height_right = Height(root->_right);//右子树高度
return 1 + max(height_left, height_right);//取左右子树高度较大值+1
}
//判断平衡(子函数)
bool Isbalance(TreeNode* root)
{
if (root == nullptr) return true;
//检查平衡因子
if (root->_fb < -1 || root->_fb>1)
{
cout << root->_kv.first << "的平衡因子为" << root->_fb << ",出现异常" << endl;
return false;
}
//检查高度
int height_left = Height(root->_left);//左子树高度
int height_right = Height(root->_right);//右子树高度
size_t height_dif = abs(height_left-height_right);//高度差的绝对值
if (height_dif > 1)
{
cout << root->_kv.first << "的左右子树高度不平衡,高度差为" << height_dif << endl;
return false;
}
return Isbalance(root->_left) && Isbalance(root->_right);
}
四、AVL树的删除
AVL树的删除与二叉搜索树的删除类似,不过难点在于平衡因子的更新与结构的调整,这一操作比插入时更为复杂,有可能要进行多次旋转,这里暂且不予讲解。