从AVL树的定义出发,一步步推导出旋转的方案。

Posted TimeShatter

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从AVL树的定义出发,一步步推导出旋转的方案。相关的知识,希望对你有一定的参考价值。

本文从AVL树的定义出发,一步步地推导出AVL树旋转的方案,这个推导是在已经清楚地知道AVL树的定义这个前提下进行的。文章注重思考的过程,并不会直接给出AVL树是怎样旋转的,用来提醒自己以后在学习的时候要注重推导的过程。在此,我要特别感谢下我的数据结构老师,是他让我意识到思考的重要性。

一、从AVL树的定义开始

1. 二叉查找树的问题

二叉查找树的出现,虽然使查找的平均时间降到了logN,但是,在多次删除或者插入操作后,可能会出现根节点的左子树比右子树高很多,或者右子树比左子树高很多的情况。如图所示:
失去平衡的二叉查找树
这样的话,查找的效率会相当低。所以现在的问题是:要想出一种解决方案,可以使得二叉查找树能保持平衡。这时,我们要定义一种新的二叉查找树,这种二叉查找树是一种带有平衡条件的二叉查找树,也就是AVL树。

2. AVL树的定义

一棵AVL树是其每个节点的左子树和右子树的高度最多差1的二叉查找树(空树的高度定义为-1)。在下图中,左边的树是AVL树,但右边的树不是。
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左边的树所示
单旋转不能修复情形2
在这种情形下,按照上面的旋转是不行的,这样,旋转后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;
}

至此,整篇文章就完了!

以上是关于从AVL树的定义出发,一步步推导出旋转的方案。的主要内容,如果未能解决你的问题,请参考以下文章

C++AVL树的实现--详细解析旋转细节

AVL树的插入与删除

C++AVL树(四种旋转方式)

C++AVL树(四种旋转方式)

C++AVL树(四种旋转方式)

AVL树