AVL树上结点的插入

 

AVL树上结点的插入

AVL算法的思想理解起来还是不太困难的,但如果真要使用代码实现就没那么简单了,它拥有超高的算法实现复杂度。我查了很多资料,大部分只给出主要算法代码,对于如何回溯修改BF值,如何处理不需要旋转的情况绝口不提,甚至对删除算法直接忽略。上网找资料,中文的,英文的全找了,大部分写代码不加注释,狂汗....,实在看不下去。大部分代码使用递归算法,C#实现更是少得可怜,在国外网站找到一个,但使用了三叉链表实现,多加了一个parent指针,总之无法找到让人满意的代码。最后一咬牙一跺脚,自己实现。最让人头痛的莫过于如何处理插入和删除后的回溯和修改BF值,庆幸的是最终还是按照我最初的想法比较漂亮地实现了AVL树。优点是:无递归;无parent指针;插入和删除操作使用同一旋转方法,使代码更为简化。缺点是:为了兼顾效率,有些地方的处理比较特殊,代码很难完全读懂。

下面对本算法做原理上的介绍:

1、如何回溯修改祖先结点的平衡因子

我们知道,在AVL树上插入一个新结点后,有可能导致其他结点BF值的改变,哪些结点的BF值会被改变?如何计算新的BF值呢?要解决这些问题,我们必须理解以下几个要点:

l         只有根结点到插入结(橙色结点)点路径(称为插入路径)上的结点的BF值会被改变。如图2所示,只有插入路径上结点(灰色结点)的BF值被改变,其他非插入路径上结点的BF值不变。

《AVL树上结点的插入》

 

 

 

l         当一个结点插入到某个结点的左子树时,该结点的BF值加1(如图2的结点50、43);当一个结点插入到某个结点的右子树时,该结点的BF值减1(如图2的结点25、30)。如何在程序中判断一个结点是插入到左子树还是右子树呢?很简单,根据二叉查找树的特性可以得出结论:如果插入结点小于某个结点,则必定是插入到这个结点的左子树中;如果如果插入结点大于某个结点,则必定插入到这个结点的右子树中。

l         修改BF值的操作需从插入点开始向上回溯至根结点依次进行,当路径上某个结点BF值修改后变为0,则修改停止。如图3所示,插入结点30后,首先由于30<43,将结点43的BF值加1,使得结点43的BF值由0变为 1;接下来由于30>25,结点25的BF值由1改为0;此时结点25的BF值为0,停止回溯,不需要再修改插入路径上结点50的平衡因子。道理很简单:当结点的BF值由1或-1变为0,表明高度小的子树添加了新结点,树的高度没有增加,所以不必修改祖先结点的平衡因子;当结点的BF值由0变为1或-1时,表明原本等高左右子树由于一边变高而导致失衡,整棵子树的高度变高,所以必须向上修改祖先结点的BF值。

 

《AVL树上结点的插入》

 

2、何时进行旋转操作?如何判断作什么类型的旋转?

在回溯修改祖先结点的平衡因子时,如果碰到某个结点的平衡因子变为2或-2,表明AVL树失衡,这时需要以该结点为旋转根,对最小不平衡子树进行旋转操作。由于是从插入点开始回溯,所以最先碰到的BF值变为2或-2的结点必定为最小不平衡子树的根结点。如图4所示,插入39后,43和50两个结点的BF值都会变为2,而必定先访问到结点43,所以43是最小不平衡子树的根。根据以上Flash动画演示所示,旋转操作完成后,最小不平衡子树插入结点前和旋转完成后的高度不变,所以可以得出结论:旋转操作完成后,无需再回溯修改祖先的BF值。这样,图4中的结点25和50的平衡因子实际上在插入结点操作完成后的BF值不变(对比图2)。

 

《AVL树上结点的插入》

 

可以通过旋转根及其孩子的BF值来决定作什么类型的旋转操作:

l         当旋转根的BF值为2时:

如果旋转根的左孩子的BF值为1,则进行LL型旋转;

如果旋转根的左孩子的BF值为-1,则进行LR型旋转。

l         当旋转根的BF值为-2时:

如果旋转根的右孩子的BF值为1,则进行RL型旋转;

如果旋转根的右孩子的BF值为-1,则进行RR型旋转。

可通过观察之前的Flash动画检验以上结论。

3、如何保存插入路径?

可以使用栈来保存插入路径上的各个结点,但由于栈是由数组抽象而来,为了进一步加快AVL树的运行速度,我直接使用数组存放插入路径,这样可以减少方法的调用,尽量避免一些不必要的操作。

如果实现AVL树实现索引器,而在索引器中使用int32,那么AVL树元素的长度不会超过一个32位整数的最大值。一个深度为32的满二叉树可以存放结点数为:2^32-1=4294967295,这个值已经远远超出32位的整数范围,所以我将数组的长度定为32。这样就不必如ArrayList那样进行扩容操作了。另外本程序还使用了一个成员变量p用于指示当前访问结点,由于p指针的存在可以不必在每次进行插入和删除操作后清空数组中的元素,进一步增加了AVL树的运行速度。

使用数组的另一个好处是可以随时访问旋转根的双亲结点,以方便进行旋转操作时修改根结点。

AVL树上结点的删除

AVL树的删除操作与插入操作有许多相似之处,它的大体步骤如下:

⑴用二叉查找树的删除算法找到并删除结点(这里简称为删除点);

⑵沿删除点向上回溯,必要时,修改祖先结点的BF值;

⑶回溯途中,一旦发现某个祖先的BF值失衡,如插入操作那样旋转不平衡子树使之变为平衡,跟插入操作不同的是,旋转完成后,回溯不能停止,也就是说在AVL树上删除一个结点有可能引起多次旋转。

AVL树上的删除和插入操作虽然大体相似,但还是有一些不同之处,大家需要注意以下几点:

1、  回溯方式的不同

在删除结点的回溯过程中,当某个结点的BF值变为1或-1时,则停止回溯。这一点同插入操作正好相反,因为BF值由0变为1或-1,表明原本平衡的子树由于某个结点的删除导致了不平衡,子树的总体高度不变,所以不再需要向上回溯。

2、旋转方式的不同

如图5所示:删除AVL树中的结点25导致结点50的BF值由原来的-1变为-2,但旋转根50的右孩子的BF值为0,这种情况在前面所讲的旋转操作中并不存在,那么是需要对它进行RR旋转还是RL旋转呢?正确方法是使用RR旋转,所不同之处是旋转后的BF值不同,需要单独处理。需要注意,这种情况在插入操作时不可能发生,LL旋转也存在类型的情况。另外旋转完成后树的整体高度没有改变,所以大部分情况下旋转操作完成后,子树的高度降低,需要继续向上回溯修改祖先的BF值,而只有这种情况由于子树的高度未改变,所以停止回溯。

 

 《AVL树上结点的插入》

 

 

3、删除点的选择特例

在二叉查找树中,我们知道当删除点p既有左子树,又有右子树,此时可以令p的中序遍历直接前驱结点代替p,然后再从二叉查找树中删除它的直接前驱。如图7.13所示,结点5既有左子树,又有右子树,它的直接前驱结点为4。在删除结点5时,首先用结点4代替结点5,然后再删除结点4完成删除操作。这里需要注意的是此时必须将删除前的结点4作为删除点来进行向上回溯操作,而不是结点5。

 

 

《AVL树上结点的插入》

    原文作者:AVL树
    原文地址: https://blog.csdn.net/luyafei_89430/article/details/7622945
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞