看完了《数据结构与算法分析(C++描述)》的4.4节AVL树,做一个总结,整理一下自己实现删除算法的思路.(注:本文中图片均来自《数据结构与算法分析(C++描述)》)
AVL(Adelson-Velskii and Landis,由阿德尔森一维尔斯和兰迪斯在1962年提出,因此得名)树是带有平衡条件(balance condition)的二叉查找树.
我们知道空子树的高度通常被定义为-1,叶子结点的高度为0,其它结点的高度为其左右子树中的最大高度加1.一颗AVL树中的每一个结点的左子树和右子树的高度差不超过1.由此,我们可以考察一下一颗高度为h的AVL树的最少结点数S(h).显然S(0)=1,S(1)=2,而S(2)=S(0)+S(1)+1=3.由数学归纳法可以证明S(h)=S(h-1)+S(h-2)+1.
AVL树的优势显而易见,它能更快的进行查找.那么怎样形成并保持一颗AVL树呢?首先我们来看insert(插入)操作.假设我们向一颗空的AVL树中按顺序插入1,2,3这3个数.1是根,2是1的右子树,3是2的右子树,这时我们发现1已经不满足平衡条件了,它的左子树高度为-1,而右子树高度为1.当遇到这种情况时,我们要立即进行某种操作使得不满足平衡条件的结点重新满足平衡条件.通过画图分析,可得在insert操作中,出现一个结点t不满足平衡条件可能出现在下面4种情况中:
(1)在t的左儿子的左子树中进行一次插入.(例如:3,2,1)
(2)在t的左儿子的右子树中进行一次插入.(例如:3,1,2)
(3)在t的右儿子的左子树中进行一次插入.(例如:1,3,2)
(4)在t的右儿子的右子树中进行一次插入.(例如:1,2,3)
注意,上面举得例子只是最简单的的例子,我们应该多插入一些结点需找规律.这种寻找”测试数据”的能力也是必不可少的,以后自己要多加注意.课本上给出了一个不长但很有价值的例子.从初始为空的AVL树开始插入项3,2和1,然后依序插入4到7.
当插入1时,根结点3不满足平衡条件,这时情况比较简单,我们自然会想到让2作为根,而1和3分别作为2的左右子树.那么继续插入,当插入5时,结点3出现了不平衡,同样我们想到让4取代3的位置,而3和5分别作为4的左右子树.
当插入6时,我们发现2不满足平衡条件,这时我们按照刚才的思路,试着让4取代2的位置,而2作为4的左儿子,2的左子树和4的右子树不变.最后我们注意到4的左子树中结点的值一定大于2且小于4,因此将原来4的左子树移到新的2结点的右子树.
由以上分析,我们可以得到情况(1)和(4)的解决方法.以情况(1)为例,如下图的左边部分,k1是k2的左儿子,在k1的左子树插入一个结点后使得k2不满足平衡条件.可以这样推理:
k2出现不平衡
->k2的左子树比右子树高2
->k1的高度本就比Z高1,现在又提高了1;又k1在其左子树X中插入结点后依然满足平衡条件的且其高度增加了1
->X,Y,Z原本一样高;在X中插入结点使得X本身高度加1.
得到这些子树的高度关系后,我们就可以通过”旋转”使得k2重新满足平衡条件.我们将Y先拿掉,将k1和k2对调(k1的左子树X和k2的右子树Z依然跟着它们),最后再根据查找树的性质,将Y作为k2的右子树,这样k1和k2就都满足了平衡条件.这种操作可以叫做左单旋转.注意,这样一来,这整个一颗树的高度在插入前和插入并旋转后没有发生变化,因此必然不会影响它的父节点(如果存在的话)的平衡情况.也就是说,对于情况(1)和(4),一次insert操作最多只需要一次的旋转操作就可以使整颗AVL树保持平衡.事实上,我们将看到情况(2)和(3)也是这样.
接下来,我们来看情况(2).如下图,k1的右子树Y在进行一次插入操作后比X高1使得k2不满足平衡条件.这时如果我们按照上面的方法进行旋转会发现只是从情况(2)变成情况(3).
继续在纸上画图,既然k1的右子树Y进行了一次插入操作,那么它必定不会为空.使用课本上的图,重新给这几个关键结点命名将Y的根设为k2,如下图:
我们不知道B和C是否一样高或者谁高,我们也不需要知道,但我们知道k1和k2都是满足平衡条件的并且k2的高度比A高1也就是说B和C中至少有1个和A一样高,至多有一个比A矮1;类似上面情况(1)的推理,我们还能推得A和D一样高.这样一来,我们就可以放心的开始使一切变得平衡了.根据查找树的性质和我们得出的A,B,C,D四颗子树高度的关系,如图4-38的右边部分,我们将k2作为新的根,k1和k3分别为它的左右子树,A和D的相对位置没有变,而B和C被移动到了合适的位置.这种操作可以叫做左双旋转.注意到,和情况(1)一样,这样一来这整个一颗树的高度在插入前和插入并旋转后没有发生变化,因此必然不会影响它的父节点(如果存在的话)的平衡情况.
知道怎样使AVL树保持平衡之后,就可以得出insert的算法.像一般的二叉查找树那样找到结点应该被插入的位置.然后从被插入点的父亲向上开始检查它的每个祖先,如果发现了一个不满足平衡条件的结点,立即进行”旋转”操作.如前所述,插入结点后,只要最多一次旋转就可以AVL树依然是平衡的.
下面给出两种旋转和insert操作的C++代码,这里给出情况(1)和(2)的代码,完整的代码在这里.至于insert的代码,基本就是课本上图4-42中代码,只是做了一个小小的改进.
1 void leftSingleRotation(AVLnode * &t)//左单旋转
2 {
3 AVLnode *s=t->left;
4 t->left=s->right;
5 s->right=t;
6 t->height=max(height(t->left),height(t->right))+1;//重新计算s和t的高度
7 s->height=max(height(s->left),t->height)+1;
8 t=s;
9 }
1 void leftDoubleRotation(AVLnode * &t)//左双旋转
2 {
3 AVLnode *p=t->left;
4 AVLnode *q=p->right;
5 t->left=q->right;
6 p->right=q->left;
7 q->left=p;
8 q->right=t;
9 t->height=max(height(t->left),height(t->right))+1;//重新计算3个结点的高度
10 p->height=max(height(p->left),height(p->right))+1;
11 q->height=max(p->height,t->height)+1;
12 t=q;
13 }
1 void insert(const Object &x,AVLnode * &t)
2 {
3 if(!t)
4 t=new AVLnode(x,NULL,NULL);
5 else if(x<t->data)
6 {
7 insert(x,t->left);
8 if(height(t->left)-height(t->right)==2)//在左子树插入结点后,不可能右子树比左子树高2
9 if(x<t->left->data)
10 leftSingleRotation(t);//左单旋转
11 else
12 leftDoubleRotation(t);//左双旋转
13 else//不需要调整就满足平衡条件的话,只需要重新计算其高度就好
14 t->height=max(height(t->left),height(t->right))+1;
15 }
16 else if(x>t->data)
17 {
18 insert(x,t->right);
19 if(height(t->right)-height(t->left)==2)
20 if(x>t->right->data)
21 rightSingleRotation(t);//右单旋转
22 else
23 rightDoubleRotation(t);//右双旋转
24 else
25 t->height=max(height(t->left),height(t->right))+1;
26 }
27 else;//不考虑重复的值
28 }
继续,我们来看remove(删除)操作.首先,回顾一下对于一般的二叉查找树是怎样进行remove操作的.先根据二叉树的性质找到应该删除的结点t(如果有的话);如果t是个叶子结点,直接delete就好;如果它只有一颗非空的子树,那么就让这颗子树继承被删除结点的位子;而如果它有两颗非空的子树,就在右子树X中找到值最小的结点s,将s的data(值)写到t中,再在X中删除s(因为s的左子树一定为空,否则它就不是X中最小的了.另外,当然也可以在左子树中找值最大的结点.)
接下来,我们得仔细分析删除的时候会发生什么,在什么情况下会出现怎样的不平衡.这里我使用的是课本上在讲解insert时用到的一颗树,如右图.由于我比较懒,就不在这里画出删除结点之后的各种图形了,大家可以在纸上画,也许画着画着就想出比我的解法更好的解法了.
假设删除结点10,再删除结点12,这时候结点11就不满足平衡条件了,出现了类似情况(1)的情形.
再假设先删除结点8,再删除结点12,这时候还是结点11不平衡,但这时类似情况(2).
再假设直接删除结点12,这时依然是结点11不平衡,这时虽然和情况(1),(2)都不相同,但我们可以像图4-34那样经过一次单旋转使结点11变得平衡.(只不过这时图4-34中的Y和X一样高而不是比X矮1.)
这里特别需要注意的是,不管上上面3中情况中的哪一种情况,通过旋转使结点11平衡之后,它的高度都从2变成了1.这意味着,它的父亲的高度可能发生变化并且可能不再满足平衡条件.所以我们要从被删除点的父亲向上开始检查它的每个祖先,平衡每一个不满足平衡条件的祖先,直到找到了一个高度没有发生变化的祖先.
另外,而对于删除有一个或者两个非空子树的结点,实际上都只是删除一个叶子结点.这样一来,我们得到了remove操作可能引发不平衡的4种情况.我们假设第一个不满足平衡条件的结点为t,t的左子树为X,右子树为Y.
(1)在Y中删除了一个结点,而X的右子树不高于X的左子树.
(2)在Y中删除了一个结点,而X的右子树高于X的左子树.
(3)在X中删除了一个结点,而Y的左子树不高于Y的右子树.
(4)在X中删除了一个结点,而Y的左子树高于Y的右子树.
对于情况(1),我们可以通过一次左单旋转使t平衡;而对于情况(1),我们可以通过一次左双旋转使t平衡;而情况(3)和(4)就可能分别需要进行右单旋转和右双旋转.下面给出我的remove操作代码:
1 void AVLTree<Object>::remove(const Object &x,AVLnode *&t)
2 {
3 if(!t)return;//没有找到要删除的值,do nothing
4 if(x<t->data)
5 {
6 remove(x,t->left);
7 if(height(t->right)-height(t->left)==2)
8 {
9 //右子树比左子树高2,那么在删除操作之前右子树比左子树高1,
10 //也就是说t的右子树必然不为空,甚至必然有非空子树(高度至少为1).
11 AVLnode *s=t->right;
12 if(height(s->left)>height(s->right))
13 rightDoubleRotation(t);//右双旋转
14 else
15 rightSingleRotation(t);//右单旋转
16 }
17 else
18 //不需要调整就满足平衡条件的话,只需要重新计算其高度就好
19 t->height=max(height(t->left),height(t->right))+1;
20 }
21 else if(x>t->data)
22 {
23 remove(x,t->right);
24 if(height(t->left)-height(t->right)==2)
25 {
26 AVLnode *s=t->left;
27 if(height(s->right)>height(s->left))
28 leftDoubleRotation(t);//左双旋转
29 else
30 leftSingleRotation(t);//左单旋转
31 }
32 else
33 t->height=max(height(t->left),height(t->right))+1;
34 }
35 else
36 {
37 if(t->left&&t->right)
38 //t的左右子树都非空,把remove操作转移到只有一个非空子树的结点或者叶子结点上去
39 {
40 if(height(t->left)>height(t->right))
41 //把remove操作往更高的那颗子树上转移
42 {
43 //左子树中的最大值
44 t->data=max_node(t->left)->data;
45 remove(t->data,t->left);
46 }
47 else
48 {
49 //右子树中的最小值
50 t->data=min_node(t->right)->data;
51 remove(t->data,t->right);
52 }
53 }
54 else
55 {
56 AVLnode *oldnode=t;
57 t=t->left?t->left:t->right;
58 delete oldnode;
59 }
60 }
61 }
这里是我写的一个AVL的简单实现: C++实践笔记(四)----AVL树的简单实现
其中有一个粗糙的使用队列进行层次遍历打印树的代码,现在看起来如果想使对齐更完美,可以设置一个字符串最大长度的值.如果数据的长度比这个值小,就用空格补齐.