最近啃了一下排序算法中的平衡二叉树(AVL树),经过一晚上苦逼种树之后,总算是对平衡二叉树的插入与调整算法有了一定认识。(本文暂时没有关于删除操作的内容,至于为什么,因为我看的那本书上没讲。。。)
关于平衡二叉树的定义,可以参考以下文章:
在感受了左旋/右旋操作的魅力之后,我开始尝试着自己编写一个平衡二叉树的初始化与插入算法。定义节点结构如下:
typedef struct Node
{
int data; //数据域
int bf; //平衡因子 balance factor
struct Node* lchild, * rchild;
} Node, * TreeNode;
根据平衡二叉树的定义可知,平衡因子bf的取值只能是-1,0, 1三者之一,如果出现了|bf|>1的情况,则说明树的结构出现了不平衡,需要进行调整。但这种调整并不一定是针对整棵树,只需要对最小不平衡子树进行调整即可。
所谓的最小不平衡子树,是指“离插入位置最近,且满足|bf|>1的节点作为根节点的树”,从数值角度来理解,也就是找到整棵树中唯一一个自身的|bf|>1,且左右子节点(如果存在的话)的|bf|=1/0的节点作为根节点。
对于最小不平衡子树的查找,应该放在什么时候呢?我最初的想法是,等当前的插入过程全部结束,即递归返回根节点之后,再从根节点开始,对整棵树进行搜索,但是经过思考我便发现,最小不平衡子树的根节点,必然会落在之前插入过程中所访问过的某个节点上!也就是说,在插入成功进行递归返回时,便可以顺道寻找满足上述条件的节点。于是我构想了以下这样的算法:
enum Status { NO, YES };
int AVL_insert(TreeNode *T, int value)
{
if (value > (*T)->data) //向右子树中进行查找或插入
{
if ((*T)->rchild == NULL)
{
TreeNode new_node = (TreeNode)malloc(sizeof(Node));
new_node->lchild = new_node->rchild = NULL;
new_node->data = value;
new_node->bf = 0;
(*T)->rchild = new_node;
if ((*T)->lchild == NULL)
{
(*T)->bf -= 1;
return YES;
}
else
return NO;
}
else
{
if (AVL_insert(&(*T)->rchild, value))
{
(*T)->bf -= 1;
if ((*T)->bf <= -2)
{
AVL_adjust(T);
return NO;
}
return YES;
}
}
}
else //向左子树中进行查找或插入
{
if ((*T)->lchild == NULL)
{
TreeNode new_node = (TreeNode)malloc(sizeof(Node));
new_node->lchild = new_node->rchild = NULL;
new_node->data = value;
new_node->bf = 0;
(*T)->lchild = new_node;
if ((*T)->rchild == NULL)
{
(*T)->bf += 1;
return YES;
}
else
return NO;
}
else
{
if (AVL_insert(&(*T)->lchild, value))
{
(*T)->bf += 1;
if ((*T)->bf >= 2)
{
AVL_adjust(T);
return NO;
}
return YES;
}
}
}
}
在以上代码中,我设置了标志当前插入过程返回结果的枚举量Status,NO=0表示当前完成返回的这一次插入操作没有对根节点T的bf值造成影响(也就是树的层数没有增高),而YES=1则表示当前返回的插入操作改变了根节点T的bf值。由于所有的插入过程都只可能对插入节点的直系亲属(也就是爸爸、爸爸的爸爸、……)的bf值造成影响(这个应该还是比较容易理解的),因此在每次递归返回时对当前根节点的bf值进行判别和修改,就能够完成对整棵树中所需要的全部bf值更新操作。
下面我们考虑在什么情况下需要对树的平衡性进行调整。平衡二叉树的理念是“时刻维护一棵平衡二叉树”,也就是说,不论在什么时候,只要树中出现了任何不平衡的因素(具体表现为|bf|>1),我们都要进行调整,使其平衡之后,再进行后续操作。经前述可知,某一次插入过程中,不平衡现象只可能发生在递归返回过程中对根节点的bf值进行修改时,因此,每次对根节点的bf值进行调整之后(也就是AVL_insert函数返回YES后),都需要判断当前节点的bf值是否已经超出了范围:
if (AVL_insert(&(*T)->rchild, value))
{
(*T)->bf -= 1;
if ((*T)->bf <= -2)
{
AVL_adjust(T);
return NO;
}
return YES;
}
因为我自己写的时候没有看书上的方法,因此在AVL_insert()函数的实现细节上,与书上介绍的方法还是有比较大的区别的。书上的做法是逐节点比较,直到当前调用的根节点为NULL时,才进行插入操作,通过一个叫做taller的变量来判断子树是否长高,函数自身返回值被用来判断是否插入成功了(即有没有重复项)。我写的时候没有考虑重复项问题(其实考虑了最多也就是设置一个表示有重复项的flag),而且正好返回值可以表明子树是否长高,就直接拿来用了,效果好像还不错。
关于一些细节问题的解释:
1.如何判断插入节点是否会引起高度变化:
相信下面这这张图已经足够说明问题了,关键就是在插入时判断另一侧有无兄弟节点,这也是在根节点的子树而不是空节点上插入带来的方便之处。
2.为什么可以直接将“>2”和“<-2”分开判断:
在左子树上进行添加操作,也就是通过value < (*T)->data这一条件来调用AVL_insert()时,如果返回了YES,只有可能是左子树增高,也即bf=左高-右高会增大;同理,在右子树上操作时,只有可能是右子树增高。因此若要产生不平衡,只有可能是左子树增高后bf>2,或右子树增高后bf<-2。
在弄清了应该在何时调整树的结构后,接下来要关注的就是怎样进行调整。通常的教材中会大致介绍一遍四种旋转方法——RR、LL、RL和LR。
下面先从比较简单的RR和LL两种旋转方法入手。
RR是由于不断往右子树中添加节点而造成|bf|=-2的情况,就最简单的三节点情况来说,需要把最小不平衡子树根节点的右子树替换到根节点的位置,而原本的根节点则作为其左子树,看起来就像把整棵子树向左旋转了一样,故称这种操作为“左旋”。在一般情况中,还可能有更多的下层结构,在旋转时,需要将最小不平衡子树根节点的右子树的左子树(也就是图中的 2,这个名字有点长。。。)重接为原根节点的右子树,以保证原本树中节点的完整。类似的,LL也是同样的操作,只不过改成了“右旋”。
具体代码实现如下,其中T是最小不平衡子树的根节点,L/R分别是其左、右子树:
void R_rotate(TreeNode *T) //右旋操作
{
TreeNode L = (*T)->lchild;
(*T)->lchild = L->rchild;
L->rchild = (*T);
*T = L;
}
void L_rotate(TreeNode *T) //左旋操作
{
TreeNode R = (*T)->rchild;
(*T)->rchild = R->lchild;
R->lchild = (*T);
*T = R;
}
顺带一提,在进行左旋/右旋后,只有两个节点的bf值会发生改变,也就是根节点T、左子节点L或右子节点R,而且在两种旋转方式中,从图例可以看出旋转后两个节点的bf值都变成了0。(为之后埋伏笔);另外LL和RR两种旋转方式的判定条件为T与R/L节点的bf值同号(即图示的+2/+1或-2/-1)。
以上两种操作结合图例说明应该还是比较好理解的,但是到了LR和RL,事情似乎就没有那么简单了。先来看看LR:
乍看之下,LR相比于之前的LL和RR,要多出一步旋转操作,但这么做的目的是什么呢?让我们从一般情况出发:
从图中可以看出,LR相较于LL,区别在于L节点上的bf值由+1变为了-1(注意L的bf值必不能为0!至于为什么不能是0,可以从“时刻维护一棵平衡二叉树”这一思想的角度证明,暂且按下不表(PS.或许会有附录什么的吧)),这就意味着L其实是“左轻右重”的。但根节点T却是“左重右轻”,如果直接像LL一样进行右旋的话,会使得旋转后的新根节点(L)的bf变成-2,无法达到使树平衡的目的。参考LL的经验,如果能将L的bf值转化为正值(“左重右轻”),则在旋转后可以避免上述问题。因此,我们先对以L为根节点的子树进行考虑:
如果要将本来bf<0的L节点子树变为bf>0,则需要进行左旋操作,让L的左子树层级增高,右子树层级减少。这里需要根据L节点右子节点Lr的bf值,分以下几类情况来讨论:
其中第三种情况正好对应之前提到的简单情况,个人感觉考虑的时候很容易忽略掉这个(至少我自己就忽略了。。。)根据Lr节点的bf值不同,初步旋转得到的L子树也存在一些差异。值得注意的是在情况①中,可能会出现局部bf值为±2的情况,这种情况在调整过程中是允许出现的,因为从之后的再次旋转操作可以看到,最终+2的bf值被调整为了0。
通过这幅图,我相信对于下面代码的逻辑,你们应该能够一目了然了:
TreeNode lrchild = (*T)->lchild->rchild;
switch (lrchild->bf)
{
case 1:
(*T)->bf = -1;
(*T)->lchild->bf = lrchild->bf = 0;
break;
case 0:
(*T)->bf = (*T)->lchild->bf = lrchild->bf = 0;
break;
case -1:
(*T)->lchild->bf = 1;
(*T)->bf = lrchild->bf = 0;
break;
}
L_rotate(&(*T)->lchild);
R_rotate(T);
说句题外话,在我看书上的代码的时候,他对于switch语句中那些case0、case1、case2表示什么意思全都语焉不详,而我一开始自己写的时候又没考虑Lr的bf=0的情况,费了好大劲才看懂为什么会有case0这个分支。这段代码没有解释的时候看起来很神奇,自己试过之后才发现其实是暴力枚举出来的。。。
RL的情况与LR大致相同,只是左右方向相反,具体看下图:
此时需要对R节点的左子节点Rl的bf值进行分类讨论,也可以枚举出三种可能情况。细节就不再赘述了。
对树进行调整平衡的函数代码如下,主要分RR、LL、RL和LR四种情况,分别进行处理:
void AVL_adjust(TreeNode *T)
{
/*调整最小不平衡子树*/
if ((*T)->bf == 2)
{
if ((*T)->lchild->bf == 1)
{
/*LL型,只需要将左子树右旋*/
(*T)->bf = (*T)->lchild->bf = 0;
R_rotate(T);
}
else
{
/*LR型,需要对左子树先进行左旋调整*/
TreeNode lrchild = (*T)->lchild->rchild;
switch (lrchild->bf)
{
case 1:
(*T)->bf = -1;
(*T)->lchild->bf = lrchild->bf = 0;
break;
case 0:
(*T)->bf = (*T)->lchild->bf = lrchild->bf = 0;
break;
case -1:
(*T)->lchild->bf = 1;
(*T)->bf = lrchild->bf = 0;
break;
}
L_rotate(&(*T)->lchild);
R_rotate(T);
}
}
else
{
if ((*T)->rchild->bf == -1)
{
/*RR型,只需要将左子树左旋*/
(*T)->bf = (*T)->rchild->bf = 0;
L_rotate(T);
}
else
{
/*RL型,需要对左子树先进行右旋调整*/
TreeNode rlchild = (*T)->rchild->lchild;
switch (rlchild->bf)
{
case 1:
(*T)->rchild->bf = -1;
(*T)->bf = rlchild->bf = 0;
break;
case 0:
(*T)->bf = (*T)->rchild->bf = rlchild->bf = 0;
break;
case -1:
(*T)->bf = 1;
(*T)->rchild->bf = rlchild->bf = 0;
break;
}
R_rotate(&(*T)->rchild);
L_rotate(T);
}
}
}
在具体使用的时候,只需要调用前面的AVL_insert()函数进行元素插入即可,调节平衡的操作会在插入过程中自动判断并实现。
最后,肝文不易,如果觉得对你有帮助的话,还请看到这里的各位读者动动手指点个赞或者收藏支持一下,谢谢啦!当然也欢迎关注哦!(虽然更新比较看心情……)