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

本文从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;
}

至此,整篇文章就完了!

时间: 2024-11-05 20:39:12

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

AVL树原理及实现(C语言实现以及Java语言实现)

欢迎探讨,如有错误敬请指正 如需转载,请注明出处http://www.cnblogs.com/nullzx/ 1. AVL定义 AVL树是一种改进版的搜索二叉树.对于一般的搜索二叉树而言,如果数据恰好是按照从小到大的顺序或者从大到小的顺序插入的,那么搜索二叉树就对退化成链表,这个时候查找,插入和删除的时间都会上升到O(n),而这对于海量数据而言,是我们无法忍受的.即使是一颗由完全随机的数据构造成的搜索二叉树,从统计角度去分析,在进行若甘次的插入和删除操作,这个搜索二叉树的高度也不能令人满意.这个

AVL树的初步生成与插入操作

平衡二叉树(Balanced Binary Tree)又被称为AVL树(有别于AVL算法),且具有以下性质:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树.构造与调整方法 平衡二叉树的常用算法有红黑树.AVL.Treap等. 最小二叉平衡树的节点的公式如下 F(n)=F(n-1)+F(n-2)+1 这个类似于一个递归的数列,可以参考Fibonacci数列,1是根节点,F(n-1)是左子树的节点数量,F(n-2)是右子树的节点数量. AVL是最先发明的

平衡二叉搜索树(AVL树)的原理及实现源代码(有图文详解和C++、Java实现代码)

一.AVL树(平衡二叉搜索树)是什么? AVL树是根据它的发明者G.M. Adelson-Velsky和E.M. Landis命名的.AVL树本质上还是一棵二叉搜索树,它的特点是: 1.本身首先是一棵二叉搜索树. 2.带有平衡条件:每个非叶子结点的左右子树的高度之差的绝对值(平衡因子)最多为1. 例如: 5             5 / \            /  \ 2   6         2   6 / \    \         / \ 1  4   7       1  4

AVL树学习(平衡二叉树)

一.基本概念 AVL树既是平衡二叉树.AVL树的定义首先要求该树是二叉查找树(满足排序规则),并在此基础上增加了每个节点的平衡因子的定义,一个节点的平衡因子是该节点的左子树树高减去右子树树高的值. =========================================================================== 1. 二叉查找树:又称二叉排序树/二叉搜索树,具有以下性质: (1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值: (2)若右子树不空,则

[转载]AVL树(一)之 图文解析 和 C语言的实现

概要 本章介绍AVL树.和前面介绍"二叉查找树"的流程一样,本章先对AVL树的理论知识进行简单介绍,然后给出C语言的实现.本篇实现的二叉查找树是C语言版的,后面章节再分别给出C++和Java版本的实现. 建议:若您对"二叉查找树"不熟悉,建议先学完"二叉查找树"再来学习AVL树. 目录 1. AVL树的介绍 2. AVL树的C实现3. AVL树的C实现(完整源码) 4. AVL树的C测试程序 转载请注明出处:http://www.cnblogs.

AVL树的插入操作(旋转)图解

=================================================================== AVL树的概念 在说AVL树的概念之前,我们需要清楚二茬搜索树的概念.对于二叉搜索树,我们知道它可以降低查找速率,但是如果一个二叉搜索树退化成一棵只剩单支的搜索树,此时的查找速率就相当于顺序表中查找元素,效率变低,时间复杂度由原来的O(logN)变为O(N). 此时就有了AVL(高度平衡二叉搜索树),从它的名字就能知道它也是一棵二叉搜索树,只是它在插入元素的时候

数据结构(三)实现AVL树

AVL树的定义 一种自平衡二叉查找树,中面向内存的数据结构. 二叉搜索树T为AVL树的满足条件为: T是空树 T若不是空树,则TL.TR都是AVL树,且|HL-HR| <= 1 (节点的左子树高度与节点的右子树高度差的绝对值小于等于1) 说明 AVL树的实现类为AVLTree继承自前篇中的二叉搜索树BTreeSort ,AVL树的节点类为AVLNode继承自二叉树节点类BTreeNode. 实现代码 AVL树节点定义 1  public class AVLNode extends BTreeNo

C++模板实现的AVL树

1 AVL树的定义 AVL树是一种自平衡二叉排序树,它的特点是任何一个节点的左子树高度和右子树的高度差在-1,0,1三者之间.AVL树的任何一个子树都是AVL树. 2 AVL树的实现 AVL树本质是一种二叉排序树,所以二叉排序树的任何性质AVL树都具有,但是AVL树稍微复杂的地方就是AVL树必须满足平衡条件,具体跟BST不同的地方主要体现在插入,删除操作. 插入操作:当插入之后可能会出现不平衡,所以这时候要通过旋转树来实现平衡.旋转有四种类型,左左,左右,右左,右右.其中左左旋转和右右旋转是镜像

红黑树和AVL树的比较

1. 红黑树并不追求"完全平衡"--它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能. 红黑树能够以O(log2 n) 的时间复杂度进行搜索.插入.删除操作.此外,由于它的设计,任何不平衡都会在三次旋转之内解决.当然,还有一些更好的,但实现起来更复杂的数据结构,能够做到一步旋转之内达到平衡,但红黑树能够给我们一个比较"便宜"的解决方案.红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高. 当然,红黑树并不适应所有应用树的领域.如果数据基本上是