注意:本博客的平衡因子计算方式统一为右子树的高度减左子树的高度
1.简介AVL树
当我们学习完了搜索二叉树,而紧跟着当然要开始学习AVL树啦。AVL树呢,就是搜索二叉树的一种强化版,它叫做平衡搜索二叉树,为什么会出现这种树?
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
AVL树具有以下性质:
1.它的左右子树都是AVL树
2.左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
2.部分实现
知道了以上的性质,为了发现它更独特的元素——平衡因子,那我们在实现方面和搜索二叉树有什么不同呢?(ps:对搜索二叉树实现有问题的同学可以看看我上一篇博客哦)
由于我们的平衡因子只能出现0 1 -1,而我们插入的数据是不固定的,难免我们的平衡因子会出现2或者-2,那么我们该如何把他们再变回1或-1呢?这就来到了我们的标题——旋转。
旋转一共分为四种,分别是左单旋、右单旋、左右旋、右左旋。在不同的场景下要选择不同的旋转方式。下面我就从AVL树的插入来一步一步引进旋转吧!
看到了这里,我就默认你有搜索二叉树的基础啦,我们在实现插入的时候,不难发现,其实逻辑和搜索二叉树的逻辑是差不多的,只是会多了对平衡因子的处理,接下来就看看代码和注释吧:
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
template<class K>
struct AVLTreeNode
{
K _key;
AVLTreeNode<K>* _left; // 右节点
AVLTreeNode<K>* _right; // 左节点
AVLTreeNode<K>* _parent; // 父亲节点
int _bf; // 平衡因子 balance factor
AVLTreeNode(const K& key)
:_key(key);
,_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_bf(0)
{}
};
template<class K>
class AVLTree
{
using Node = AVLTreeNode<K>;
bool Insert(const K& key)
{
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else
return false;
}
// 到此之前的都和搜索二叉树的逻辑一样,目的是找到插入的位置
cur = new Node(key); // 此时cur就是新建节点的位置,此时是空的,可以直接new,把cur当作新节点
if (cur->_key > parent->_key) // 如果是要插入到右边
{
parent->_right = cur;
cur->_parent = parent;
cur->_parent->_bf++; // 唯一的不同,父亲的平衡因子要发生改变,由于是插入到右边,所以当然是加一,下面同样
}
else
{
parent->_left = cur;
cur->_parent = parent;
cur->_parent->_bf--;
}
}
处理到这,我们算是把之前搜索二叉树的逻辑搞完了,新增的内容就是父亲的平衡因子要发生改变。但是你以为这就完了吗?还早着呢!难道除了父亲的会改变,爷爷不会改变平衡因子吗?祖爷不会吗?就算改变了,你能保证你爷爷祖爷的平衡因子满足条件不为2或者-2吗?所以这才是我们的开始,现在我们的任务就是继续往上更新平衡因子,代码继续:(ps:接在上述代码的后面,还是实现插入)
接下来是什么逻辑呢?我们要写一个循环,不断的向上更新,所以当cur的父亲为空的时候再停止,那么我们该如何确定什么时候停止更新,什么时候不要更新,又什么时候要旋转呢?接下来我们就分析这个问题:
我们知道,插入一个节点对于AVL树来说,影响最大的就是它的平衡因子,如果插入在左边,平衡因子会--,如果插入在后边,平衡因子会增大。所以我们从重要的平衡因子下手:
1.如果插入了节点cur后,parent的平衡因子是0。
这里说明原来parent的平衡因子是1或者-1,由于插入左边或者右边导致平衡因子发生变化,所以我们可以发现之前parent是一边高一边矮的,而cur插入到了较矮的一边,实现了两边的平衡,所以我们祖先的节点平衡因子是不会发生变化的,因为插入了较矮一边,更加平衡了整棵树。
2.如果插入节点cur后,parent的平衡因子是1或者-1。
这里说明原来parent的平衡因子是0,所以我们可以发现之前parent是两边高度一样的,插入了一个后导致了树的一边高,这里还是满足AVL树的性质的,我们只能保证parent的平衡因子不需要调整,但是我们又不能保证祖先的平衡因子是否要发生变化,因为新插入了节点会导致整个右树或者左树高度增大,很有可能祖先原来是-1或者1,从而导致变成了-2或者2,所以我们继续向上找祖先,看它的平衡因子是否需要调整
3.如果插入节点cur后,parent的平衡因子是2或者-2。
这里说明原来parent的平衡因子是1或者-1,我们的cur插入到了两边较高的那一边,从而导致树的不平衡,这里已经违反了规则,我们需要进行调整,进行调整的方式是什么呢?也就是我们标题所说的——旋转
根据以上的情况,我们先写一部分代码:
while (cur->_parent)
{
if (cur == cur->_parent->_left)
cur->_parent->_bf--;
else
cur->_parent->_bf++;
if (cur->_parent->_bf == 0) // 说明之前是1或-1,不需要往上更新
break;
else if (cur->_parent->_bf == 1 || cur->_parent->_bf == -1) // 说明之前是0,需要往上更新
cur = cur->_parent;
else if (cur->_parent->_bf == 2 || cur->_parent->_bf == -2) // 现在是2或-2,之前是1或者-1,已经违反了规则,需要往上更新,需要开始旋转
{
//...
}
else // 说明出现问题了
assert(false);
}
return true;
3.旋转
看到了上面的 //... 了嘛,我们只要完成了那部分的代码,插入的实现代码我们就了结啦。
进行到了这里,我们已经完成了50%了,接下来就是AVL树最重要也是最复杂的旋转部分。由于我们的不平衡,所以我们需要使它所有的子树都平衡,我们知道不平衡的原因就是某一边比另一边高了两个高度,所以为了让他平衡,就是要把太高的那部分降下来,所以就是旋转的本质。接下来我们就分类讨论有哪几种情况需要旋转。
3.1右单旋
如果我们新插入的节点在较高左子树的左侧,我们就要进行右单旋,使左侧平衡:
在这种情况下,对于parent的左边比右边高了两个高度,所以解决方案就是将60下沉,让subL做根,这样就能使右边的高度也变成h+1,而左边的高度也会是h+1,这样不就实现了平衡嘛。如下:
经过了这样的旋转之后,就能完美的解决问题啦!
那么我们的代码该如何编写呢?核心逻辑无非是那两个,一是各个节点之间的关系要改变,像我定义了每个节点的parent,所以要改变父亲是谁,要改变儿子是谁,要改变父亲的父亲的儿子要换成谁;二是平衡因子要改变。
其次还有一个特殊的问题就是,如果我们的60是根呢?也就是我们parent节点没有父亲了呢?这就说明我们的root节点要改变换人,还要把新根的父亲置为空,这里面的逻辑十分复杂,要考虑的情况特别多,同学们要谨慎哦!
话不多说,我们来把想的来实现为代码,拥有了上述的思路和图的辅助,写代码还是不难的:
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
Node* pparent = parent->_parent; // 父亲的父亲
subL->_right = parent;
if (subLR) // 有可能为空
subLR->_parent = parent;
parent->_left = subLR;
parent->_parent = subL;
if (pparent == nullptr) // 说明是根
{
_root = subL;
subL->_parent = nullptr;
}
else // 是子树
{
if (parent == pparent->_left) // parent是左子树
pparent->_left = subL;
if (parent == pparent->_right) // parent是右子树
pparent->_right = subL;
subL->_parent = pparent;
}
parent->_bf = subL->_bf = 0; // 我们的高节点下沉以后,平衡因子是一定为0的
}
3.2左单旋
如果我们新插入的节点是较高右子树的右侧,我们就要实现左单旋,来使其平衡。
有了右单旋的基础,我们再来看左单旋,其实就是一种镜像版,来看图就知道了:
而这里我们要实现的就是高的那边左下沉,实现后如下:
这里就不需要多说了吧,和右单旋是一样的道理,就直接代码咯:
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* pparent = parent->_parent;
parent->_right = subRL; // 链接parent的右节点,若subRL是空也没事
if (subRL)
subRL->_parent = parent; // 链接subRL的父亲,如果是空肯定没有父亲
subR->_left = parent; // 链接subR的左孩子
parent->_parent = subR; // 链接parent的父亲
// 开始处理根节点与新根的关系
if (pparent == nullptr) // 如果原本parent的父亲是空,说明parent是根节点
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (pparent->_left == parent)
pparent->_left = subR;
else if (pparent->_right == parent)
pparent->_right = subR;
subR->_parent = pparent;
}
parent->_bf = subR->_bf = 0;
}
3.3左右旋
当新节点插入到较高左子树的右侧,那么我们该如何是好呢?我们先看看图再分析分析:
我们能看到,原本60的平衡因子是0的,因为两边都是h-1,而30的平衡因子原来也是0的,因为两边都是h,而90原来是-1的,因为右边是h,左边是h+1,而我们新曾了这个节点之后,60变为了-1,30变为了1,而90变为了-2,这就导致了我们的违反规则。
还是一样,我们的想法还是:太高的往下沉,记住这个思想就好了,90不是右边只有h,左边有h+2嘛,我们就让他们都变成h+1,这样不就实现平衡了嘛,那我们看看,哪个节点适合做新的根呢?我们发现,只有60是比30大,但是又比90小的,那么我们是不是可以把60推上去做根,而且我们也可以发现,如果把60推上去了,90就会在60的右边,那么右边的高度将会是h+1,而30会在60的左边,那么左边的高度也会是h+1,好像会实现平衡诶,所以为了把60推上去,我们需要先把60进行左单旋,结果如下:
很好,60离根只有一步之遥了,接下来我们需要把90下来,让60上去,所以我们需要对90进行一次右单旋,结果如下:
是不是很神奇,当进行两次旋转之后,竟然达到了平衡,相信你们肯定认为代码会很复杂,其实不是的,只是进行了两次旋转,但是这个过程是非常难想的,我们只是站在了巨人的肩膀上,那么我们旋转完后最复杂的点在于哪里呢?当然是——平衡因子!
我们的平衡因子经过这么多的变化,到底是这么变化的呢?有什么规律嘛?答案是有的,比如左右旋,根据上图发现,我们把60做为了根节点,而它原来的左子树给了30的右子树,原来的右子树给了90的左子树,而我们原来的b和c的高度都是h-1,新增后b的高度变为了h,而我们插入到了具有高度为h的左子树的30节点的右子树上,刚刚好实现了30节点的平衡,所以30节点的平衡因子一定是0,而我们的90节点,我们把c插入到了具有高度为h的右子树的90节点的左子树上,所以90节点的平衡因子一定是1,因为右边比左边高一,而60不用多说,平衡因子一定是0,因为右边和左边的最大高度都是h
但是别忘了,我们上图只讨论了插入在左边的情况,还可能插入在右边,所以我们又要讨论一番,而插入在右边的话就会影响60原来的平衡因子,会变为1,而最终会使30的平衡因子变为-1,90的平衡因子还是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) // 新增节点
{
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 0;
}
else if (bf == 1)
{
subL->_bf = -1;
subLR->_bf = 0;
parent->_bf = 0;
}
else if (bf == -1)
{
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 1;
}
else
{
assert(false);
}
}
3.4右左旋
新节点插入在较高右子树的左侧,这样我们就需要右左旋了,有了左右旋的基础,再看右左旋就容易多了,也是一样的思想,把高的下沉,我们先看看图:
我们会先对90进行右旋:
再对30进行左旋,使其下沉:
这和左右旋的思路几乎一模一样,还有值得注意的是,也是有可能插入到左边的,所以继续我们的代码吧:
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(parent->_right);
RotateL(parent);
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);
}
}
4.链接
我们实现完旋转的代码之后,就可以和我们的总体部分进行链接了,经过上面的分析,我们可以发现什么时候旋转是有逻辑的、有规律的,那就是:同号单旋、异号双旋。看谁的号呢,就是看parent和cur 的号,cur就是从插入节点上来的那个cur,也就是有一个出问题的父亲的cur,所以一总结就能得出以下代码:
if (parent->_bf == -2 && cur->_bf == -1)
RotateR(parent);
else if (parent->_bf == 2 && cur->_bf == 1)
RotateL(parent);
else if (parent->_bf == -2 && cur->_bf == 1)
RotateLR(parent);
else if (parent->_bf == 2 && cur->_bf == -1)
RotateRL(parent);
break;
最后别忘了break哦,防止再进去变平衡因子。就会全部错误。
5.总结
到这呢,我们的AVL树的插入代码算是完成了,你没看错,这仅仅是插入,现在认识到了它的恐怖之处了吧,我也认识到了,不多说,完整代码放在下面:
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
template<class K>
struct AVLTreeNode
{
K _key;
AVLTreeNode<K>* _left; // 右节点
AVLTreeNode<K>* _right; // 左节点
AVLTreeNode<K>* _parent; // 父亲节点
int _bf; // 平衡因子 balance factor
AVLTreeNode(const K& key)
:_key(key)
,_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_bf(0)
{}
};
template<class K>
class AVLTree
{
public:
using Node = AVLTreeNode<K>;
bool Insert(const K& key)
{
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else
return false;
}
cur = new Node(key);
if (key > parent->_key)
parent->_right = cur;
else
parent->_left = cur;
cur->_parent = parent;
while (parent)
{
// 更新平衡因子
if (cur == parent->_left)
parent->_bf--;
else
parent->_bf++;
if (parent->_bf == 0) // 说明之前是1或-1,不需要往上更新
break;
else if (parent->_bf == 1 || parent->_bf == -1) // 说明之前是0,需要往上更新
{
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2) // 现在是2或-2,之前是1或者-1,已经违反了规则,需要往上更新,需要开始旋转
{
if (parent->_bf == -2 && cur->_bf == -1)
RotateR(parent);
else if (parent->_bf == 2 && cur->_bf == 1)
RotateL(parent);
else if (parent->_bf == -2 && cur->_bf == 1)
RotateLR(parent);
else if (parent->_bf == 2 && cur->_bf == -1)
RotateRL(parent);
break;
}
else // 说明出现问题了
assert(false);
}
return true;
}
private:
// 左旋
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* pparent = parent->_parent;
parent->_right = subRL; // 链接parent的右节点,若subRL是空也没事
if (subRL)
subRL->_parent = parent; // 链接subRL的父亲,如果是空肯定没有父亲
subR->_left = parent; // 链接subR的左孩子
parent->_parent = subR; // 链接parent的父亲
// 开始处理根节点与新根的关系
if (pparent == nullptr) // 如果原本parent的父亲是空,说明parent是根节点
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (pparent->_left == parent)
pparent->_left = subR;
else if (pparent->_right == parent)
pparent->_right = subR;
subR->_parent = pparent;
}
parent->_bf = subR->_bf = 0;
}
// 右旋
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
Node* pparent = parent->_parent; // 父亲的父亲
parent->_left = subLR;
if (subLR) // 有可能为空
subLR->_parent = parent;
subL->_right = parent;
parent->_parent = subL;
if (pparent == nullptr) // 说明是根
{
_root = subL;
subL->_parent = nullptr;
}
else // 是子树
{
if (parent == pparent->_left) // parent是左子树
pparent->_left = subL;
if (parent == pparent->_right) // parent是右子树
pparent->_right = subL;
subL->_parent = pparent;
}
parent->_bf = subL->_bf = 0; // 我们的高节点下沉以后,平衡因子是一定为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) // 新增节点
{
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 0;
}
else if (bf == 1)
{
subL->_bf = -1;
subLR->_bf = 0;
parent->_bf = 0;
}
else if (bf == -1)
{
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 1;
}
else
{
assert(false);
}
}
// 右左旋
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(parent->_right);
RotateL(parent);
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);
}
}
Node* _root = nullptr;
};