本文由作者收集整理所得,作者不保证内容的正确行,转载请标明出处。
作者:关新全
1、AVL的插入算法描述
在平衡的二叉排序树T上插入一个关键码为kx的新元素,递归算法可描述如下:
(一) 若T为空树,则插入一个数据元素为kx的新结点作为T的根结点,树的深度增1;
(二) 若kx和T的根结点关键码相等,则不进行插入;
(三) 若kx小于T的根结点关键码,而且在T的左子树中不存在与kx有相同关键码的结点,则将新元素插入在T的左子树上,并且当插入之后的左子树深度增加1时,分别就下列情况进行处理:
⑴ T的根结点平衡因子为-1(右子树的深度大于左子树的深度),则将根结点的平衡因子更改为0,T的深度不变;
⑵ T的根结点平衡因子为0(左、右子树的深度相等),则将根结点的平衡因子更改为1,T的深度增加1;
⑶ T的根结点平衡因子为1(左子树的深度大于右子树的深度),则若T的左子树根结点的平衡因子为1,需进行单向右旋平衡处理并且在右旋处理之后,将根结点和其右子树根结点的平衡因子更改为0,树的深度不变;若T的左子树根结点平衡因子为-1,需进行先左后右双向旋转平衡处理,并且在旋转处理之后,修改根结点和其左、右子树根结点的平衡因子,树的深度不变。
(四) 若kx大于T的根结点关键码,而且在T的右子树中不存在与kx有相同关键码的结点,则将新元素插入在T的右子树上并且当插入之后的右子树深度增加1时,分别就不同情况处理之。其处理操作和(三)中所述相对称。
AVL树的四种情况:
1. 简单右旋:当插入项位于最近的平衡因子为+2的祖先节点的左孩子的左子树中时采用。见图1-1.
图1-1 简单右旋
图1-1中,将新节点插入到A节点的左孩子B的左子树L(B)中。
简单右旋的操作步骤如下:
1. A的父指针指向B
2. A的左孩子指向B的右孩子
3. B的右孩子指向A。
4. 更改A的平衡因子为0,更改B的平衡因子为0.
注意L(B)中的平衡因子在添加节点回溯时已经进行了修改,其他部分的平衡因子不变。(添加节点回溯将在稍后介绍)。
2. 简单左旋:当插入项位于最近的平衡因子为-2的祖先节点的右孩子的右子树中时。如图1-2.
图1-2 简单左旋
图1-2中,新节点插入到了A节点右孩子B的右子树R(B)中,使用简单左旋后,二叉树变的平衡。
简单左旋的步骤如下:
1. A的父指针指向B
2. A的右孩子指向B的左孩子
3. B的左孩子指向A
4. 修改B的平衡因子为0,修改A的平衡因子为0
5. R(B)中的平衡因子在添加新节点回溯过程中进行修改,其他部分的平衡因子不变。
3. 左右旋:当插入项位于最近的平衡因子为+2的祖先节点的左孩子的右子树中时,见图1-3.
图1-3 左右旋(LRL)
从图1-3可以看出,新节点插入到了A节点的左孩子B的右孩子C的左子树中。
基本操作步骤为:
1. B的右儿子指向C的左儿子
2. C的左儿子指向B
3. A的父指针指向C
4. A的左儿子指向C的右儿子
5. C的右儿子指向A
6. B的平衡因子置为0,C的平衡因子置为0,A的平衡因子置为-1.L(C)中的平衡因子在回溯的时候进行修改。
注意,图1-3所示的为,新节点插入到A节点的左孩子B的右孩子C的左子树中,称这种情况为(LRL),还有一种情况是,新节点插入到A节点的左孩子B的右孩子C的右子树中,这种情况称为(LRR),LRR的操作步骤与LRL相似,就是第6步不同。
LRR第6步为:
LRR中平衡因子B修改为+1,C修改为0,A修改为0,R(C)中的平衡因子在回溯过程中进行更改。
LRR参考图1-4.
图1-4 左右旋(LRR)
4. 右左旋:当插入项位于最近的平衡因子为-2的祖先节点的右孩子的左子树中时使用。见图1-5.
图1-5 右左旋(RLL)
图1-5中,新节点插入到了A的右孩子B的左孩子C的左子树中。
基本操作步骤:
1. B的左孩子指向C的右孩子
2. C的右孩子指向B
3. A的父指针指向C
4. A的右孩子指向C的左孩子
5. C的左孩子指向A
6. A的平衡因子改为0,B的平衡因子为-1,C的平衡因子为0,L(C)的平衡因子在回溯过程中进行修改。
与左右旋相似,右左旋的第二种情况称为(RLR),旋转后见图1-6.
图1-6 右左旋(RLR)
RLR与RLL的前五步相同,RLR的第六步为:
RLR需要将A的平衡因子修改为+1,将B的平衡因子修改为0,将C的平衡因子修改为0。R(C)中的平衡因子在回溯过程中修改。
注意:旋转后,路径上剩余的节点的平衡因子不需要改变。(这个自己可以很容易推导出来)
如何插入:
(1)、如何回溯修改祖先结点的平衡因子
我们知道,在AVL树上插入一个新结点后,有可能导致其他结点BF(平衡因子)值的改变,哪些结点的BF值会被改变?如何计算新的BF值呢?要解决这些问题,我们必须理解以下几个要点:只有根结点到插入结(橙色结点)点路径(称为插入路径)上的结点的BF值会被改变。如图2所示,只有插入路径上结点(灰色结点)的BF值被改变,其他非插入路径上结点的BF值不变。
当一个结点插入到某个结点的左子树时,该结点的BF值加1(如图2的结点50、43);当一个结点插入到某个结点的右子树时,该结点的BF值减1(如图2的结点25、30)。如何在程序中判断一个结点是插入到左子树还是右子树呢?很简单,根据二叉查找树的特性可以得出结论:如果插入结点小于某个结点,则必定是插入到这个结点的左子树中;如果如果插入结点大于某个结点,则必定插入到这个结点的右子树中。
修改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值。
(2)、何时进行旋转操作?如何判断作什么类型的旋转?
在回溯修改祖先结点的平衡因子时,如果碰到某个结点的平衡因子变为2或-2,表明AVL树失衡,这时需要以该结点为旋转根,对最小不平衡子树进行旋转操作。由于是从插入点开始回溯,所以最先碰到的BF值变为2或-2的结点必定为最小不平衡子树的根结点。如图4所示,插入39后,43和50两个结点的BF值都会变为2,而必定先访问到结点43,所以43是最小不平衡子树的根。根据以上Flash动画演示所示,旋转操作完成后,最小不平衡子树插入结点前和旋转完成后的高度不变,所以可以得出结论:旋转操作完成后,无需再回溯修改祖先的BF值。这样,图4中的结点25和50的平衡因子实际上在插入结点操作完成后的BF值不变(对比图2)。
实现考虑:
整个算法需要一个栈来配合,首先,在节点插入时,从根节点开始判断,直至找到节点需要插入的位置,并将经过的整个路径放入栈中进行存储。整个查找过程时间复杂度为log(n).然后按照栈的顺序进行回溯,修改栈内(即插入路径)节点的平衡因子的值,直到碰到修改后的值为0的节点(结束),或者碰到修改后值为正负2的节点(进行旋转),整个回溯过程最坏会在log(n)内完成,旋转操作是常数时间复杂度。因此,整个插入过程的时间复杂度为log(n)。(更准确的说在2*log(n)内完成)。
本文是作者总结他人成果之上编写而成,转载引用时请标明出处。
最后感谢那些提供丰富示例的网络资源,尤其是参考资料中的那个连接,连接里楼主不光语言精练,还做了平衡二叉树的动画演示,希望大家多去看看。
参考:
http://webtrados.llh4.com/post/522.html
数据结构与算法分析 C++语言描述 第二版 jarry Nyhoff著。
http://blog.sina.com.cn/s/blog_616e189f0100qgcm.html