本文从AVL树的定义出发,一步步地推导出AVL树旋转的方案,这个推导是在已经清楚地知道AVL树的定义这个前提下进行的。文章注重思考的过程,并不会直接给出AVL树是怎样旋转的,用来提醒自己以后在学习的时候要注重推导的过程。在此,我要特别感谢下我的数据结构老师,是他让我意识到思考的重要性。
一、从AVL树的定义开始
1. 二叉查找树的问题
二叉查找树的出现,虽然使查找的平均时间降到了logN,但是,在多次删除或者插入操作后,可能会出现根节点的左子树比右子树高很多,或者右子树比左子树高很多的情况。如图所示:
这样的话,查找的效率会相当低。所以现在的问题是:要想出一种解决方案,可以使得二叉查找树能保持平衡。这时,我们要定义一种新的二叉查找树,这种二叉查找树是一种带有平衡条件的二叉查找树,也就是AVL树。
2. AVL树的定义
一棵AVL树是其每个节点的左子树和右子树的高度最多差1的二叉查找树(空树的高度定义为-1)。在下图中,左边的树是AVL树,但右边的树不是。
在右边的树中,根节点7的左子树的高度为2,右子树的高度为0,左右子树的高度差为2,所以不是一棵AVL树。
有了AVL树的定义,下面就可以开始推导AVL树是如何通过旋转来达到平衡条件的了。
二、开始推导。
1. 分析需要注意的地方。
首先,我们用正向的思维来分析下。正常情况下,当进行插入操作时,我们需要更新通向根节点路径上的那些节点的所有平衡信息,而插入操作隐含着困难的原因在于,插入一个节点可能破坏AVL树的特性。例如,将6插入到图4-29中的AVL树中将会破坏关键字为8的节点的平衡条件。如果发生这种情况,那么就要把性质恢复(也就是恢复平衡)以后才认为这一步插入完成。
2.把影响降到最小。
作为一个程序员,我们总是希望在解决问题的过程中,把影响降到最小。
在上面的分析中,我们了解到,在插入一个节点后,很可能会破坏AVL树的平衡性。而且,这种破坏平衡性的行为很可能是连锁反应,比如,当我们在图4-29中的AVL树中的关键字为3的叶节点下面添加个关键字为2.5的叶节点时,就会破坏关键字为4的节点的平衡性,不仅如此,关键字为2的节点和关键字为5的根节点的平衡性都被打破了。
这种连锁反应是十分可怕的,因此我们要把影响降至最小。如果我们能够在第一个平衡性被破坏的节点上阻止连锁反应的扩散,那么就可以把影响降到最小了。在这里,我们通过旋转来对树的局部进行简单的修正,从而把影响降到最小。
我们现在不用管是怎样旋转的,下面将进行一步步的推导,把旋转的本质推导出来,精彩的内容在下面!
3.假设某个节点为第一个平衡性被破坏的节点
现在开始,我们要分析所有可能出现的情况。
第一种情况:从第一个平衡性被破坏的节点的左边插入,从而导致的失衡
先设下图中的K2是第一个不满足AVL平衡特性的节点,也就是说,K2在没插入新节点前,是满足平衡性的。然后,插入的地方为K2的左子树:
在这种情况下,K2的左子树的高度就比它的右子树高2了。设
a. 上图中的树的名字为STree,作为整棵AVL树的一部分。
b 插入节点前,以K2为根节点的树的高度为h+2,即STree的高度为h+2;
c. 在这种情况下,插入节点前,K2的左子树的高度为h+1,K2的右子树Z的高度为h。
d.插入节点后,以K2为根节点的树的高度变成了h+3;
e.插入节点后,K2的左子树的高度为h+2 ,K2的右子树Z的高度仍然为h。
现在我们面临的问题是:要使STree的高度在插入新节点后,仍然保持不变。也就是说,要使STree的高度由h+3变回到h+2。这样,就不会发生连锁反应,把影响降至最小,因为STree的高度仍然是插入节点前的高度。由这个问题,我们又会引出一连串的问题。(有问题就对了,毕竟结论不是一下子得出来的,要经过详细的分析,不断地问为什么?)
问题1:要怎样做才能使STree的高度变回到h+2呢?
我们可以把里面的节点的位置改动下,以达到改变STree高度这个目的。但注意到,AVL树首先是一颗二叉查找树,里面的节点的位置是有规律的:对于树中的每个节点X,它的左子树中所有关键字值小于X的关键字值,而它的右子树中所有关键字值大于X的关键字值。因此,我们在改动STree中节点的位置时,要不破坏上面的规律。
在众多的限制下,我们只能把在中间的节点的位置调整下,显然,K2的左子树比右子树高2,所以,我们应该把K2的左儿子K1提到K2的位置。这样STree的高度就变成了K1的高度,即变回了h+2。这时,K1的右儿子是K2,K1的右子树Y变成K2的左子树,如下图:
到这里,已经分析出了怎样去旋转,而且这个做法好像真的能解决问题,但怎样才能确定这样做是正确的呢,仔细想想,这个情况还可以再细分:
情形1: 新节点是在K1的左子树X中插入,从而导致K2失去平衡(注意,K1是平衡的,不要忘记了,K2才是第一个失衡的)。
情形2:新节点是在K1的右子树Y中插入,从而导致K2失去平衡。
问题2:情形1下,插入新节点前,X、Y的高度是多少,在这样的高度下,按照上图这样旋转是不是正确的?
由题设,我们已经知道,Z的高度为h。插入节点前,以K1为根的树的高度为h+1,因此X、Y的高度最多只能是h。此时,X的高度是可以确定是h的,因为新节点是从X中插入,从而使K1的高度由h+1变成h+2,所以X在插入新节点前,高度为h。再来确定Y的高度:
如果Y的高度为h-1,则在X中插入新节点后,X的高度变成h+1,这样的话,X、Y的高度差就变成了2,首先失去平衡的节点是K1而不是K2了,这与假设相矛盾,所以Y的高度不会是h-1.
到这里已经可以确定,Y的高度也是h了,只有这样,在X中插入新节点时,才不会导致K1失衡,毕竟有个大前提在那里,K2才是第一 个失衡的节点。
现在解决了X、Y的高度,在这样的高度下,旋转后的树(图4-31右边的树),X的高度是h+1,K1的高度为h+2,K2的高度为h+1,Y和Z的高度为h。这时每个节点的高度都满足平衡性条件,所以,在情形1下,上图的旋转(这种旋转叫单旋转)是正确了,它有效地修正了STree的高度,使影响截断在STree中,从而使整颗AVL树的性质不变。
问题2:情形2下,插入新节点前,X、Y的高度是多少,在这样的高度下,按照上图这样旋转是不是正确的?
这里,X、Y高度的分析和前面是一样的,分析完后,插入前X、Y的高度也都是h。插入后,Y的高度变成了h+1,如下图4-34左边的树所示
在这种情形下,按照上面的旋转是不行的,这样,旋转后Y成为了K2的左子树。这时,K1仍然是不平衡的,所以这样的旋转是不正确的。我们要寻找另外的方法来使STree平衡。
问题3:怎样才能解决情形2所产生的问题呢?
这里的问题在于子树Y太深,单旋转没有降低它的深度。在情形1中,我们通过在K2进行单旋转来使比较高的子树K1提上去,从而解决了问题。于是,我们就想,既然单旋转可以把比较高的子树提上去,那么我们先在K1处进行一次单旋转,把Y子树提上去,这看起来是个不错的主意。既然要进行单旋转,我们就需要一个K1的右儿子K3,如下图所示:
这里,我们并不用在意子树A,B的高度了,他们的高度不会超过h,也不会少于h-1。
我们在K1处进行一次单旋转,旋转后的图片如下:
在这里,可以看到,STree还是不平衡,也许,我们还要用同样的原理来进行一次单旋转。这次,我们把K3提到K2的位置来进行多一次单旋转,结果如下图右边的树所示:
至此,旋转完毕,容易验证,在进行两次旋转后,得出的树(图4-35右边的树)是满足AVL平衡特性的,这种旋转叫双旋转。
我们现在解决了情形1和情形2这两种情况,下面给出这两种情形的准确定义,让我们把必须重新平衡的节点叫做a。
情形1:对a的左儿子的左子树进行一次插入。
情形2:对a的左儿子的右子树进行一次插入。
到这里可能会有点疑问,这些情形是否已经包括齐所有的情形了?比如情形1中,还需要对X进行拆分,继续产生情形3,4…..n吗?其实不用了,我们只要把X看成一个整体就行,我们只需要知道插入后X子树的高度为h + 1,并不用去关心插入在什么位置了。
第二种情况:从第一个平衡性被破坏的节点的右边插入,从而导致的失衡
其实第二种情况就是第一种情况的对称,只是换了一边插入而已,其推导过程和第一种情况是一样的。同样的,具有两种情形:
设a是必须重新平衡的节点,
情形3:对a的右儿子的左子树进行一次插入。
情形4:对a的右儿子的右子树进行一次插入。
在这里对4种情形进行下总结:
情形1和情形4是关于a点的镜像对称,而2和3是关于a点的镜像对称。因此,理论上只有两种情况,当然从编程的角度来看还是四种情形。
第一种情况是插入发生在“外边”的情况(即左-左的情况或右-右的情况),该情况通过对树的一次单旋转而完成调整。第二种情况是插入发生在“内部”的情形(即左-右的情况或右-左的情况),该情况通过稍微复杂些的双旋转(两次单旋转)来处理。
3.实际的例子
接来下来演示一个例子。假设从初始的空AVL树开始插入关键字3、2和1,然后依序插入4到7。在插入关键字 1时第一个问题出现了,AVL特性在根处被破坏。我们在根与其左儿子之间实施单旋转修正这个问题。下面是旋转之前和之后的两棵树:
虚线连接要旋转的两个节点,它们是旋转的主体。下面我们插入关键字为4的节点,这没有问题,但插入5破坏了在节点3处的AVL特性,而通过单旋转又将其修正。
这里需要注意的是,在编程的时候,要注意让2的右儿子变成4,否则会导致4是不可访问的。下面我们插入6。这在根节点产生一个平衡问题,因为它的左子树高度是0而右子树高度为2。因此我们在根处在2和4之间实施一次单旋转。
旋转的结果使得2是4的一个儿子而4原来的左子树变成节点2的新的右子树。我们插入的下一个关键字是7,它导致另外的旋转:
下面演示双旋转的情况:
我们现在以倒序插入关键字10到16,接着插入8,然后再插入9。插入16容易,因为它并不破坏平衡特性,但插入15就会引起节点在7处的高度不平衡。这属于情形3,需要通过一次右-左双旋转来解决,这个右-左双旋转将涉及7,16和15。
下面我们插入14,它也需要一个双旋转。此时修复该树的双旋转还是右-左双旋转,它将涉及到6、15和7。
现在插入13,那么在根处就会产生一个不平衡。由于13不在4和7之间,因此我们知道一次单旋转就能完成修正的工作。
插入12也要一个单旋转:
为了插入11,还需要进行一个单旋转,对于其后的10的插入也需要这样的旋转。我们插入8不进行旋转,这样就建立了一棵近乎理想的平衡树了。
最后,我们插入9以演示双旋转的对称情形。注意,9引起含有关键字10的节点产生不平衡。由于9在10和8之间(8是通向9的路径上的节点10的儿子),因此需要进行一个双旋转。
例子到此结束。
4.代码实现
下面是代码实现,就不详细介绍了,毕竟本文着重的是推导过程:
#include <stdio.h>
#include <stdlib.h>
struct AvlNode;//树的结点
typedef struct AvlNode *AvlPostion;//树的指针
typedef struct AvlNode *AvlTree;//树的指针
typedef int AvlElementType;//树结点中的元素
#define Max(X,Y) ((X) > (Y) ? (X) : (Y))
struct AvlNode {
AvlElementType element;
AvlTree left;//左子树
AvlTree right;//右子树
int height;//树的高度
};
void makeAvlEmpty(AvlTree tree) {
if(tree != NULL){
makeAvlEmpty(tree->left);
makeAvlEmpty(tree->right);
free(tree);
}
}
//查找关键字为x的相应的树结点,并返回
AvlPostion findAvl(AvlElementType x,AvlTree tree) {
if(tree == NULL){
return NULL;
}
if(x < tree->element) {
return findAvl(x,tree->left);
}else if(x > tree->element) {
return findAvl(x,tree->right);
}else {
return tree;
}
}
//查找最小的结点
AvlPostion findMinAvl(AvlTree tree) {
if(tree != NULL) {
while(tree->left) {
tree = tree->left;
}
}
return tree;
}
//查找最大的结点
AvlPostion findMaxAvl(AvlTree tree) {
if(tree != NULL) {
while(tree->right) {
tree = tree->right;
}
}
return tree;
}
//返回树的高度
static int height(AvlPostion p) {
if(p == NULL) {
return -1;
}else {
return p->height;
}
}
/*如果k2有一个左儿子k1,则將k1作为根返回,k2变成k1的右儿子,并更新他们的高度*/
static AvlPostion singleRotateWithLeft(AvlPostion k2) {
AvlPostion k1 = k2->left;
k2->left = k1->right;
k1->right = k2;
k2->height = Max(height(k2->left),height(k2->right)) + 1;
k1->height = Max(height(k1->left),k2->height) + 1;
return k1;
}
/*如果k2有一个右儿子k1,则將k1作为根返回,k2变成k1的左儿子,并更新他们的高度*/
static AvlPostion singleRotateWithRight(AvlPostion k2) {
AvlPostion k1 = k2->right;
k2->right = k1->left;
k1->left = k2;
k2->height = Max(height(k2->left),height(k2->right)) + 1;
k1->height = Max(k2->height,height(k1->right)) + 1;
return k1;
}
static AvlPostion doubleRotateWithLeft(AvlPostion k3) {
k3->left = singleRotateWithRight(k3->left);
return singleRotateWithLeft(k3);
}
static AvlPostion doubleRotateWithRight(AvlPostion k3) {
k3->right = singleRotateWithLeft(k3->right);
return singleRotateWithRight(k3);
}
//最关键的插入方法
AvlTree insertAvl(AvlElementType x,AvlTree tree) {
if(tree == NULL) {
tree = (AvlPostion)malloc(sizeof(struct AvlNode));
if(tree == NULL) {
printf("create tree fail!\n");
}else {
tree->element = x;
tree->left = NULL;
tree->right = NULL;
tree->height = 0;
}
}else if(x < tree->element) {
//如果出来平衡性被破坏的情况,那么一定是左子树的高度更高
tree->left = insertAvl(x,tree->left);//让下次调用来更新tree->left;
if(height(tree->left) - height(tree->right) == 2) {
if(x < tree->left->element) {//是在tree的左儿子的左子树中插入的
tree = singleRotateWithLeft(tree);
} else {//在tree的左儿子的右子树中插入的
tree = doubleRotateWithLeft(tree);
}
}
}else if(x > tree->element) {
tree->right = insertAvl(x,tree->right);
if(height(tree->right) - height(tree->left) == 2) {
if(x > tree->right->element) {
tree = singleRotateWithRight(tree);
}else {
tree = doubleRotateWithRight(tree);
}
}
}//如果有相等的节点,就什么也不做
//更新节点的高度信息
tree->height = Max(height(tree->left),height(tree->right)) + 1;
return tree;//实现把上一次调用的节点更新的目的
}
//中序遍历
void inorderTraversal(AvlTree tree) {
if(tree != NULL) {
inorderTraversal(tree->left);
printf("%d ",tree->element);
inorderTraversal(tree->right);
}
}
int main() {
int n;
printf("请输入结点数:\n");
if(scanf("%d",&n) == 1) {
int x;
AvlTree tree = NULL;
for(int i = 0; i < n; i++) {
if(scanf("%d",&x) == 1) {
tree = insertAvl(x,tree);
}
}
inorderTraversal(tree);
}
printf("\n");
return 0;
}
至此,整篇文章就完了!