题目来源:
中国大学MOOC-陈越、何钦铭-数据结构
题目详情:
译文:
AVL树是一种自平衡二叉搜索树。 在AVL树中,任何节点的两个子树的高度最多相差1。 如果它们之间的高度差超过1,则将进行重新平衡以恢复此属性。 图1-4为旋转规则。
现在给出了一系列插入,返回得到的AVL树的根结点。
输入格式:
每个输入文件包含一个测试用例。 对于每种情况,第一行都包含一个正整数N
(≤20),它是要插入的键的总数。 然后在下一行中给出N
个不同的整数键。 一行中的所有数字都用空格分隔。
输出格式:
对于每个测试用例,将AVL树的根打印在一行中。
输入样例1:
5
88 70 61 96 120
输出样例1:
70
输入样例2:
7
88 70 61 96 120 90 65
输出样例2:
88
解题思路:
AVL树是一棵动态的树,即在新的结点插入的过程中,它会自动平衡使自己满足任意结点的左右子树高度差不大于1,因此题目输入样例给出AVL树的每个结点值我们可以“在线”插入,然后根据插入数据后树的状态进行调整。具体怎么调整就要引入2个概念,即破坏平衡的发现者与破坏者,我们调整AVL树就是针对这两个结点进行调整。
破坏者:顾名思义,即为新插入了一个结点,该结点使得树变得不满足任意结点的左右子树的高度差不大于1这条性质,那么这个结点成为“破坏者”。
发现者:即离“破坏者”最近的,左右子树高度差大于1这条性质被破坏的结点。
直观地说明的话,参考题目中的Fig1,因为最后插入了结点61,导致结点88的左子树高度为2,右子树的高度为0,因此破坏了AVL树的性质,所以结点61就是“破坏者”,结点“88”就是发现者。
知道了这两个概念后,再看如何将平衡被破坏的AVL调平衡,再看Fig的调整方式,它是将结点70提上来作为根节点,把结点88放下去左右结点70的右子树,相当于将该树按着顺时针的方向旋转了一下,这种方法成为左单旋(LL旋转),为什么叫左单旋呢,看LL旋转更好理解,因为破坏者(即结点61)是位于发现者(即结点88)的左子树的左边,因此为LL旋转。具体的做法就是将破坏者的父结点移到发现者的位置上,将发现者移到破坏者父结点的右儿子的位置上,因为破坏者父结点的右儿子一定是空,并且需要右儿子的值大于父结点,这么做刚好满足AVL树的性质,并且还是一棵搜索树。
同理,看Fig2,因为结点120的插入,导致平衡被破坏,破坏者为结点120,发现者为结点88,因此我们要调平衡的话就要使用右单旋(RR旋转),因为破坏者位于发现者的右子树的右边。做法参考LL旋转,即将破坏者的父结点移到发现者的位置上,将发现者移到破坏者父结点左儿子的位置上。
还有剩下的两种情况,即破坏者位于发现者的左子树的右边和破坏者位于发现者的右子树的左边,我们分别可以使用LR旋转(先做一次RR旋转再做一次LL旋转)和RL旋转(先做一次LL旋转,再做一次RR旋转)。图Fig4即是采用LR旋转恢复平衡,因为结点65的插入导致树的平衡被破坏,因此结点65是破坏者,而离结点65最近的被破坏平衡的结点是结点70,因此结点70为发现者,而结点65位于结点70的左子树的右边,因此做LR旋转。
以上就是如何将被破坏平衡的AVL树恢复平衡,4个旋转的方式
具体的旋转方法可以参考程序。
完整的程序如下:
#include <stdio.h>
#include <stdlib.h>
#define max(x, y) (x >= y ? x : y) //定义的宏,用来取最大值
typedef struct AVLNode *Tree;
typedef struct AVLNode{
int val; //结点的值
Tree left; //左子树
Tree right; //右子树
int height; //该结点的高度
}</