AVL树的创建
之前我们提到了二叉搜索树的创建、删除,这回我们来讨论一下AVL树的创建。
以下是来自维基百科对AVL的定义:
在计算机科学中,AVL树是最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为一,所以它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下都是O(log n)。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。AVL树得名于它的发明者G.M. Adelson-Velsky和E.M. Landis,他们在1962年的论文《An algorithm for the organization of information》中发表了它。
其重点是可以自我平衡,从而减少检索的时候所消耗的时间。假设1-10依次插入到二叉树中,在没有自我平衡的情况下,其形成的树和链表无异,并没有起到加快检索的工作。假如二叉树可以在插入节点的同时进行自我平衡的操作,那么就可以大大减少检索所消耗的时间。
我们先来明确一些概念:
1.一棵树的平衡系数是其左子树的高度减去其右子树的高度,当一棵树的平衡系数为-1、0、1的时候,这棵树是处于平衡状态的,反之则是不平衡的。
为了编写程序方便,当出现平衡系数的绝对值大于等于2的情况的时候,就要将这棵非平衡树转换成平衡树的状态。
2.离插入节点最近的平衡系数的绝对值大于等于二的节点作为根节点所形成的树称为最小非平衡子树(minimum unblanced subtree)

例如这棵二叉树,当插入节点“J”的时候,以B为根节点所形成的子树称为最小非平衡子树,因为其左子树高度3与右子树高度0相差3,已经大于等于2.为了让树处于一个平衡树的状态,我们需要将子树B进行一定的旋转操作。
旋转分为四种情况:
1.从右侧开始的左旋转(非平衡状态均发生在右子树侧:RR)
2.从左侧开始的右旋转(非平衡状态均发生在左子树侧:LL)
3.先进行右旋转再进行左旋转(非平衡状态从上到下先发生在左子树,再发生到整棵树:LR)
4.先进行左旋转,再进行右旋转(非平衡状态从上到下先发生在右子树,再发生到整棵树:RL)
我们先从简单的1、2种情况说起:首先是从右侧开始的向左旋转:

假设这是一棵处于平衡的树,当我们在BR下面再进行插入节点工作后(对右子树的右孩子进行插入节点操作,所以称为RR状态),整棵树就处于非平衡状态了:

我们发现,A节点(根)此时处于非平衡状态,其BF已经等于-2了(A->L->height - A->R->height = -2)。为了将其变成平衡状态,我们要进行从右侧开始的向左旋转。我们不妨将这棵二叉树当作一座未扶正的房子,当且仅当其三根柱子都处于着地的状态,其才能处于平衡状态,并且这期间是不能多一根柱子,少一根柱子的。当其处于平衡状态的时候,树如图所示:

我们不妨想象一下这棵树的旋转过程:首先,这棵树的根节点从A变成了B,将A变成了B的左节点,并将原先B的左节点赋值给了A的右节点。假如原先A节点上面还有节点(即A实际上为一棵子树),那么直接将B转上去显然是不可行的(相当于是形成三叉树了)。这时候只能将B的左子树接到A的右子树处。原因在于BL肯定小于B,但由于处于A的右侧,所以肯定比A大,而当A转下来的时候,其右子树必定处于NULL的状态,因此将BL接到A的右侧(即作为AR)是没有任何问题的。BR理论上也可以接到AR处,但考虑到程序的可行性以及与A的相近性等因素,所以将BL接到AR处显然更合适。
部分伪码如下:singleRR{
0.设定一个子节点 rNode;
1.将A的右节点(B)赋值给rNode;
2.将B(rNode)的左节点赋值给A的右节点;
3.将A赋值给rNode的左节点
}
需要注意的的是,第二句和第三句是绝对不能调换顺序的,其原因在于如果先执行了第三句,那么B的左节点就没发再定位到了,所以要先将BL赋值给A的右节点,然后再将整个A赋值给B(rNode)的左节点
说完RR,趁热说了LL:

当在BL处插入一个节点后(对左子树的左孩子进行插入节点操作,所以称为LL状态),整棵树就处于非平衡状态:

此时进行同RR相似的操作:将B提上去,将B的右孩子接到A的左孩子处。操作缘由和RR相似,在这里不再赘述。进行旋转完毕后,树如图所示:

部分伪代码如下:
singleLL{
0.建立节点lNode;
1.将A的左节点(B)赋值给lNode
2.将B(lNode)的右节点赋值给A的左节点
3.将A赋值给lNode(B)的右节点
}
对RR和LL有了一定了解后,接下来就要进行双旋转LR和RL的了解了
与RR和LL不同的是,LR和RL都要进行二次旋转,而旋转方式和RR与LL是完全相同的,因此只要理解旋转方式,那么对于LR与RL就不难理解了
我们先来看LR旋转:对树的左子树的右节点进行节点插入操作:
首先是一棵平衡的树:
当对A的左子树的右孩子C进行插入节点操作时,就处于非平衡状态:

而这部分旋转是分成两部分的(这也就是为什么不能一眼看出原课件那张图是怎么转出平衡状态的):首先对A的左子树进行RR旋转:

我们可以看到,将B转下,将C的左子树接到原树(B)的右节点和原旋转方式RR是一模一样的(即将整棵子树B进行RR旋转),之后,对整棵树A进行LL旋转:

将C提上去,并将C的右节点接到原树的左节点处,和LL旋转是完全一样的。
由此,我们总结出一条规律:当出现LR状态的时候,首先对原树的左子树进行RR旋转,之后再对整棵树进行LL旋转。
也因此,伪码比LL和RR还要简单:
{
1.对原树的左子树进行RR旋转
2.对整棵树进行LL旋转
}
由此,RL也就不难理解了:
假设原树如下:
当对树的右子树的左孩子进行插入节点操作后,整棵树处于不平衡状态:

与LR一样,RL也是进行两次操作:先将A的右子树进行LL旋转,然后再对整棵树进行RR旋转,旋转完后树如下图所示:

{
1.对原树的右子树进行LL旋转
2.对整棵树进行RR旋转
}
接下来是代码部分:
首先是节点部分:

关于插入部分,C语言的表达如下:(根据课件调整)

首先请确保你对之前的insert和delete部分的调用很了解,否则最好先退一步对前面的二叉搜索树进行一定了解,然后再对这一部分进行复习。
这部分采用的仍然是返回值的方式进行插入工作,导入的变量是被插入的树和插入元素(此处为整型的num)
首先是第一部分:

这一部分不多赘述,如果是空的话,分配空间,进行相关赋值。需要注意的是,这里的树是有高度这一变量的,写程序的时候一定不要忘记。
假如不是空树(或者空节点)那么就要判断数值并进行插入操作了:
我们假设插入到数值是小的,需要插入到左节点处:

首先是插入操作:假如此时只有一个节点,那么高度差是不会出现大于等于二的情况的,也就是说下面的if语句并不会执行。跳转到下面的高度赋值语句。
假设插入的节点构成非平衡状态了,为什么只判断2,而不是-2呢?因为如果插入到左子树部分出现非平衡状态,左子树的高度必定大于右子树,这也就是为什么不必判断-2以及其他情况。另外需要注意的是,每次插入,调换的永远是从节点处往上数的部分。而每次递归回到上一个函数,同样会对从这棵树(子树)开始进行平衡状态的判断,直到整棵树进入平衡状态为止。
当处于判断是否等于2后,又出现两个分支:如果插入的值比左子树上的数值更小,那么只能插到左子树的左孩子处(LL),所以要进行从左处开始的向右旋转,反之,插入到了右孩子处,那么插入到哪里就都无所谓了,只要进行RR,再进行LL(完整旋转即为LR)旋转即可
旋转完毕后,直接对整棵树的高度进行高度判断,之后返回节点地址,结束函数。
关于右半部分和左半部分原理相同,不再赘述。

浙公网安备 33010602011771号