一. AVL的概念
AVL树是最先发明的⾃平衡⼆叉查找树,AVL是⼀颗空树,或者具备下列性质的⼆叉搜索树:它的 左右⼦树都是AVL树,且左右⼦树的⾼度差的绝对值不超过1。AVL树是⼀颗⾼度平衡搜索⼆叉树,通过控制⾼度差去控制平衡。
•
AVL树得名于它的发明者G. M. Adelson-Velsky和E. M. Landis是两个前苏联的科学家,他们在1962年的论⽂《An algorithm for the organization of information》中发表了它。
•
AVL树实现这⾥我们引⼊⼀个平衡因⼦(balance factor)的概念,每个结点都有⼀个平衡因⼦,任何 结点的平衡因⼦等于右⼦树的⾼度减去左⼦树的⾼度,也就是说任何结点的平衡因⼦等于0/1/-1, AVL树并不是必须要平衡因⼦,但是有了平衡因⼦可以更⽅便我们去进⾏观察和控制树是否平衡,就像⼀个⻛向标⼀样。
•
思考⼀下为什么AVL树是⾼度平衡搜索⼆叉树,要求⾼度差不超过1,⽽不是⾼度差是0呢?0不是更好的平衡吗?画画图分析我们发现,不是不想这样设计,⽽是有些情况是做不到⾼度差是0的。⽐如⼀棵树是2个结点,4个结点等情况下,⾼度差最好就是1,⽆法做到⾼度差是0
•
AVL树整体结点数量和分布和完全⼆叉树类似,⾼度可以控制在 ,那么增删查改的效率也可
以控制在 O(logN),相⽐⼆叉搜索树有了本质的提升。

这颗树是平衡的。

此时插入了一个13,此时这棵树就失衡了,失衡点是10。
二. AVL树的实现
2.1 AVL树的结构

这是我们实现的树结构。
2.2 AVL树的插⼊
2.2.1 AVL树插⼊⼀个值的⼤概过程
-
插⼊⼀个值按⼆叉搜索树规则进⾏插⼊。
-
新增结点以后,只会影响祖先结点的⾼度,也就是可能会影响部分祖先结点的平衡因⼦,所以更新 从新增结点->根结点路径上的平衡因⼦,实际中最坏情况下要更新到根,有些情况更新到中间就可 以停⽌了,具体情况我们下⾯再详细分析。
-
更新平衡因⼦过程中没有出现问题,则插⼊结束
-
更新平衡因⼦过程中出现不平衡,对不平衡⼦树旋转,旋转后本质调平衡的同时,本质降低了⼦树的⾼度,不会再影响上⼀层,所以插⼊结束。
2.2.2 平衡因⼦更新
更新原则:
平衡因⼦ = 右⼦树⾼度-左⼦树⾼度
只有⼦树⾼度变化才会影响当前结点平衡因⼦。
插⼊结点,会增加⾼度,所以新增结点在parent的右⼦树,parent的平衡因⼦++,新增结点在
parent的左⼦树,parent平衡因⼦--
parent所在⼦树的⾼度是否变化决定了是否会继续往上更新
更新停⽌条件:
更新后parent的平衡因⼦等于0,更新中parent的平衡因⼦变化为-1->0 或者 1->0,说明更新前
parent⼦树⼀边⾼⼀边低,新增的结点插⼊在低的那边,插⼊后parent所在的⼦树⾼度不变,不会
影响parent的⽗亲结点的平衡因⼦,更新结束。
更新后parent的平衡因⼦等于1 或 -1,更新前更新中parent的平衡因⼦变化为0->1 或者 0->-1,说
明更新前parent⼦树两边⼀样⾼,新增的插⼊结点后,parent所在的⼦树⼀边⾼⼀边低,parent所
在的⼦树符合平衡要求,但是⾼度增加了1,会影响parent的⽗亲结点的平衡因⼦,所以要继续向
上更新。
更新后parent的平衡因⼦等于2 或 -2,更新前更新中parent的平衡因⼦变化为1->2 或者 -1->-2,说
明更新前parent⼦树⼀边⾼⼀边低,新增的插⼊结点在⾼的那边,parent所在的⼦树⾼的那边更⾼
了,破坏了平衡,parent所在的⼦树不符合平衡要求,需要旋转处理,旋转的⽬标有两个:1、把
parent⼦树旋转平衡。2、降低parent⼦树的⾼度,恢复到插⼊结点以前的⾼度。所以旋转后也不
需要继续往上更新,插⼊结束。
不断更新,更新到根,跟的平衡因⼦是1或-1也停⽌了。
我们了解了上面的之后,可以再来看一下是怎么完成++操作的。

我们先完成了bf的++/--操作,接下来我们就要完成旋转操作了。
2.3 旋转
2.3.1 旋转的原则
-
保持搜索树的规则
-
让旋转的树从不满⾜变平衡,其次降低旋转树的⾼度
旋转总共分为四种,左单旋/右单旋/左右双旋/右左双旋。
说明:下⾯的图中,有些结点我们给的是具体值,如10和5等结点,这⾥是为了⽅便讲解,实际中是什么值都可以,只要⼤⼩关系符合搜索树的性质即可。
2.3.2 右单旋
本图1展⽰的是10为根的树,有a/b/c抽象为三棵⾼度为h的⼦树(h>=0),a/b/c均符合AVL树的要
求。10可能是整棵树的根,也可能是⼀个整棵树中局部的⼦树的根。这⾥a/b/c是⾼度为h的⼦树,
是⼀种概括抽象表⽰,他代表了所有右单旋的场景,实际右单旋形态有很多种,具体图2/图3/图4/
图5进⾏了详细描述。
•
在a⼦树中插⼊⼀个新结点,导致a⼦树的⾼度从h变成h+1,不断向上更新平衡因⼦,导致10的平
衡因⼦从-1变成-2,10为根的树左右⾼度差超过1,违反平衡规则。10为根的树左边太⾼了,需要
往右边旋转,控制两棵树的平衡。
•
旋转核⼼步骤,因为5 < b⼦树的值 < 10,将b变成10的左⼦树,10变成5的右⼦树,5变成这棵树新 的根,符合搜索树的规则,控制了平衡,同时这棵的⾼度恢复到了插⼊之前的h+2,符合旋转原
则。如果插⼊之前10整棵树的⼀个局部⼦树,旋转后不会再影响上⼀层,插⼊结束了。

图 1

图 2

图 3
如果感觉上面太抽象的话,可以看图二和图三,我来讲解一下。
我们可以发现图二失衡了,此时我们就需要旋转了,旋转的要求就是还是保持搜索二叉树的,此时是失衡节点的左子树的左子树,是LL型的,此时我们需要右旋,右旋就是让10变成5的右孩子,如果5本身就有右孩子的话,就是图三的情况,我们需要让10变成5的右孩子,再让5的原本的右孩子变成10的左孩子,此时还是满足搜索二叉树的,其他的节点不要动,此时就完成了平衡的操作。
我们下面用代码写一下右旋的操作吧。

我们通过插入函数中的这个if语句先找到失衡的节点,随后再看它的失衡类型,此时是右旋,我们可以通过图二看到,失衡节点的平衡因子是-2,它的左子树的平衡因子是-1,此时是LL型的,需要右旋,下面我们来完成一下这个右旋操作,首先我们先来看一下右旋之后的情况吧。

我们可以拿这个树来理一下思路。
首先我们右旋成下面这样。

我们来看一下它都需要改变哪些指向,我们的parent指向失衡节点,cur指向失衡节点的左孩子,我们先来看一下,首先就是我们的cur->right=parent,让parent变成了cur的右孩子,我们知道让parent->left=cur->right,图中cur没有右孩子,此时parent->left原来是指向cur的,此时就指向空了,但是我们的cur是可能有右孩子的,所以这里需要判断一下,存在我们还得改变这个右孩子的指向,就是cur->right->parent=parent了,这样就完成了一部分操作,还有就是parent->parent=cur,此时5变成10的父母了,所以要改变,最后就是5的父母了,我们知道5是有可能有父母的,所以这里还得判断一下5是否是根节点如果是就指向空,不是就指向10原来的父母了,下面是代码的书写。

这样我们就完成了右旋操作了。
2.3.3 左单旋
我们了解了右旋转之后,此时就要看一下左旋转了,我们直接看一个例子吧。
此时我们发现上图这棵树失衡了,失衡节点是5,此时是失衡节点的右子树的右子树失衡的,是RR型。此时需要左旋了,左旋就是让5作为10的左孩子,10原来的左孩子成为5的右孩子,此时还是保持搜索二叉树的,此时也完成了平衡。
旋转完就是这样了,这个和右旋转的思路是一样的只需要注意是左孩子还是右孩子即可,下面我们直接来看代码。
就是这样的,大家可以好好看看思考一下。
2.3.4 左右旋转
这个也很好理解,就是失衡节点的左子树的右子树导致的失衡,我们来看一个例子。

这个就是左右旋转了,我们可以看到是失衡节点的左子树的右子树导致的,此时我们通过单旋是无法完成的,此时就需要用左右旋转了,我直接来说是怎么旋转的吧,我也教大家一个方法记一下,左子树的右子树导致失衡的,就是LR型的,L是左吗,所以就是先让失衡节点的左孩子左旋,再让失衡节点右旋即可。
我们来看一下。
先左旋。

画的不好请见谅,左旋完就成这样了,此时就变成LL型了,再以失衡结点右旋一下就行了。

此时就是这样了,我们发现完成了平衡。

直接复用上面的代码即可。
这样我们只是完成了平衡,但是平衡因子我们并没有给到它该有的值,因为我们的LR型是有多种情况的,下面我们来看一下。

这是我们的第一种,此时我们通过旋转,我们的旋转的这三个的平衡因子都是0,此时是可以的。


这就是我们通过两次旋转得到的树,此时我们只需要看我们旋转的这几个点的平衡因子即可,我们发现10的平衡因子不对,也就是我们原来的parent,所以还需要分情况讨论一下。
第三种情况


这是我们旋转两次得到的平衡二叉树,我们发现它的平衡因子也是不对的。
综合上面三种情况,我们发现我们可以通过8的平衡因子来判断是哪种情况。
下面我们来补全一下我们的代码。

就是这样的,大家可以好好看看理解一下。
2.3.5 右左双旋
上面我们说了有左右双旋,那么肯定也是存在右左双旋的,我们下面举个例子来看一下吧。

这个就是一个右左双旋的例子,我们发现它是失衡结点的右子树的左子树导致的失衡,所以它是RL型的。
直接旋转就行了,先让失衡结点的右孩子右旋再让失衡结点左旋即可完成平衡操作。

就是这样的,代码也是复用上面的代码。

这个同理也是三种情况,也是需要补充代码的,我就直接给结果了,大家可以模仿上面的自己试一下。

这样就没有问题了,不好理解了,大家那张纸画画图就理解了。
三.测试
在测试之前先完成一些必要的函数。
3.1中序遍历

3.2 Height和Size

3.3 Find

这些函数实现起来都很简单,不过多说明了。
3.4 测试函数

这就是我们的测试函数。

测试一下,如果程序没崩溃,说明代码没问题。

我们发现是没有问题的。
四.结束语
感谢大家的查看,希望可以帮助到大家,做的不是太好还请见谅,其中有什么不懂的可以留言询问,我都会一一回答。 感谢大家的一键三连。
