初识C++之AVL树
Posted 网络天使莱娜酱
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了初识C++之AVL树相关的知识,希望对你有一定的参考价值。
目录
一、AVL树的概念
AVL树,说白了其实也是一颗二叉搜索树。 二叉搜索树虽然可以缩短查找到的效率,但如果数据有序或接近有序,二叉搜索树就会退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下:
因此,两位俄罗斯数学家就在1962年发明一种解决问题的方法:当向二叉搜索树中插入新节点后,如果能保证每个节点的左右子树高度之差的绝对值不超过1(需要对树中的节点进行调整),即可降低树的高度,从而减少平均搜索长度。这种树就被称为AVL树。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
(1)它的左右子树都是AVL树
(2)左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)。一般来讲,平衡因子 = 右子树的高度 - 左子树高度。
当然AVL树并不是一定要加入平衡因子。加入平衡因子只是实现AVL树的一种方法。
上图就是一棵AVL树,它的所有节点的平衡因子的绝对值都不超过1。
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个节点,其高度可保持在O(logN),时间复杂度也为O(logN)。
二、模拟实现一个AVL树
1.结构体定义
namespace MyAVLTree
template<class K, class V>
struct AVLTreeNode
pair<K, V> _kv;//kv结构,存储key和value
//AVL树和普通的二叉树不同,最好使用三叉链结构
AVLTreeNode<K, V>* _left;//左子节点
AVLTreeNode<K, V>* _right;//右子节点
AVLTreeNode<K, V>* _parent;//父节点
int _bf;//平衡因子
AVLTreeNode(const pair<K, V>& kv)
: _kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
;
template<class K, class V>
class AVLTree
typedef AVLTreeNode<K, V> Node;//节点重命名
public:
private:
Node* _root;//创建节点
;
AVL树的结构体定义就和普通的二叉树不同了。
首先因为它所使用的是KV模型,所以这里就创建了一个pair<K, V>来存储key和value。当然,如果你不想使用kv模型,只想用k模型,修改一下结构体即可。
第二个不同就是AVL树用的是三叉链,多了一个指向父节点的指针。该指针在修改平衡因子和让二叉树平衡有着重要作用。
第三个就是这里的AVL数中多了一个平衡因子,用于记录每棵子树的高度差。上文也说了,平衡因子只是实现AVL树的一种方法,你也可以使用其他方法来实现AVL树。
2.更新平衡因子
平衡因子上的高度差表示该子树是否为AVL树,如果它的绝对值大于1,就说明该树不是AVL树,修改调整。因此,每插入一个节点,都可能需要对平衡因子进行更新。
那么平衡因子怎么更新呢?先假设我们有以下一个AVL树:
(1)在它的8号节点的左子树插入一个节点:
此时8号节点上的平衡因子就需要更新为0。但是当8号因子更新后,就无需再向上更新了。
(2)假设现在有如下一棵AVL树:
在它的9号节点上插入一个节点:
此时业需要更新节点。但是这次就不再是只更新插入节点的父节点,而是需要一直向上更新,直到更新到根节点。这也就说明,平衡因子的更新并不是全部节点都需要更新,但是在最坏情况下,需要一直更新到根节点。
(3)假设现在有如下AVL树:
在9号节点插入一个节点:
此时再更新平衡因子,就会导致8号节点上的平衡因子的绝对值大于1,导致该子树不是AVL树,就需要对其进行调整。
通过上面的三种插入节点的例子,就可以总结出如下更新平衡因子的规律。
(1)如果在父节点的右节点插入,就要让父节点的平衡因子++;如果在父节点的左节点插入,就要让父节点的平衡因子--。
(2)平衡因子可能需要一直向上更新。如果一个节点的平衡因子更新为0,就不再需要更新它的父节点。因为当平衡因子更新为0时,说明该节点原来的平衡因子为-1或1,此时它的左子节点或右子节点为空。插入一个节点使其更新为0后,就是填上了空节点;反之如果更新为1或-1,就需要向上更新,因此此时该子树的高度发生变化。
(3)如果一个节点上的平衡因子更新为2或-2,就说明该子树不是AVL树,需要进行调整。
通过上面的三个结论,就可以写出更新平衡因子的代码:
//更新平衡因子
while (parent)//当parent为空,即为根节点的父节点时结束
if (cur == parent->_left)//插入在左子节点
--parent->_bf;
else if (cur == parent->_right)//插入在右子节点
++parent->_bf;
if (parent->_bf == 0)//父节点的平衡因子为0,不再需要向上更新
break;
else if (parent->_bf == 1 || parent->_bf == -1)//父节点的平衡因子为1或-1,
//继续向上跟新
cur = parent;
parent = parent->_parent;
else if (parent->_bf == 2 || parent->_bf == -2)//父节点的平衡因子为2或-2,
//插入有问题,调整
//调整方法
else//来到这里,说明程序出错有问题
assert(false);
当然,虽然更新平衡因子的代码写好了,但是当平衡因子为2或-2时,需要对子树进行调整的代码还没有写好,这里的内容放在下面讲。
这里没有写函数定义是因为这一部分是放在insert()函数中的,方便后续使用。当然,如果你想将其提取出来单独写成一个函数体也是可以的。
3.旋转子树
当更新完平衡因子后,如果遇到平衡因子大于1的绝对值的情况,就需要调整子树。而调整子树的方法,就是进行旋转。
让子树进行旋转,就要达到以下几个目的:
(1)让这棵子树的左右子树高度差不超过1
(2)旋转后的子树依然为搜索树
(3)更新调整子节点的平衡因子
(4)让这棵子树的高度和插入前保持一致
要达成旋转,又分为几种情况。
3.1 新节点插入较高右子树的右侧——右右->左单旋
从字面意思就能理解,当一个新的节点插入到较高右子树的右侧时,就需要将这棵子树进行左单旋。单看文字可能不太好理解,我们就用不同高度的AVL树来进行模拟。
在上图中,就是一个AVL树,其中的a、b、c分别代表了一个高度为h的子树。这棵子树的右子树就先天比左子树高1。在这里,就让h等于不同的值来模拟向较高右子树的右子节点插入的情况。
(1)当h = 0时:
当h等于0时,向右子树的右侧插入,可以画出如下AVL树:
可以看到,此时根节点上的平衡因子就会变成2,需要进行调整。调整的方法很简单,根据二叉搜索树左子树必然小于右子树的特性,就可以让节点20作为根节点,让节点15和30分别作为节点20的左右子节点。然后分别更新15、20和20号节点的左子节点的父指针。最后让15号节点的原父节点指向20号节点:
此时就可以在让该子树保持二叉搜索树特性的同时,左右子树的平衡因子君不大于1的绝对值,依然是一棵AVL树。
(2)当h = 1时:
当h = 2,就可以画出如上的AVL树。此时向它的右子树的右侧插入:
此时,就会导致以30位根节点的子树的平衡因子等于2,需要进行旋转。将20节点作为基点,让20号节点及其左子树作为30号节点的左子树,然后让30号节点的左子树作为它的原父节点20号节点的左子树。再分别跟新20、30和25号节点的父指针。最后让20号节点的原父节点指向30号节点。此时 就使得整棵树为AVL树。
(3)当h = 2时:
当h等于2时,就可以画出如上AVL树。继续在该AVL树的右子树的右侧插入:
此时根节点的平衡因子为2,需要调整。以20为基点,让30好节点的父节点20号节点及其左子树充当30号节点的左子树,然后让30号节点的左子树充当20号节点的右子树。再分别更新20、30和25号节点的父指针。最后让20号节点的原父节点指向30号节点:
通过上面的三个实例,就可以总结出在一棵右子树较高的AVL树的右子节点处插入时的调整规律。即首先以平衡因子为2的节点为基点,将基点及其左子树当做基点的右子节点的左子树,而基点的右子节点的左子树作为基点的右子树。然后分别更新基点、基点的右子节点、基点的右子节点的左子节点的父指针。最后让基点原来的父节点指向基点原来的右子节点。
并且通过观察,可以发现,在旋转完后,传入的基点和基点的下一个节点上的平衡因子都会被置为0。且只需调整这两个节点。因为只有这两个节点的位置的左右子树被调整了,其他节点的左右子树都未调整。
void RotateL(Node* parent)//左单旋
Node* subR = parent->_right;//构建节点指向基点的右子节点
Node* subRL = subR->_left;//构建节点指向subR节点的左子节点
parent->_right = subRL;//让基点的右节点指向cur的左子节点
subR->_left = parent;//让cur的左子节点指向基点
if(subRL)//当subRL不为空时才需要更新
subRL->_parent = parent;//该节点此时为基点的右子节点,更新_parent指向基点
Node* ppNode = parent->_parent;//记录基点的父节点
subR->_parent = parent->_parent;//此时cur替代基点的位置,让cur的parent指向上一个父节点
parent->_parent = subR;//此时基点为cur的左子节点,更新parent让其指向cur
if (parent->_parent == nullptr)//parent的父节点为空,说明它为根,更新根节点
_root = subR;
else//此时需要将原基点的父节点与现在的基点subR相链接,不链接的话该节点依然指向parent
if (ppNode->_left == parent)//基点为其父节点的左节点
ppNode->_left = subR;
else//基点为其父节点的右节点
ppNode->_right = subR;
parent->_bf == subR->_bf == 0;//更新parent和cur节点上的平衡因子,设置为0
左单旋的代码需要操作的节点其实就4个。即基点parent,基点的右节点subR,subR的左节点subRL和基点的父节点parent->_parent。
左单旋的代码有几个点需要注意。
(1)插入的节点是AVL树的右子树的右侧节点,所以在旋转时要考虑到subR节点的左子节点为空的情况。当为空时,就无需更新该节点的父节点。
(2)基点有可能是根节点。当基点为根节点时,需要将根节点_root指向subR,更新根节点。
(3)当基点不是根节点时,在更新完基点,subR和subRL的父节点后,要记得更新基点的原父节点所指向的节点。如果仅仅是让parent->_parent = subR,就会导致断链。
如在上图中,在更新完节点30、40和50后,要让节点20的_left或_right指向节点40。
从整体上看,就可以看成是以基点为中心进行了一次左旋转,因此这种旋转方式被叫做“左单旋”。
3.2 插入较高左子树的左侧——右右->右单旋
右单旋对应的情况和左单旋相反。右单旋是用于在一棵AVL树的左子树的左侧节点上插入时使用的。同样的,以如下一棵AVL树为例子,先画出h,即高度等于不同值的情况:
(1)当h等于0时:
向该AVL数的左子树的左侧插入,可以画出如下AVL树图:
此时50号节点的平衡因子为2,需要调整。即以50号节点为基点进行旋转,让50号节点的左子节点指向40号节点的右子节点,然后让40号节点的左子节点指向50号节点。再分别更新40号节点、50号节点和40号节点的左子节点(如果存在)的父节点。最后让50号指针的父节点指向40号节点。
(2)当h = 1时:
向该AVL树的左子树的左侧插入:
此时50号节点的平衡因子为-2,需要进行调整。调整方法和h=0时一样。以50号节点为基点进行旋转。先让50号节点的左子节点指向40号节点的右子树,再让40号节点的右子节点指向50号节点。然后分别跟新50、40和45号节点的父指针。最后让50号节点的原父节点指向40号节点。
通过上面的两个例子,就可以总结出当在AVL树的左子树的最左节点插入时的旋转规律。假设平衡因子为-2的节点为parent,parent的左子节点为subL,subL的右子节点为subLR。首先让parent的左子节点指向subLR,然后让subL的右子节点指向parent。然后分别更新parent、subL和subLR的父指针,最后让parent原来的父节点指向subL即可。
void RotateR(Node* parent)//右单旋
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;//让基点指向subLR
subL->_right = parent;//让subL指向基点
if (subLR)//subLR节点不为空时,更新它的父指针
subLR->_parent = parent;
Node* ppNode = parent->_parent;
parent->_parent = subL;//更新基点的父指针,指向subL
subL->_parent = ppNode;//更新subL的父指针,指向基点的原父节点
if (ppNode == nullptr)//基点的原父节点为空,说明基点为根节点
_root = subL;
_root->_parent = nullptr;
else
if (ppNode->_left == parent)//基点为原父节点的左子节点
ppNode->_left = subL;
else//基点为原父节点的右子节点
ppNode->_right = subL;
parent->_bf = subL->_bf = 0;//更新平衡因子
和左单旋一样,右单旋后的AVL树的只有parent和subL的平衡因子被改变。且通过观察可以发现,这两个节点的平衡因子都被改为了0
3.3 插入较高右子树的左侧节点——右左->右左双旋
上面的两种情况都是只需要单次旋转。但是在某些情况下,可能需要进行二次旋转。
以下图的AVL树为例,h代表的是子树的高度。
(1)当h = 0时:
在该AVL树的右子树的左侧插入:
可以看见,左单旋的情况不同,左单旋是在右子树的最右子节点的右边插入,而这里却是在左边插入。
在这种情况下,单旋就不再起作用,而是需要进行双旋转。
要将这个树调整为AVL树,第一步要以30节点为基点进行右单旋。即让25号节点的右子节点指向30号节点,让30号节点的左子节点指向25号节点的右子节点。再更新对应节点的父指针和让25号节点与20号节点相链接。
通过这种方法,就让这棵树的结构变成了可以使用左单旋的结构。
因此第二步就是进行左单旋。即让25号节点的左子节点指向20号节点,然后让20号节点的右子节点指向25号节点的左子节点。再更新对应节点的父指针和让25号节点与20号节点的原父节点相链接。
通过右左两次单旋,就将这棵树调整为了AVL树。
(2)当h = 1时:
在这个AVL树的右子树的左侧插入:
可以看到,该树的20号节点的平衡因子为2,需要调整。同时它插入的节点在右子树的最右节点的左侧。这就导致单次旋转失效。需要进行双旋。
和h=0时一样,首先以30号节点为基点进行右旋。让30号节点的左子节点指向25号节点的右子节点,再让25号节点的右子节点指向30号节点,然后更新对应节点的父指针,并让30号节点的原父节点20号节点指向25号节点。
第二步进行左单旋。以20号节点为基点进行左单旋。首先让25号节点的左子节点指向20号节点,然后让20号节点的右子节点指向23号节点,再更新20、25、23号节点的父指针,然后让20号节点的原父节点指向25号节点。
通过上面的两个例子,就可以总结出当在AVL树的右子树的左侧插入节点时的旋转规律。首先以平衡因子为2的节点的右节点为基点,进行右单旋;然后再以平衡因子为2的节点为基点,进行左单旋。
旋转完后,需要对平衡因子进行更新。一共分为三种情况。设平衡因子为2的节点为parent,parent的右子节点为subR,subR的左子节点为subRL。
情况一:在右子树的左侧节点的左子节点插入(subRL的平衡因子 == -1):
以上图为例,在进行第一次右单旋时,25号节点的右子节点指向30号节点,30号节点的左子节点指向25号节点的右子节点,该节点为空。这就导致25号节点的平衡因子变为1,30号节点的平衡因子也变为1,而20号节点的平衡因子变为2。当进行第二次左单旋后,25号节点的左子节点指向20号节点,20号节点的右子节点指向25号节点的左子节点,进而导致30号节点的平衡因子改变为1,25号和20号节点的平衡因子改变为0。
因此,当subRL的平衡因子为-1时,subR的平衡因子修改为1,parent和subRL的平衡因子修改为0
情况二:在右子树的左侧节点的右子节点插入(subRL的平衡因子 == 1):
上图的情况就是在右子树的左侧的右子节点插入。在进行第一次右单旋后,25号节点的右子节点指向30号节点,30号节点的左子节点指向25号节点的左子节点27号节点,此时就会导致25号节点的平衡因子修改为1,30号节点的平衡因子修改为0,20号节点的平衡因子修改为2。然后进行第二次左单旋。让25号节点的左子节点指向20号节点,20号节点的右子节点指向25号节点的左子节点,该节点此时为空。这就导致20号节点的平衡因子修改为-1,30号节点的平衡因子修改为0,25号节点的平衡因子修改为0
因此,当subRL的平衡因子为1时,parent的平衡因子修改为-1,subR和subRL的平衡因子修改为0
情况三:插入的节点就是它本身(subRL的平衡因子 == 0):
如上图,此时插入后引起调整的节点就是插入的subRL自己。此时的情况很简单,parent、subR和subRL的平衡因子全部置为0
void RotateRL(Node* parent)//右左双旋
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(parent->_right);
RoatteL(parent);
//更新parent、subR、subRL的平衡因子
if (bf == -1)//在subRL的左子树插入
parent->_bf = 0;
subR->_bf = 1;
subRL->__bf = 0;
else if (bf == 1)//在subRL的右子树插入
parent->_bf = -1;
subR->_bf = 0;
subRL->_bf = 0;
else if (bf == 0)//subRL就是引起调整的节点
parent->_bf = 0;
subR->_bf = 0;
subRL->_bf = 0;
else
assert(false);//走到这里,说明出现错误
右单旋和左单旋的代码实现在上面中已经写过了,所以此处直接复用即可。
3.4 在左子树的右侧插入——左右->左右双旋
以以下一棵AVL树为例:
h为该子树的高度。
(1)当h = 0时:
在该AVL树的左子树的右侧插入节点:
此时50号节点的平衡因子为-2,需要调整。要注意,这一情况和右单旋的情况是相反的。
右单旋的需要旋转的节点可以看成直线,而需要进行双旋的节点可以看成曲线。
这种情况下,需要进行两次旋转。首先是以40号节点进行左单旋。首先让45号节点的左子节点指向40号节点,然后让40节点的右子节点指向45号节点的左子节点,再更新对应节点的父指针,最后让40号节点的原父节点指向45号节点。
接着进行第二次单旋。以50号节点进行右单旋。首先让45号节点的右子节点指向50号节点,然后让50号节点的左子节点指向45号节点的右子节点,再更新对应节点的父指针,最后让50号节点的原父节点指向45号节点。
(2)当h = 1时:
在该AVL树的左子树的右侧插入节点:
此时它的50号节点的平衡因子为-2,需要调整。
第一步,以40号节点为基点进行左单旋。首先让45号节点的左子节点指向40号节点,然后让40号节点的右子节点指向45号节点的左子节点,再更新40、43、45好节点的父指针,最后让40号节点的原父节点50号节点指向45号节点。
第二步,以50号节点为基点进行右单旋。首先让45号节点右子节点指向50号节点,然后让50号节点的左子节点指向45号节点的右子节点,再更新对应节点的父指针,最后让50号节点的原父节点指向45号节点。
通过上面两个例子,就可以总结出如果在AVL树的左子树的右侧插入节点时的调整规律。首先是以平衡因子为-2的parent节点的左子节点为基点进行左单旋;然后再以parent节点为基点进行右单旋。
在旋转完成后,要记得更新平衡因子。通过观察,其实可以发现在左右双旋的情况下, 平衡因子的更新分为三种情况。
设需要调整的节点的为parent,parent节点的左子节点为subL,新插入的节点为subRL。
情况一:在左子树的右侧节点的左子节点插入(subRL的平衡因子 == -1):
如上图,在左子树的右侧节点的左子节点插入导致调整,如上图的43号节点就是这种情况。此时45号节点的左侧高,在进行左单旋时,就会将43号节点给49号节点,使40号节点的平衡因子+1变为0,45节点的平衡因子由-1变2。当进行第二次右单旋时,40节点不变,由于让45号节点指向50号节点,此时平衡,45号平衡因子为0。50号节点因为指向的45号节点的右子节点为空,平衡因子被修改为1。
在subL的平衡因子等于-1时,会有三个节点的平衡因子被修改。此时parent的平衡因子修改为1,subL和subLR的平衡因子都修改为0。
情况二:在左子树的右侧节点的右子节点插入(subRL 的平衡因子== 1):
如上图,当在AVL树的左子树的右侧的右子节点插入,即在45号插入时,45号的平衡因子由0变1,40号平衡因子由0变1。50号平衡因子由-1变为-2。在经过第一次左单旋后,因为40号拿的45号节点的左子节点为空,且让45号节点的右子节点指向40号节点,此时40号节点的平衡因子由1变为-1,45号节点的平衡因子由1变为-1,50号节点平衡因子不变。当经过第二次右单旋后,50号节点接受45号节点的右子节点,45号节点指向50号节点。导致45号节点的平衡因子由-1变为0,50号节点的平衡因子由-2变为0,40号节点的平衡因子不变。
在subRL的平衡因子等于1时,会有三个节点的平衡因子被修改。subL的平衡因子修为该-1,parent和subLR的平衡因子修改为0
情况三:当插入的节点就是它本身时(subRL的平衡因子等于0):
如上图的情况,subLR就是本身就是被插入的节点,此时subLR的平衡因子为0。
这种情况就无需过多讲解了,很容易理解。此时parent、subL和subRL的平衡因子全部置为0
void RotateLR(Node* parent)//左右单旋
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(parent->_left);
RotateR(parent);
if (bf == -1)//subLR的左子树插入
parent->_bf = 1;
subL->_bf = 0;
subLR->_bf = 0;
else if (bf == 1)//subLR的右子树插入
parent->_bf = 0;
subL->_bf = -1;
subLR->_bf = 0;
else if (bf == 0)//subLR自己就是新插入的节点
parent->_bf = 0;
subL->_bf = 0;
subLR->_bf = 0;
else
assert(false);//到这里,说明程序有错误
右单旋和左单旋的代码实现在上面中已经写过了,所以此处直接复用即可。
3.5 总结
通过上面的分析,就可以得出一棵AVL树要进行调整,一共分为4中情况。
(1)当在AVL树的右子树的右侧插入时,采用左单旋。将parent和subL的平衡因子更新为0
(3)当在AVL树的左子树的左侧插入时,采用右单旋。将parent和subL的平衡因子更新为0
(3)当在AVL树的右子树的左侧插入时,采用右左双旋,先右单旋,再左单旋。parent、subR和subRL的平衡因子都需更新,分三种情况
(4)当在AVL树的左子树的右侧插入时,采用左右双旋,先做单旋,再右单旋。parent、subL和subLR的平衡因子都需更新,分三种情况
4.完善调整平衡因子和实现数据插入
有了调整AVL树的方法后,就可以完善调整平衡因子的方法了。同时,也可以将向AVL树插入数据的方法完成了。
bool insert(const pair<K, V>& kv)
if (_root == nullptr)//检查二叉搜索树是否为空,为空则直接创建节点
_root = new Node(kv);
return true;
Node* parent = nullptr;
Node* cur = _root;
while (cur)
if (kv.first < cur->_kv.first)//键值小于节点上的键值,向左走
parent = cur;
cur = cur->_left;
else if (kv.first > cur->_kv.first)//键值大于节点上的键值,向右走
parent = cur;
cur = cur->_right;
else//传入的键值与节点上的键值相等
return false;
cur = new Node(kv);
if (kv.first < parent->_kv.first)//插入的键值比parent的小,插入在左边
parent->_left = cur;
cur->_parent = parent;//三叉链链接,让子节点的parent指针指向父节点
else//插入的键值比parent的大,插入在右边
parent->_right = cur;
cur->_parent = parent;
//更新平衡因子
while (parent)//当parent为空,即为根节点的父节点时结束
if (cur == parent->_left)//插入在左子节点
--parent->_bf;
else if (cur == parent->_right)//插入在右子节点
++parent->_bf;
if (parent->_bf == 0)//父节点的平衡因子为0,不再需要向上更新
break;
else if (parent->_bf == 1 || parent->_bf == -1)//父节点的平衡因子为1或-1,
//继续向上跟新
cur = parent;
parent = parent->_parent;
else if (parent->_bf == 2 || parent->_bf == -2)//父节点的平衡因子为2或-2,
//插入有问题,调整
//调整方法
if (parent->_bf == 2 && cur->_bf == 1)//该条件说明是在右子树的右侧插入。采用左单旋
RotateL(parent);
break;
else if (parent->_bf == -2 && cur->_bf == -1)//该条件说明是在左子树左侧插入。右单旋
RotateR(parent);
break;
else if (parent->_bf == 2 && cur->_bf == -1)//该条件说明该树的右子树高,且插入的节点在右子树的左侧
RotateRL(parent);//右左双旋
break;
else if (parent->_bf == -2 && cur->_bf == 1)//该条件说明该树的左子树高,且插入的节点在左子树的右侧
RotateLR(parent);//左右双旋
break;
else
assert(false);//走到这里,说明平衡因子调整方法出现问题
else//来到这里,说明程序出错有问题
assert(false);
return true;
void RotateL(Node* parent)//左单旋
Node* subR = parent->_right;//构建节点指向基点的右子节点
Node* subRL = subR->_left;//构建节点指向subR节点的左子节点
parent->_right = subRL;//让基点的右节点指向cur的左子节点
subR->_left = parent;//让cur的左子节点指向基点
if(subRL)//当subRL不为空时才需要更新
subRL->_parent = parent;//该节点此时为基点的右子节点,更新_parent指向基点
Node* ppNode = parent->_parent;//记录基点的父节点
subR->_parent = parent->_parent;//此时cur替代基点的位置,让cur的parent指向上一个父节点
parent->_parent = subR;//此时基点为cur的左子节点,更新parent让其指向cur
if (ppNode == nullptr)//parent的父节点为空,说明它为根,更新根节点
_root = subR;
_root->_parent = nullptr;
else//此时需要将原基点的父节点与现在的基点subR相链接,不链接的话该节点依然指向parent
if (ppNode->_left == parent)//基点为其父节点的左节点
ppNode->_left = subR;
else//基点为其父节点的右节点
ppNode->_right = subR;
parent->_bf = subR->_bf = 0;//更新parent和cur节点上的平衡因子,设置为0
void RotateR(Node* parent)//右单旋
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;//让基点指向subLR
subL->_right = parent;//让subL指向基点
if (subLR)//subLR节点不为空时,更新它的父指针
subLR->_parent = parent;
Node* ppNode = parent->_parent;
parent->_parent = subL;//更新基点的父指针,指向subL
subL->_parent = ppNode;//更新subL的父指针,指向基点的原父节点
if (ppNode == nullptr)//基点的原父节点为空,说明基点为根节点
_root = subL;
_root->_parent = nullptr;
else
if (ppNode->_left == parent)//基点为原父节点的左子节点
ppNode->_left = subL;
else//基点为原父节点的右子节点
ppNode->_right = subL;
parent->_bf = subL->_bf = 0;//更新平衡因子
void RotateRL(Node* parent)//右左双旋
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(parent->_right);
RotateL(parent);
//更新parent、subR、subRL的平衡因子
if (bf == -1)//在subRL的左子树插入
parent->_bf = 0;
subR->_bf = 1;
subRL->_bf = 0;
else if (bf == 1)//在subRL的右子树插入
parent->_bf = -1;
subR->_bf = 0;
subRL->_bf = 0;
else if (bf == 0)//subRL就是引起调整的节点
parent->_bf = 0;
subR->_bf = 0;
subRL->_bf = 0;
else
assert(false);//走到这里,说明出现错误
void RotateLR(Node* parent)//左右单旋
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(parent->_left);
RotateR(parent);
//更新parent、subL、subLR的平衡因子
if (bf == -1)//subLR的左子树插入
parent->_bf = 1;
subL->_bf = 0;
subLR->_bf = 0;
else if (bf == 1)//subLR的右子树插入
parent->_bf = 0;
subL->_bf = -1;
subLR->_bf = 0;
else if (bf == 0)//subLR自己就是新插入的节点
parent->_bf = 0;
subL->_bf = 0;
subLR->_bf = 0;
else
assert(false);//到这里,说明程序有错误
为了检查这棵树是否是AVL树,我们可以用以下代码来进行测试:
void _inorder(Node* root)//中序遍历
if (root == nullptr)
return;
_inorder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << endl;
_inorder(root->_right);
int Height(Node* root)//计算子树高度
if (root == nullptr)
return 0;
int lh = Height(root->_left);
int rh = Height(root->_right);
return lh > rh ? lh + 1 : rh + 1;
bool _isBalance(Node* root)//检查是否为AVL树
if (root == nullptr)
return true;
int leftHeight = Height(root->_left);
int rightHeight = Height(root->_right);
if (rightHeight - leftHeight != root->_bf)
cout << "平衡因子异常" << endl;
return abs(rightHeight - leftHeight) < 2 &&
_isBalance(root->_left) &&
_isBalance(root->_right);
中序遍历用于查看排序是否正常,而isBalance()则用于查看这棵树的所有子树的高度差是否不超过1的绝对值。
在最后,其实AVL树大家现在只需要知道它的插入逻辑就可以了,不需要去了解如何进行删除等操作。一是因为AVL树本身就比较难,在大家未来的笔试面试中都不会考删除相关操作,只会考AVL树的插入逻辑。二是AVL树的删除比插入还要复杂一些,学习成本有点高,在现阶段无需过多深入。当然,如果自己有兴趣,也可以尝试了解一下。
C++ 树进阶系列之平衡二叉查找树( AVL)的自平衡算法
1. 前言
树的深度与性能的关系。
在二叉排序树
上进行查找时,其时间复杂度
理论上接近二分算法
的时间复杂度O(logn)
。
但是,这里有一个问题,如果数列中的数字顺序不一样时,构建出来的二叉排序树的深度会有差异性,对最后评估时间性能会有影响。
如有数列 [36,45,67,28,20,40]
,其构建的二叉排序树如下图。
基于上面的树结构,查询任何一个结点的次数不会超过 3
次。
稍调整一下数列中数字的顺序 [20,28,36,40,45,67]
,由此构建出来的树结构会出现一边倒的现象,即增加了树的深度。此棵树的深度为6
,最多查询次数是 6
次。
可知,二叉树上的查询时间与树的深度有关,所以,减少查找次数的最好办法,就是尽可能维护树左右子树之间的对称性,也就让其有平衡性。
什么是平衡二叉排序树?
所谓平衡二叉排序树
,顾名思义,基于二叉排序树的基础之上,维护任一结点的左子树和右子树之间的深度之差不超过 1
。把二叉树上任一结点的左子树深度减去右子树深度的值称为该结点的平衡因子
。
我们经常说的平衡树指AVL树
,是Adelson-Velskii
和Landis
在1962
年提出的,它的定义如下:
- 一颗空的二叉树就
AVL
树。 - 如果
T
是一颗非空的二叉树,TL
和TR
是其左子树和右子树,如果TL
和TR
是AVL
树且|hL-hR|<=1
,其中hL和hR
指TL
和TR
的高。那么T
树一定是平衡二叉树。
平衡树的平衡因子只可能是:
0
:左、右子树深度一样。1
:左子树深度大于右子树。-1
:左子树深度小于右子树。
如下图,就是平衡二叉排序树
,根结点的左右子树深度相差为 0
, 结点 28
的左右子树深度为 1
,结点 45
的左右子树深度相差为 0
。
平衡树的意义何在?
平衡二叉树能保证在树上操作的时间复杂度始终为O(logn)
。
平衡二叉排序树本质还是二叉排序树,在此基础之上,其 API
多了维持平衡的算法。
2. 平衡算法
2.1 平衡二叉排序树的抽象数据结构
结点类:
#include <iostream>
using namespace std;
/*
*结点类
*/
template<typename T>
struct TreeNode
//结点上附加的值
T value;
//左子结点
TreeNode<T>* leftChild;
//右子结点
TreeNode<T>* rightChild;
//平衡因子,默认值为 0
int balance;
//无参构造
TreeNode()
this->leftChild=NULL;
this->rightChild=NULL;
this->balance=0;
//有参构造
TreeNode(T value)
this->value=value;
this->leftChild=NULL;
this->rightChild=NULL;
this->balance=0;
;
二叉平衡排序树类: 主要强调维持自平衡的特征函数。
/*
*树类
*/
template<typename T>
class BalanceTree
private:
//根结点
TreeNode<T>* root;
public:
BalanceTree(T value)
this->root=new TreeNode<T>(value);
TreeNode<T>* getRoot()
return this->root;
/*
*LL型调整
*/
TreeNode<T>* llRotate(TreeNode<T>* node);
/*
*RR 型调整
*/
TreeNode<T>* rrRotate(TreeNode<T>* node);
/*
*LR型调整
*/
TreeNode<T>* lrRotate(TreeNode<T>* node);
/*
*RL型调整
*/
TreeNode<T>* rlRotate(TreeNode<T>* node);
/*
*插入新结点
*/
void insert(T value);
/*
*中序遍历
*/
void inorderTraversal(TreeNode<T>* root);
bool isEmpty()
return this->root==NULL;
;
在插入或删除结点时,如果导致树结构发生了不平衡性,则需要调整让其达到平衡。这里的方案可以有 4
种。
2.2 LL
型调整(顺时针)
左边不平衡时,向右边旋转。
如下图所示,现在根结点 36
的平衡因子为 1
。
当插入值为 18
结点,定然是要作为结点 20
的左子结点,才能维持二叉排序树的有序性,但是破坏了根结点的平衡性。根结点的左子树深度变成 3
,右子树深度为1
,平衡性被打破,结点 36
的平衡因子变成了2
。
怎样旋转才能让树继续保持平衡?
旋转思路是既然左边不平衡,必然是左高右低,向右旋转(顺时针)方能维持平衡。
- 让结点
28
成为新根结点,结点36
成为结点28
的左子结点(降维左子树)。
- 新根结点的右子树
29
成为原根结点36
的新左子结点。
- 原根结点成为新根结点的右子树。
旋转后,树结构即满足了有序性,也满足了平衡性。
LL
旋转算法具体实现:
/*
*LL型调整
*/
template<typename T>
TreeNode<T>* BalanceTree<T>::llRotate(TreeNode<T>* parentRoot)
//原父结点的左子结点成为新父结点
TreeNode<T>* newparentRoot =parentRoot->leftChild;
// 新父结点的右子结点成为原父结点的左子结点
parentRoot->leftChild = newparentRoot->rightChild;
// 原父结点成为新父结点的右子结点
newparentRoot->rightChild =parentRoot;
// 重置平衡因子
parentRoot->balance = 0;
newparentRoot->balance = 0;
return newparentRoot;
2.3 RR 型调整(逆时针旋转)
RR
旋转和 LL
旋转的算法差不多,只是当右边不平衡时,向左边旋转。
如下图所示,结点 50
插入后,树的平衡性被打破。
这里使用左旋转(逆时针)方案。
- 结点
45
成为新根结点,原根结点36
向左旋转,将成为根结点45
的左子结点。
- 先将结点
45
原来的左子结点成为结点36
的右子结点。
- 再将原根结点作为新根结点的左子结点。逆时针旋转后,结点
45
的平衡因子为0
,结点36
的平衡因子为0
,结点48
的平衡因子为-1
。树的有序性和平衡性得到保持。
RR
旋转算法具体实现:
/*
*RR 型调整
*/
template<typename T>
TreeNode<T>* BalanceTree<T>::rrRotate(TreeNode<T>* parentNode)
// 右子结点
TreeNode<T>* newParentNode = parentNode->rightChild;
parentNode->rightChild = newParentNode->leftChild;
//原父结点成为新父结点的左子树
newParentNode->leftChild = parentNode;
// 重置平衡因子
parentNode->balance = 0;
newParentNode->balance = 0;
return newParentNode;
2.4 LR型调整(先逆后顺)
如下图当插入结点 28
后,结点 36
的平衡因子变成 2
,则可以使用 LR
旋转算法。
- 以结点
29
作为新的根结点,结点27
以结点29
为旋转中心,逆时针旋转。
- 结点
36
以结点29
为旋转中心向顺时针旋转。
最后得到的树还是一棵二叉平衡排序树
。
LR
旋转算法实现:
/*
*LR型调整
*/
template<typename T>
TreeNode<T>* BalanceTree<T>::lrRotate(TreeNode<T>* p_node)
// 原根结点的左子结点
TreeNode<T>* b = p_node->leftChild;
//得到新的根结点
TreeNode<T>* new_p_node = b->rightChild;
//更新原根结点的左子结点
p_node->leftChild = new_p_node->rightChild;
b->rightChild = new_p_node->leftChild;
//更新新根结点的左子结点
new_p_node->leftChild = b;
// 更新新根结点的右子结点
new_p_node->rightChild = p_node;
//重置平衡因子
if (new_p_node->balance == 1)
p_node->balance = -1;
b->balance = 0;
else if (new_p_node->balance == -1)
p_node->balance = 0;
b->balance = 1;
else
p_node->balance = 0;
b->balance = 0;
new_p_node->balance = 0;
return new_p_node;
2.5 RL型调整
如下图插入结点39
后,整棵树的平衡打破,这时可以使用 RL
旋转算法进行调整。
- 把结点
40
设置为新的根结点,结点45
以结点40
为中心点顺时针旋转,结点36
逆时针旋转。
RL
算法具体实现:
/*
*RL型调整
*/
template<typename T>
TreeNode<T>* BalanceTree<T>::rlRotate(TreeNode<T>* p_node)
//原根结点的右子树
TreeNode<T>* b = p_node->rightChild;
//新根结点
TreeNode<T>* new_p_node = b->leftChild;
//更新右子树
p_node->rightChild = new_p_node->leftChild;
b->leftChild = new_p_node->rightChild;
new_p_node->leftChild = p_node;
new_p_node->rightChild = b;
if (new_p_node->balance == 1)
p_node->balance = 0;
b->balance = -1;
else if (new_p_node->balance == -1)
p_node->balance = 1;
b->balance = 0;
else
p_node->balance = 0;
b->balance = 0;
new_p_node->balance = 0;
return new_p_node;
2.6 插入算法
编写完平衡算法后,就可以编写插入算法。在插入新结点时,需要检查是否破坏二叉平衡排序树的的平衡性,否则调用平衡算法。
当插入一个结点后,为了保持平衡,需要找到最小不平衡子树。
什么是最小不平衡子树?
指离插入结点最近,且平衡因子绝对值大于 1
的结点为根结点构成的子树。
如下图所示,树结构整体上是平衡的,但根结点的平衡因子是 1
,其实是一个脆弱的临界值,插入或删除操作就有可能打破这个平衡因子。
如插入值为 20
的结点,因为小于根结点的值,必然会导致从插入位置一路向上,一直到根结点所有直接、间接父结点的平衡因子发生变化。此时,可以把根结点到插入的新结点之间的树称为最小不平衡子树
。
出现了最小不平衡树,就要考虑怎么旋转,方能维持平衡。
/*
*插入新结点
*/
template<typename T>
void BalanceTree<T>::insert(T value)
// 创建新结点
TreeNode<T>* new_node =new TreeNode<T>(value);
if (BalanceTree<T>::root==NULL)
//如果是空树
BalanceTree<T>::root = new_node;
return;
//初始设定根结点为最小平衡树
TreeNode<T>* min_b = BalanceTree<T>::root;
//存储前驱结点
TreeNode<T>* f_node = NULL;
//移动指针
TreeNode<T>* move_node = this->root;
TreeNode<T>* f_move_node = NULL;
//查找
while (move_node!=NULL)
if (move_node->value == value)
//结点已经存在
return;
if (move_node->balance != 0)
// 记录最小不平衡子树
min_b = move_node;
//记录其前驱
f_node = f_move_node;
//移动之前,记录前驱
f_move_node = move_node;
if (new_node->value < move_node->value)
//向左边移动
move_node = move_node->leftChild;
else
//向右边移动
move_node = move_node->rightChild;
if (new_node->value < f_move_node->value)
//插入在左边
f_move_node->leftChild = new_node;
else
//插入在右边
f_move_node->rightChild = new_node;
//开始更新最小不平衡树上各父结点的平衡因子
move_node = min_b;
// 修改相关结点的平衡因子
while (move_node != new_node)
if (new_node->value < move_node->value)
move_node->balance++;
move_node = move_node->leftChild;
else
move_node->balance--;
move_node = move_node->rightChild;
if (min_b->balance > -2 && min_b->balance < 2)
//插入结点后没有破坏平衡性
return;
TreeNode<T>* b=NULL;
if (min_b->balance == 2)
b = min_b->leftChild;
if (b->balance == 1)
//打破平衡,且左边高
move_node = BalanceTree<T>:: llRotate(min_b);
else
//打破平衡,右边高
move_node = BalanceTree<T>::lrRotate(min_b);
else
b = min_b->rightChild;
if (b->balance == 1)
move_node = BalanceTree<T>::rlRotate(min_b);
else
move_node = BalanceTree<T>::rrRotate(min_b);
if (f_node==NULL)
BalanceTree<T>::root = move_node;
else if (f_node->leftChild == min_b)
f_node->leftChild = move_node;
else
f_node->rightChild = move_node;
也可以在结点类中添加一个指向父指针的成员变量,插入数据后,由下向上查找且更新平衡因子。
中序遍历: 二叉平衡排序树本质还是二树排序树,使用中序遍历输出的数字应该是有序的。
/*
*中序遍历
*/
template<typename T>
void BalanceTree<T>::inorderTraversal(TreeNode<T>* root)
if (root==NULL)
return;
BalanceTree<T>::inorderTraversal(root->leftChild);
cout<<root->value<<"->";
BalanceTree<T>::inorderTraversal(root->rightChild);
测试代码。
int main(int argc, char** argv)
int nums[] = 3, 12, 8, 10, 9, 1, 7;
BalanceTree<int>* tree=new BalanceTree<int>(3);
for (int i=1;i<sizeof(nums)/4;i++)
tree->insert(nums[i]);
// 中序遍历
tree->inorderTraversal(tree->getRoot());
return 0;
输出结果:
3. 总结
利用二叉排序树
的特性,可以实现动态查找
。在添加、删除结点之后,理论上查找到某一个结点的时间复杂度与树的结点在树中的深度是相同的。
但是,在构建二叉排序树时,因原始数列中数字顺序的不同,则会影响二叉排序树的深度。
这里引用二叉平衡排序树,用来保持树的整体结构的平衡性,方能保证查询的时间复杂度为 Ologn
(n
为结点的数量)。
以上是关于初识C++之AVL树的主要内容,如果未能解决你的问题,请参考以下文章