在上一篇博文中我们提到了,如果对普通二叉查找树进行随机的插入、删除,很可能导致树的严重不平衡

  

  所以这一次,我们就来介绍一种最老的、可以实现左右子树“平衡效果”的树(或者说算法),即AVL树。其名字与其发明者有关,这种数据结构的发明者为Adelson-Velskii和Landis,所以这种树或者说这种算法就叫AVL树。

  那么,AVL树如何实现“平衡”呢?

  首先我们来想一想,除了肉眼观察外,如何看出一棵树的“平衡程度”?我们知道任一结点都有两个属性:高度和深度,很显然,这两个属性可以反映出树的平衡程度。比如一个结点的左子树中深度最大的结点深为3,而右子树深度最大的结点深度为10,那么该结点的左右子树显然是严重不平衡的。类似的方法,高度也可以反映出平衡程度。

  那么,高度和深度,AVL树选择用哪个作为“度量”呢?AVL树选择的是高度。原因很简单,即使每个结点都准确地存储了自身的高度和深度,我们也无法通过查看结点左孩子和右孩子的深度来快速的判断出结点的左右子树是否不平衡,因为结点的左孩子和右孩子深度一定是一样的(若都存在的话)。但是我们可以直接通过结点左孩子和右孩子的高度来判断出结点的左右子树的平衡程度。

  至此,我们可以给出结点的定义了,其与普通二叉查找树的唯一区别就是多了一个高度信息height

struct treeNode {
int data;
int height;
int frequency;
struct treeNode *left;
struct treeNode *right;
};
typedef struct treeNode *AVLtree;

  接下来我们要做的就是给不平衡下一个定义:什么情况下我们认为不平衡?假设我们要求根结点的左孩子和右孩子高度必须一样,这种想法可以实现,但不完美,因为只在根结点保持平衡依然不够。比如下图中的树,其本质上已经与链表一样了。

  

  那么,我们可以要求所有结点的左孩子和右孩子高度一致吗?这个实现基本不可能,因为要想满足这一点,二叉树必须是满二叉树,结点个数只要少一个,就没有达到要求(下图为满二叉树,自己假设一下如果结点个数少一个或者多一个,是否还能满足此苛刻条件)

  所以在AVL树中,对于不平衡的定义是任一结点的左孩子和右孩子高度差大于1。换句话说,我们在AVL树中要实现的效果就是任一结点的左孩子和右孩子高度差不大于1,而这一点是可以实现的!(如果没有某个孩子,则我们认为“那儿”的高度为-1,换句话说我们认为空树的高度为-1。这样的设定恰好符合我们的期望)

  下图中,左侧的树符合AVL树条件,右侧的不符合,不平衡处为根结点7(左孩子2的高度为2,右孩子8的高度为0,两者高度差大于1)

  如下图,也是一棵AVL树

  接下来我们看看AVL树如何实现这一点(任一结点左右孩子或者说左右子树高度差不超过1)。我们知道,对于二叉查找树,一般的操作有三种:查询、增加、删除(修改也是有可能的,但其与查找的性质相似)。而这些操作中,可能使原先平衡的树变得不平衡的只有两种:增加和删除。我们以增加作为切入点,看看AVL树对增加操作进行了怎样的改进,使其可以不使得树变得不平衡。

  现在,假设我们有如下一棵二叉树,显然它符合AVL树的条件。

  然后我们按照普通的二叉树插入的方式插入结点3,其将变成这样

  我举这个例子想说明什么呢?那就是,当插入一个结点时,只有从插入点到根结点的路径上的结点的平衡程度可能被改变,因为只有它们的子树发生了变化。比如这个例子中,插入3以后结点4,5,2和根6的平衡程度被改变了,其中变得不平衡了的有结点5,2和根6。

  此外,我们还可以给出一个结论:我们只需要对从插入结点开始到根结点的这条路径上遇到的第一个不平衡结点进行一定操作,就可以使整棵树变回AVL树。比如这个例子中不平衡结点有5,2,6。我们遇到的第一个不平衡结点是5,我们只需要对它进行如下操作,就可以令树变回AVL树

  

  这类操作(对从下向上遇到的第一个不平衡结点进行的操作)我们称之为“旋转”,尽管我个人不是很喜欢这个说法╮(╯_╰)╭

  接下来就是学习如何进行“旋转”了。而要想知道如何旋转,我们必须得先分析插入有哪些可能,因为对于不同的插入形式,我们将会有不同的旋转方式。

  我们假设需要进行旋转操作的结点为a(即从下向上遇到的第一个不平衡结点),那么a的左右孩子高度一定相差2,也就是说,插入操作一定是下面四种情形之一:

  1.插入结点在a的左孩子的左子树中(我们简记为左左)

  2.插入结点在a的左孩子的右子树中(我们简记为左右)

  3.插入结点在a的右孩子的左子树中(我们简记为右左)

  4.插入结点在a的右孩子的右子树中(我们简记为右右)

  理论上来说,左左和右右对于a来说是镜像对称的,左右和右左对于a来说也是镜像对称的,但是在代码实现上它们依然是不同的情形,所以我们依然需要区别对待。

  首先,我们可以给出的结论是:对于左左和右右,我们均对a进行“单旋转”,而对于左右和右左,我们将对a进行“双旋转”(本质就是两次单旋转)。单旋转是更容易理解与实现的,我们先来说说单旋转。

  对于“左左”的情形,我们进行的是“左单旋转”,下图中需要进行旋转操作的就是结点k2,我们先通过图来看看单旋转对树造成了怎样的变化,之后再来说代码。

  

  显然,新结点插入到了k2的左孩子k1的左子树X中,并导致了子树X高度的提升、k2的不平衡,那么我们进行了怎样的“单旋转”呢?其实很简单,就是令k2成为其左孩子k1的右孩子,并让k1的原右子树变为k2的左子树。这样做可以使得子树X“上移一层”,而子树Z“下移一层”,从而整棵树再次符合AVL树的要求。

  知道了左左情形如何处理后(令k2成为其左孩子k1的右孩子,并让k1的原右子树变为k2的左子树),我们就可以写出对应左左情形的“左单旋转”代码了:

int Max(int a, int b)
{
return (a > b) ? a : b;
} //返回结点height
int Height(struct treeNode *t)
{
if (t == NULL)
return -;
else
return t->height;
} //左单旋转,令oldRoot的左孩子替代oldRoot的位置,oldRoot的左孩子改为oldRoot原左孩子的右孩子
AVLtree SingleRotateWithLeft(AVLtree oldRoot)
{
//newRoot即k1,oldRoot即k2
AVLtree newRoot = oldRoot->left;
oldRoot->left = newRoot->right;
newRoot->right = oldRoot;
//记得更新旋转后新旧根的高度信息
oldRoot->height = Max(Height(oldRoot->left), Height(oldRoot->right)) + ;
newRoot->height = Max(Height(newRoot->left), oldRoot->height) + ; return newRoot;
}

  本文最开始举的例子就是一次左左插入情形,k1对应结点4,k2对应结点5

  

  对于左左插入,我们可以再给出一个例子,下例中k2对应8,k1对应7,插入结点为6

  

  右右插入其实就是左左插入关于不平衡结点k1的镜像对称,所以理解上应该是类似的,只不过代码实现需要有所更改

  对于如下右右插入(旋转前k1为根,k2为k1右孩子),我们只需要令k1成为其左孩子k2的左孩子,并令k2的原左子树Y成为k1的右子树即可。

  代码与左左插入是类似的:

//右单旋转,令oldRoot的右孩子替代oldRoot的位置,oldRoot的右孩子改为oldRoot原右孩子的左孩子
AVLtree SingleRotateWithRight(AVLtree oldRoot)
{
AVLtree newRoot = oldRoot->right;
oldRoot->right = newRoot->left;
newRoot->left = oldRoot;
//记得更新旋转后新旧根的高度信息
oldRoot->height = Max(Height(oldRoot->left), Height(oldRoot->right)) + ;
newRoot->height = Max(Height(newRoot->left), oldRoot->height) + ; return newRoot;
}

  右右插入的一个例子,下图中出现不平衡的“第一个”结点就是根2,我们对根2进行右单旋转

  但是,不论是“左单旋转”还是“右单旋转”,都不能解决左右插入和右左插入的情况。比如对于左右插入,我们应用左单旋转将是这样的

  

  经过左单旋转后,子树X“上移一层”,子树Z“下移一层”,但真正引起不平衡问题的子树Y却“没动”。所以对于左右插入和右左插入(因为右左插入是左右插入的镜像对称,我们可以类似的推导结论),我们需要对不平衡结点使用一种称为“双旋转”的操作。对于左右插入情形的,我们称为左双旋转,对于右左插入情形的,我们称之为右双旋转。

  下面是左双旋转解决左右插入情形的示意图

  

  对比上图,可以看出我们将子树Y进行了“详细化”,将其视为了结点k2和左右子树B、C,那么,双旋转到底是怎么个操作呢?虽然双旋转看上去很复杂,但其实它本质上就是“两次单旋转”,第一次是对k1进行的右单旋转,第二次则是对k3进行左单旋转

  

  明白了左双旋转实际进行的操作后,我们就可以给出左双旋转的代码了:

//左双旋转,先令oldRoot(上图中的k3)的左孩子进行右单旋转,再令oldRoot进行左单旋转
AVLtree DoubleRotateWithLeft(AVLtree oldRoot)
{
oldRoot->left = SingleRotateWithRight(oldRoot->left);
return SingleRotateWithLeft(oldRoot);
}

  而右双旋转的示意图如下

  

  右双旋转本质就是令k1的右孩子k3进行一次左单旋转,然后令k1进行一次右单旋转

//右双旋转,先令oldRoot的右孩子进行左单旋转,再令oldRoot进行右单旋转
AVLtree DoubleRotateWithRight(AVLtree oldRoot)
{
oldRoot->right = SingleRotateWithLeft(oldRoot->right);
return SingleRotateWithRight(oldRoot);
}

  

  至此,AVL树的插入操作可以说已经讲完了,接下来我们只需要写一个Insert函数,然后在其中判断插入结点的“去向”,即插入情形,接着选择对应的旋转方式和旋转结点即可:

//向AVL树插入数据
AVLtree InsertToAVL(AVLtree t, int data)
{
//若t为NULL,则创建新结点,于函数最后返回新结点
if (t == NULL)
{
t = (AVLtree)malloc(sizeof(struct treeNode));
t->frequency = ;
t->data = data;
t->height = ;
t->left = t->right = NULL;
}
//若插入数据小于当前结点数据
else if (data < t->data)
{
//将数据插入到t的左孩子,此时可能导致t的左孩子高度比右孩子高2,需要看情况选择旋转方式
t->left = InsertToAVL(t->left, data);
if (Height(t->left) - Height(t->right) == )
{
if (data < t->left->data)
t = SingleRotateWithLeft(t);
else
t = DoubleRotateWithLeft(t);
}
else
t->height = Max(Height(t->right), Height(t->left)) + ; //即使左右子树高度差不为2,也有可能需要更新当前结点高度
}
//若插入数据大于当前结点数据
else if (data > t->data)
{
//将数据插入到t的右子树,此时可能导致t的右孩子高度比左孩子高2,需要看情况选择旋转方式
t->right = InsertToAVL(t->right, data);
if (Height(t->right) - Height(t->left) == )
{
if (data > t->right->data)
t = SingleRotateWithRight(t);
else
t = DoubleRotateWithRight(t);
}
else
t->height = Max(Height(t->right), Height(t->left)) + ; //即使左右子树高度差不为2,也有可能需要更新当前结点高度
}
//若数据已存在,则递增结点的frequency
else
t->frequency++; return t;
}

  说完了插入操作后,我们来看看同样可以导致不平衡的删除操作该怎么办。其实我们在结点定义时既然给定了frequency,那么懒惰删除其实是最简单方便的删除方式。但如果就这样跳过删除操作,未免令人遗憾。所以我还是简单说说删除操作该如何实现。

  首先,AVL是一种特殊的二叉查找树,所以AVL树的删除操作与普通二叉查找树的删除操作是有部分相同的,比如若被删除结点没有孩子或有一个孩子则直接释放,否则需要将被删除结点“替换”到右子树的最小结点。不同之处在于删除之后,该如何再次平衡树?

  详细的解析删除后平衡树的方法会很长、很麻烦,所以我在这里直接给出删除后将树平衡的核心思想:若我们删除了结点a左子树中的某结点,导致a左子树的高度降低,那么从树的平衡角度来说,其效果等同于向a的右子树插入一个结点并使得a右子树高度增加。

  所以,当删除a左子树结点并导致结点a不平衡时,我们可以假设是我们向a的右子树插入了结点导致的不平衡,然后采用对应的旋转方式,而办法就是比较a->right->left和a->right->right的高度,若a->right->left的高度更大,则我们假设是对a进行了右左插入导致的不平衡,反之我们假设是对a进行了右右插入导致的不平衡。

//删除AVL树中的结点
AVLtree DeleteNode(AVLtree t, int data)
{
if (t == NULL)
return t;
//若给定数据小于当前结点,则前往左子树删除目标结点
//若删除成功,则只可能出现左子树高度低于、等于右子树,对于低于右子树高度2的情况,我们根据右子树状态决定对当前结点的旋转
//若删除操作后,左子树高度等于右子树(说明原来左子树高于右子树),我们依然要更新当前结点高度,这个操作在函数末尾进行
if (data < t->data)
{
t->left = DeleteNode(t->left, data);
if (Height(t->right) - Height(t->left) == )
{
if (Height(t->right->left) > Height(t->right->right))
t = DoubleRotateWithRight(t);
else
t = SingleRotateWithRight(t);
}
}
//若给定数据大于当前结点,则前往右子树删除目标结点
//若删除成功,则只可能出现右子树高度低于、等于左子树,对于低于左子树高度2的情况,我们根据左子树状态决定对当前结点的旋转
//若删除操作后,右子树高度等于左子树(说明原来右子树高于左子树),我们依然要更新当前结点高度,这个操作在函数末尾进行
else if (data > t->data)
{
t->right = DeleteNode(t->right, data);
if (Height(t->left) - Height(t->right) == )
{
if (Height(t->right->left) > Height(t->right->right))
t = SingleRotateWithLeft(t);
else
t = DoubleRotateWithLeft(t);
}
}
//若当前结点即需要删除的结点,且有两个孩子,则我们删除其右子树中的最小结点并将当前结点数据修改为该最小结点数据
//删除后的操作同上
else if (t->left && t->right && t->frequency == )
{
int minData;
t->right = DeleteMin(t->right, &minData);
t->data = minData;
if (Height(t->left) - Height(t->right) == )
{
if (Height(t->right->left) > Height(t->right->right))
t = SingleRotateWithLeft(t);
else
t = DoubleRotateWithLeft(t);
}
}
//若当前结点需要删除且只有一个孩子或没有孩子,则返回其唯一孩子(也可能是NULL)并释放当前结点
else if (t->frequency == )
{
AVLtree temp = NULL;
temp = (t->left) ? t->left : t->right;
free(t);
return temp;
}
else
t->frequency--;
t->height = Max(Height(t->right), Height(t->left)) + ;
return t;
}

  上面用到的DeleteMin函数如下:

//用于删除子树中的最小结点,需保证所给t不为NULL
AVLtree DeleteMin(AVLtree t,int *pMinData)
{
//若当前结点没有左孩子则必为最小结点,我们将其数据保存于pMinData,然后将其释放并返回其右孩子
if (t->left == NULL)
{
(*pMinData) = t->data;
AVLtree temp = t->right;
free(t);
return temp;
}
//若当前结点不是最小结点,则我们继续前往左子树寻找并删除最小结点
//左子树删除了结点所以只可能出现左子树高度低于、等于右子树的情况
//若左子树高度低于右子树2,此时我们根据右子树的状态决定对当前结点进行何种旋转
else
{
t->left = DeleteMin(t->left,pMinData);
if (Height(t->right) - Height(t->left) == )
{
if (Height(t->right->left) > Height(t->right->right))
t = DoubleRotateWithRight(t);
else
t = SingleRotateWithRight(t);
}
//若删除操作后,左子树高度等于右子树(说明原来左子树高于右子树),我们依然要更新当前结点高度
else
t->height = Max(Height(t->left), Height(t->right)) + ;
}
return t;
}

  至此,对于AVL树的讨论可以说结束了。下面的链接是一个简单的示例程序,简单对比了顺序数据插入到AVL树和普通二叉查找树后两者分别有怎样的高度,然后对AVL树进行了一步步的删除操作和单次删除操作后树的先序遍历结果。

https://github.com/nchuXieWei/ForBlog------AVLtree

深入浅出数据结构C语言版(12)——平衡二叉查找树之AVL树的更多相关文章

  1. 算法学习 - 平衡二叉查找树实现(AVL树)

    平衡二叉查找树 平衡二叉查找树是非常早出现的平衡树,由于全部子树的高度差不超过1,所以操作平均为O(logN). 平衡二叉查找树和BS树非常像,插入和删除操作也基本一样.可是每一个节点多了一个高度的信 ...

  2. 深入浅出数据结构C语言版(7)——特殊的表:队列与栈

    从深入浅出数据结构(4)到(6),我们分别讨论了什么是表.什么是链表.为什么用链表以及如何用数组模拟链表(游标数组),而现在,我们要进入到对线性表(特意加了"线性"二字是因为存在多 ...

  3. 深入浅出数据结构C语言版(8)——后缀表达式、栈与四则运算计算器

    在深入浅出数据结构(7)的末尾,我们提到了栈可以用于实现计算器,并且我们给出了存储表达式的数据结构(结构体及该结构体组成的数组),如下: //SIZE用于多个场合,如栈的大小.表达式数组的大小 #de ...

  4. 深入浅出数据结构C语言版(4)——表与链表

    在我们谈论本文具体内容之前,我们首先要说明一些事情.在现实生活中我们所说的"表"往往是二维的,比如课程表,就有行和列,成绩表也是有行和列.但是在数据结构,或者说我们本文讨论的范围内 ...

  5. 深入浅出数据结构C语言版(10)——树的简介

    到目前为止,我们一直在谈论的数据结构都是"线性结构",不论是普通链表.栈还是队列,其中的每个元素(除了第一个和最后一个)都只有一个前驱(排在前面的元素)和一个后继(排在后面的元素) ...

  6. 深入浅出数据结构C语言版(12)——从二分查找到二叉树

    在很多有关数据结构和算法的书籍或文章中,作者往往是介绍完了什么是树后就直入主题的谈什么是二叉树balabala的.但我今天决定不按这个套路来.我个人觉得,一个东西或者说一种技术存在总该有一定的道理,不 ...

  7. 深入浅出数据结构C语言版(5)——链表的操作

    上一次我们从什么是表一直讲到了链表该怎么实现的想法上:http://www.cnblogs.com/mm93/p/6574912.html 而这一次我们就要实现所说的承诺,即实现链表应有的操作(至于游 ...

  8. 深入浅出数据结构C语言版(15)——优先队列(堆)

    在普通队列中,元素出队的顺序是由元素入队时间决定的,也就是谁先入队,谁先出队.但是有时候我们希望有这样的一个队列:谁先入队不重要,重要的是谁的"优先级高",优先级越高越先出队.这样 ...

  9. 深入浅出数据结构C语言版(20)——快速排序

    正如上一篇博文所说,今天我们来讨论一下所谓的"高级排序"--快速排序.首先声明,快速排序是一个典型而又"简单"的分治的递归算法. 递归的威力我们在介绍插入排序时 ...

随机推荐

  1. tomcat apache 实现负载平衡的小demo

    软件:1个apache,2个tomcat module包:mod_jk.so(下载地址:http://tomcat.apache.org/download-connectors.cgi) 下载文件解压 ...

  2. MOOCULUS微积分-2: 数列与级数学习笔记 6. Power series

    此课程(MOOCULUS-2 "Sequences and Series")由Ohio State University于2014年在Coursera平台讲授. PDF格式教材下载 ...

  3. 免费WebService服务

    国内手机号码归属地查询:http://webservice.webxml.com.cn/WebServices/MobileCodeWS.asmx 中国股票及时行情数据:http://webservi ...

  4. hdu1853 km算法

    //hdu1853 #include<stdio.h> #include<string.h> #define INF 99999999 ][],pr[],pl[],visr[] ...

  5. hdu 3018

    欧拉回路的题: 主要利用的是并查集,为了节省时间,压缩了它的路径: 代码: #include<cstdio> #include<cstring> #define maxn 10 ...

  6. Tree2cycle

    Problem Description A tree with N nodes and N-1 edges is given. To connect or disconnect one edge, w ...

  7. CRM客户关系管理系统(一)

    第一章.CRM介绍和开发流程 1.1.CRM简介 客户关系管理(CRM) 客户关系管理(customer relationship management)的定义是:企业为提高核心竞争力,利用相应的信息 ...

  8. Saltstack_使用指南04_数据系统-Grains

    1. 主机规划 Grains文档 https://docs.saltstack.com/en/latest/topics/grains/index.html 注意事项 修改了master或者minio ...

  9. [PHP]算法-拼接最小字典序的实现

    拼接最小字典序: 给定一个字符串类型的数组strs,请找到一种拼接顺序,使得将所有字符串拼接起来组成的大字符串是所有可能性中字典顺序最小的并放回这个大字符串. 思路: 1.字典序,12345这五个数, ...

  10. 深入理解Java虚拟机04--类结构文件

    一.程序存储格式 统一的程序存储格式:不同平台的虚拟机于所有平台都统一使用程序存储格式——字节码(ByteCode); Java 虚拟机不关心 Class 文件的来源,而只和“Class文件" ...