数据结构和算法分析学习笔记(三)--二叉查找树的懒惰删除(lazy deletion)
这次的问题来自《数据结构与算法分析(C++描述)》的习题4.16,如下:
--------------------------
4.16 重做二叉查找树类以实现懒惰删除.注意,这将影响所有的例程.特别具有挑战性的是findMin和findMax,它们现在必须递归的完成.
--------------------------
这题没有参考答案,我也不敢保证自己写的是对的,有不对的地方请指正,谢谢.先做一下说明,首先这只是一般的二叉查找树,不是AVL树.其次其中的printTree()函数只是将树中的结点按升序打印出来,不是像树的结构那样打印(可以参见这里的print()函数).最后,我定义的数据结构是在有重复项的情况下使用的.也就是说,每一个结点有一个记录数据出现次数的成员count(非负整数).当第一次插入某一数据时,count为1,以后每插入一次这一数据则它的count加1.而当某一数据没有被实际删除且它的count大于0时,对它的删除操作就是将它的count减1.当逻辑上被删除的结点(count值为0的结点)的数目达到实际上存在的总结点数目的一半时,就对二叉查找树进行真正的删除操作(delete).
下面在课本中给出的一般二叉查找树结构(参见书中图4-16至图4-28)的基础上进行修改,得到我们所需要的结构.首先自然是结点的结构,如前所述,需要对结点BinaryNode类增加一个记录数据出现次数的成员count.而在BSTree类中,我们需要两个int型的私有成员分别统计树的总结点数和其中在逻辑上已经被删除的结点数.接下来是contains(),makeEmpty(),printTree()和insert(),这些都比较简单.
我与课本上makeEmpty( BinaryNode * & t )(置空操作)的不同是,我没有在最后将节点置为NULL,而是在调用这个函数之后将实参置为NULL,不知道这样做会不会有什么错误.对于insert函数,我们可以看到这种结构的好处,对于那些仅在逻辑上被删除的数据,重新插入它只需要将count加1并且将delSize减1.
1 template <typename Object>
2 class BSTree
3 {
4 public:
5 BSTree( ){theSize=delSize=0;root=NULL;}
6 ~BSTree( )
7 {
8 makeEmpty(root);
9 theSize=delSize=0;
10 }
11
12 bool contains( const Object & x ) const//是否包含某值
13 {return contains(x,root);}
14 bool isEmpty( ) const//是否为空
15 {return theSize-delSize==0;}
16 void printTree( ) const//打印树
17 {printTree(root);}
18
19 void makeEmpty( )//置空
20 {
21 makeEmpty(root);
22 theSize=delSize=0;root=NULL;
23 }
24 void insert( const Object & x )//插入值
25 {insert(x,root);}
26
27 private:
28 struct BinaryNode
29 {
30 Object element;//Object类型的值
31 BinaryNode *left;//左子树
32 BinaryNode *right;//右子树
33 int count; //计数
34
35 BinaryNode( const Object & theElement, BinaryNode *lt, BinaryNode *rt ,int c=1)//第一次插入值为1
36 : element( theElement ), left( lt ), right( rt ), count(c) { }
37 };
38
39 BinaryNode *root;//根结点
40 int theSize;
41 int delSize;
42
43 void insert( const Object & x, BinaryNode * & t ) ;
44
45 bool contains( const Object & x, BinaryNode *t ) const;
46 void makeEmpty( BinaryNode * & t );
47 void printTree( BinaryNode *t ) const
48 {
49 if(t)
50 {
51 printTree(t->left);
52 if(t->count>0)std::cout<<t->element<<" ";
53 printTree(t->right);
54 }
55 }
56 };
1 template <typename Object>
2 bool BSTree<Object>::contains( const Object & x, BinaryNode *t ) const
3 {
4 if(!t)return false;
5 if(x<t->element)
6 return contains(x,t->left);
7 else if(x>t->element)
8 return contains(x,t->right);
9 else if(t->count>0)
10 return true;
11 else
12 return false;
13 }
14
15 template <typename Object>
16 void BSTree<Object>::makeEmpty( BinaryNode * & t )
17 {
18 if(t)
19 {
20 makeEmpty(t->left);
21 makeEmpty(t->right);
22 delete t;
23 }
24 }
25
26 template <typename Object>
27 void BSTree<Object>::insert( const Object & x, BinaryNode * & t )
28 {
29 if(t==NULL)
30 {
31 ++theSize;
32 t=new BinaryNode(x,NULL,NULL);//count默认为1
33 }
34 else if(x<t->element)
35 insert(x,t->left);
36 else if(x>t->element)
37 insert(x,t->right);
38 else if(t->count++==0)
39 --delSize;//如果count的值原本为0,则将其加1的同时要将delSize减1.
40 }
接下来是remove操作,现在remove也显得代价更小,当删除某项数据时,只需要将它的count减1而不需要从树真正删除它.而当它的count为0时,则需要将delSize加1以表示又有一个结点在逻辑上已经被删除.最后再检查delSize是否达到了theSize的一半.如果达到,则执行真正的delete操作.
1 template <typename Object>
2 void BSTree<Object>::remove( const Object & x, BinaryNode * & t )
3 {
4 if(t==NULL)return;
5 if(x<t->element)
6 remove(x,t->left);
7 else if(x>t->element)
8 remove(x,t->right);
9 else if(t->count>0&&--(t->count)==0&&(++delSize>=theSize-delSize))
10 //若count大于0,则减1;若减1后为0,则delSize加1;
11 //若delSize达到theSize的一半,则执行delete操作
12 {
13 delete_nodes(root);//真正的删除操作
14 theSize-=delSize;//更新总结点数
15 delSize=0;//delSize归零
16 }
17 }
现在的问题是delete_nodes( BinaryNode * & t )例程的实现,我们要删除树中所有count为0的结点.我这里使用了递归的方法,如果某个结点不必删除,那么就继续对它的左子树和右子树执行delete_nodes().回想一般二叉查找树的删除,对于叶子结点,直接删除即可,这在delete_nodes()中也是一样.而对于只有一个非空子树的结点,直接删除后将它的非空子树"拼接"上即可,但这时需要对拼接上的子树继续执行delete_nodes().
最后就是删除那些两个子树都非空的结点,我们还是在它的右子树上找到一个值最小的结点(注意:要在count大于0的结点中找),我们假设这个点为min.然后将t的值和count值都替换成min相应的值,同时将min的count值修改为0以保证稍后删除.最后,继续对t的左子树和右子树执行delete_nodes()操作.这里面一个可能存在的情况是t的右子树中所有结点的count值都为0,那么我们可以将它的右子树置空,这样t就变成了一个只有一个非空子树的结点.
在实际的编程中,我觉得比较费脑筋的就是findMin()和findMax()例程.以findMin(BinaryNode *t)为例,首先要找到t中的最小值,然后再升序一步步往上寻找第一个count大于0的结点.我采用的递归的方法,这一例程返回bool值,false表示没有找到最小值,也就意味着t为空或者t中所有结点的count均为0.而为了保存查找到的最小(最大)结点,我给BSTree类增加了两个私有成员,两个BinaryNode *类型的指针min和max.另外,我还提供了两个公有的函数,分别返回整颗树的最小和最大值,但是这两个函数需要在保证整颗树非空的情况下使用.
1 private:
2 BinaryNode *min;
3 BinaryNode *max;
4
5 template <typename Object>
6 bool BSTree<Object>::findMin( BinaryNode *t )
7 {
8 if(t)
9 {
10 if(findMin(t->left))return true;//在t的左子树中找到count大于0的点,查找结束.
11 if(t->count>0){min=t;return true;}//找到count大于0的点,查找结束.
12 return findMin(t->right);//否则继续查找右子树
13 }
14 return false;//空结点则返回false
15 }
16
17 template <typename Object>
18 bool BSTree<Object>::findMax( BinaryNode *t)
19 {
20 if(t)
21 {
22 if(findMax(t->right))return true;
23 if(t->count>0){max=t;return true;}
24 return findMax(t->left);
25 }
26 return false;
27 }
28
29 template <typename Object>
30 void BSTree<Object>::delete_nodes(BinaryNode * & t )
31 {
32 if(t==NULL)return;//空子树,do nothing
33 if(t->count==0)//t需要被删除
34 {
35 if((t->left!=NULL)&&(t->right!=NULL)&&findMin(t->right))
36 //t的两个子树均非空,且在它的右子树中找到了可用的最小结点
37 {
38 t->element=min->element;
39 t->count=min->count;
40 min->count=0;//右子树中的最小结点即将被删除
41 delete_nodes(t->left);
42 delete_nodes(t->right);
43 }
44 else
45 {
46 if((t->left!=NULL)&&(t->right!=NULL))
47 //t的右子树中的所有结点的count均为0,则将其置空.
48 {makeEmpty(t->right);t->right=NULL;}
49 BinaryNode *oldnode=t;
50 t=t->left?t->left:t->right;
51 delete oldnode;
52 if(t!=NULL)delete_nodes(t);//若新的t非空,继续对它执行删除操作
53 }
54 }
55 else//t不需要被删除,继续在它的子树中查找
56 {
57 delete_nodes(t->left);
58 delete_nodes(t->right);
59 }
60 }
61
62 public:
63 const Object & findMin( )//返回最小值
64 {
65 if(!findMin(root))
66 std::cerr<<"The tree is Empty!"<<std::endl;
67 return min->element;
68 }
69
70 const Object & findMax( )//返回最大值
71 {
72 if(!findMax(root))
73 std::cerr<<"The tree is Empty!"<<std::endl;
74 return max->element;
75 }
最后提供我的BSTree.h文件,有不对的地方请指正,谢谢.
BSTree.h