在上一篇博文中我们提到了,如果对普通二叉查找树进行随机的插入、删除,很可能导致树的严重不平衡
所以这一次,我们就来介绍一种最老的、可以实现左右子树“平衡效果”的树(或者说算法),即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