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

至此,整篇文章就完了!

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

  1. 平衡二叉树-AVL树(LL、RR、LR、RL旋转)

    平衡二叉树的定义: 任意的左右子树高度差的绝对值不超过1,将这样的二叉树称为平衡二叉树,二叉平衡树前提是一个二叉排序树. 平衡二叉树的插入: 二叉平衡树在插入或删除一个结点时,先检查该操作是否导致了树 ...

  2. DSA 经典数据结构与算法 学习心得和知识总结(四) | AVL树

    AVL树 从BST的角度看AVL AVL的定义及性质 AVL树的结构定义 AVL树的旋转算法 左左情况---右旋 右右情况---左旋 左右情况---左右旋 右左情况---右左旋 AVL树的遍历操作 A ...

  3. B树、B+树、AVL树、红黑树

    from: http://blog.csdn.net/chlele0105/article/details/8473846 binary search tree,中文翻译为二叉搜索树.二叉查找树或者二 ...

  4. AVL树(二)之 C++的实现

    AVL树(二)之 C++的实现 概要 上一章通过C语言实现了AVL树,本章将介绍AVL树的C++版本,算法与C语言版本的一样. 目录 1. AVL树的介绍 2. AVL树的C++实现 转载请注明出处: ...

  5. AVL树(一)之 C语言的实现

    概要 本章介绍AVL树.和前面介绍"二叉查找树"的流程一样,本章先对AVL树的理论知识进行简单介绍,然后给出C语言的实现.本篇实现的二叉查找树是C语言版的,后面章节再分别给出C++ ...

  6. 数据结构-----AVL树的旋转操作

    本文主要讲解AVL的旋转操作,供自己复习用,如有不对之处请指出.另外图片是从链接处的大神那复制的,感觉文章写的很好,可以去学习. http://www.cnblogs.com/QG-whz/p/516 ...

  7. 二叉树第i层中的所有结点_讲透学烂二叉树(二):图中树的定义amp;各类型树的特征分析...

    日常中我们见到的二叉树应用有,Java集合中的TreeSet和TreeMap,C++ STL中的set.map,以及Linux虚拟内存的管理,以及B-Tree,B+-Tree在文件系统,都是通过红黑树 ...

  8. 详解 二叉搜索树-----AVL树

    二叉搜索树 根结点比左子树中所有结点都大 根结点比右子树所有结点都小 最小的元素在最左侧 最大的元素在最右侧 中序遍历有序 具有以上的特征的二叉树就是二叉搜索树也叫二叉排序数 二叉搜索树的操作 查找 ...

  9. C++实践笔记(四)----AVL树的简单实现

    关于AVL树的分析,请见:数据结构与算法分析学习笔记(二)--AVL树的算法思路整理 这里给出AVL树的结构定义以及insert,remove和print三种操作的例程:   1 #include & ...

最新文章

  1. SQL优化常用方法36
  2. 分布式服务限流实战,已经为你排好坑了
  3. 内卷时代,互联网人相亲有多难?|漫画
  4. SpriteBuilder实际操作中如何确定合适Breaking force的值
  5. c++ 利用boost 实现文件操作
  6. CNN结构:用于检测的CNN结构进化-结合式方法
  7. 动态规划 POJ 1088 滑雪
  8. linux md5sum命令
  9. 如何搜mac_今日头条号权重怎么查?如何提高头条号权重?看完这篇你就懂了
  10. Linux文档备份工具,推荐8款免费Linux备份工具
  11. 安利4款良心的时间轴软件,建议收藏!
  12. Pytorch中文视频教程,Pytorch实战视频教程
  13. 努比亚 Z17 mini s (Nubia NX589J) 解锁BootLoader 并刷入recovery ROOT
  14. RabbitMq七种工作模式,结合简单的java实例使用,答应我不要再说你不会RabbitMq了,好吗,宝贝?
  15. 为什么人需要一个人静静--《孤独力》的读后感
  16. 《如何高效学习》总结
  17. 【微观】消费者均衡、正常品的替代效应与收入效应
  18. 超声波脉冲信号发生器设计
  19. PTA-实验五-圆形体体积计算器
  20. VR/AR产品:VR火灾隐患排查系统再次升级,多种场景任你选择!

热门文章

  1. git-flow 工作流程简介
  2. python3 str bytes bytearray 互相转换
  3. linux 内核编译错误 error: conflicting types for ‘syscall_trace_enter’
  4. Android--相机预览及拍照临时文件/SurfaceView
  5. python不支持prelu_python实现并绘制 sigmoid函数,tanh函数,ReLU函数,PReLU函数
  6. 学生计算机屏幕坏了怎么办,如果计算机显示器的屏幕坏了怎么办?
  7. Linux进程虚拟地址空间
  8. matlab gui uiwaitbar,MATLAB GUI嵌入进度条(waitBar) | 学步园
  9. java 共享锁 独占锁_Java并发编程锁之独占公平锁与非公平锁比较
  10. 湖北大学校长计算机考研复试分数线,湖北大学考研复试分数线