《数据结构与算法分析》伸展树(自底向上)详解

来源:互联网 发布:车削中心的编程视频 编辑:程序博客网 时间:2024/06/05 16:52

前言:

      完成了AVLTree之后,课本又接着讲了伸展树,不过书上就只简单的提及了一下这棵树的特点,并且简单的讲诉了树的伸展操作。没有给出树的定义,也没有给出任何的操作伪代码或者算法流程。在完成了AVLTree之后我信心大增,决定要自行实现这棵树,于是自己在纸上画了画流程图,然后就开始编码了。

结果遇上的第一个问题就是树结构定义上的问题。书本上说伸展树由于每次访问之后会将访问的节点变成根节点,因此不像AVLTree那样需要保存高度信息,节省了储存的空间。但是这样的话,按照课本上给出的旋转方法(需要知道被访问节点的父节点和祖父节点),每次都需要重新遍历一次二叉树确认父亲和祖父节点,那么这样复杂度上升了太多。如果保存一个父亲节点的话,那么和AVLtree不就变得相同了吗?带着这个问题编码,果然编码的一塌糊涂,最后都不知道自己在写什么了可怜,果然要先完全想清楚再开始写代码敲打

然后呢,没辙,只能上网搜一搜别人的博客,看看别人是怎么实现这个问题的吧,阅读了博客之后才发现有两种实现Splay操作的方式,一种是子顶向下,这样的方式最简单,并且不需要保存父节点信息,我将在下一篇博客中叙述。另一种就是书上给出的自底向上了,因为必须要访问父亲节点,所以树成员中一定需要保存父亲节点的指针。

参考的博客:

 自底向上:

http://www.cnblogs.com/vamei/archive/2013/03/24/2976545.html。我从这一篇博客中学习了不少的新的思想,比如我最开始总想着利用一个函数递归完成插入或者删除操作,却发现其中有一部分的操作不是每个节点都需要的,这个时候可以把删除操作再分出一个子函数来,需要递归的部分由这个子函数完成就行了。这样使得编码简单很多并且易于阅读。

自顶向下:

http://www.cnblogs.com/kernel_hcy/archive/2010/03/17/1688360.html 。这一篇博客写的非常的详细,并且给出了贴图详细讲诉旋转的过程,来源是sedgewick大神的《算法》一书。我根据给出的流程图和伪代码,完全理解之后,用一个小时完成了编码,并且一次测试通过。

我的github:

我实现的代码全部贴在我的github中,欢迎大家去参观。

https://github.com/YinWenAtBIT

介绍:

定义:

         伸展树,或者叫自适应查找树,是一种用于保存有序集合的简单高效的数据结构。伸展树实质上是一个二叉查找树。允许查找,插入,删除,删除最小,删除最大,分割,合并等许多操作,这些操作的时间复杂度为O(logN)。由于伸展树可以适应需求序列,因此他们的性能在实际应用中更优秀。
伸展树支持所有的二叉树操作。伸展树不保证最坏情况下的时间复杂度为O(logN)。伸展树的时间复杂度边界是均摊的。尽管一个单独的操作可能很耗时,但对于一个任意的操作序列,时间复杂度可以保证为O(logN)。
AVLTree的缺点:
1、平衡查找树每个节点都需要保存额外的信息(自底向上的实现方式同样保存了额外信息)。
2、难于实现,因此插入和删除操作复杂度高,且是潜在的错误点。
3、对于简单的输入,性能并没有什么提高。
平衡查找树可以提高性能的地方:
1、平衡查找树在最差、平均和最坏情况下的时间复杂度在本质上是相同的。
2、对一个节点的访问,如果第二次访问的时间小于第一次访问,将是非常好的事情。
3、90-10法则。在实际情况中,90%的访问发生在10%的数据上。
4、处理好那90%的情况就很好了。

自底向上的旋转:

在伸展树中,被访问的节点是需要变成新树的根节点的,那么问题的关键就是如何把这棵树通过旋转变成我们想要的样子了。

1、zig情况。
    X是查找路径上我们需要旋转的一个非根节点。
    如果X的父节点是根,那么我们用下图所示的方法旋转X到根:


2、zig-zag情况。
在这种情况中,X有一个父节点P和祖父节点G(P的父节点)。X是右子节点,P是左子节点,或者反过来。这个就是双旋转。
先是X绕P左旋转,再接着X绕G右旋转。
如图所示:


3、zig-zig情况。
    这和前一个旋转不同。在这种情况中,X和P都是左子节点或右子节点。
    先是P绕G右旋转,接着X绕P右旋转。
    如图所示:


同样在右边有对称的三种情况,一共有6种情况。有了这些分类,我们就可以写出Splay操作的伪代码了。

伪代码:

P(X) : 获得X的父节点,G(X) : 获得X的祖父节点(=P(P(X)))。    Function Buttom-up-splay:        Do            If X 是 P(X) 的左子结点 Then                If G(X) 为空 Then                    X 绕 P(X)右旋                Else If P(X)是G(X)的左子结点                    P(X) 绕G(X)右旋                    X 绕P(X)右旋                Else                    X绕P(X)右旋                    X绕P(X)左旋 (P(X)和上面一句的不同,是原来的G(X))                Endif            Else If X 是 P(X) 的右子结点 Then                If G(X) 为空 Then                    X 绕 P(X)左旋                Else If P(X)是G(X)的右子结点                    P(X) 绕G(X)左旋                    X 绕P(X)左旋                Else                    X绕P(X)左旋                    X绕P(X)右旋 (P(X)和上面一句的不同,是原来的G(X))                Endif             Endif        While (P(X) != NULL)    EndFunction
按照第二篇博客给出的方法,这个过程其实可以简化,因为左子节点的操作都有X扰P(X)右旋的操作,右子节点都有X扰P(X)左旋的操作。这里我的编码还是按照6种分类来写的,便于理解整个过程。简化的方法大家可以自行尝试。

树结构:

因为Splay操作需要知道转为树根点的父亲节点和祖父节点,所以自底向上的实现方式需要保持父亲指针。

struct SplayNode{ElementType Element;SplayTree Parent;SplayTree Left;SplayTree Right;};

 操作:

伸展:

 伸展操作就按照如上的伪代码实现,需要把变为根节点的指针和当前根节点指针输入。

SplayTree Splay(Position np, SplayTree T){Position P,G;P = np->Parent;while(P != NULL){G = P->Parent;/*X是父亲的左节点*/if(np == P->Left){/*X的父亲节点为根节点*/if(G == NULL)RightSingleRotate(P);/*zigzig形状*/else if(G->Left == P){RightSingleRotate(G);/*第一次旋转完成之后,P变成了G的父节点*/RightSingleRotate(P);}/*zagzig形状*/elseLeftDoubleRotate(G);}/*/*X是父亲的右节点*/else{/*X的父亲节点为根节点*/if(G == NULL)LeftSingleRotate(P);/*zagzag形状*/else if(G->Right == P){LeftSingleRotate(G);/*第一次旋转完成之后,P变成了G的父节点*/LeftSingleRotate(P);}/*zagzig形状*/elseRightDoubleRotate(G);}P= np->Parent;}/*while*//*此时np已经变成根节点*/return np;}

左旋/右旋:

 新的左旋右旋代码需要加上对父亲节点的操作,特别注意传入的节点的父亲节点也要考虑到。加上了父亲节点之后操作变得很复杂,需要非常小心。

SplayTree RightSingleRotate(SplayTree T){SplayTree k1, parent;parent = T ->Parent;k1 = T->Left;k1->Parent = parent;/*连接k1的右子树*/if(k1->Right)k1->Right->Parent = T;T->Left = k1->Right;/*调换k1和T的位置*/T->Parent = k1;k1->Right = T;/*连接上一个节点*/if(parent){if(parent->Left ==T)parent->Left = k1;elseparent->Right = k1;}return k1;}SplayTree LeftSingleRotate(SplayTree k1){SplayTree k2, parent;parent = k1 ->Parent;k2 = k1->Right;k2->Parent = parent;/*连接k2的左子树*/if(k2->Left)k2->Left->Parent = k1;k1->Right = k2->Left;/*调换k1和T的位置*/k1->Parent = k2;k2->Left = k1;/*连接上一个节点*/if(parent){if(parent->Left ==k1)parent->Left = k2;elseparent->Right = k2;}return k2;}

最大/小值:

 寻找最大最小值可以通过非递归的方式找到最后一个节点,然后调用Splay函数将该节点变成根节点即可。

SplayTree FindMin(SplayTree T){Position np = T;if(T!=NULL){while(np->Left !=NULL)np = np->Left;return Splay(np, T);}return NULL;}/*找到最大值之后将该值变为根节点*/SplayTree FindMax(SplayTree T){Position np = T;if(T!=NULL){while(np->Right !=NULL)np = np->Right;return Splay(np, T);}return NULL;}

搜索:

我这里使用的搜索方式是递归搜索,因为最后需要调用Splay函数,因此将搜索函数拆开了,便于阅读。

SplayTree Find(ElementType X, SplayTree T){/*空树直接返回*/if(T==NULL){fprintf(stderr, "empty tree");return T;}Position np = searchValue(X, T);/*不保证最后返回的元素等于X,需要调用者检查,不为根节点则进行splay操作*/if(np != T) return Splay(np, T);/*若为根节点,直接返回*/elsereturn T;}SplayTree searchValue(ElementType X, SplayTree T){/*X小于当前元素且有左孩子时*/if(X<T->Element && T->Left)return searchValue(X, T->Left);/*X大于当前元素且有右孩子时*/else if(X >T->Element && T->Right)return searchValue(X, T->Right);/*X等于当前元素,或者没找到该元素,又没有左右孩子时*/elsereturn T;}

删除:

我在删除的处理上使用了上一个Find函数,这样如果被删除的数据存在,那么它就在根节点上。再根据它是否有左右两个孩子来确定删除策略。

SplayTree Delete(ElementType X, SplayTree T){if(T == NULL){fprintf(stderr, "%d don't exist", X);}T =Find(X, T);/*要寻找的值已经变成根节点,如果相等则找到,不相等则不存在*/if(X == T->Element){/*删除根,剩下两个子树L(左子树)和R(右子树)。使用FindMax查找L的最大节点,此时,L的根没有右子树,使R成为L的根的右子树*/if(T->Left && T->Right){Position Ltemp, Rtemp;Ltemp = T->Left;Rtemp = T->Right;/*左右节点的父亲节点设置为NULL*/Ltemp->Parent = Rtemp->Parent =NULL;free(T);Ltemp = FindMax(Ltemp);Ltemp->Right = Rtemp;Rtemp->Parent = Ltemp;return Ltemp;}else if(T->Left){Position Ltemp = T->Left;Ltemp ->Parent =NULL;free(T);return Ltemp;}else{Position Rtemp = T->Right;Rtemp ->Parent =NULL;free(T);return Rtemp;}}else{fprintf(stderr, "%d do not exist", X);return T;}}

插入:

我在插入操作上的实现其实是过于复杂的,在这里我使用的方式与最普通的搜索二叉树相同,先找到要插入的叶子节点,再执行插入,再调用Splay函数。这个函数我编码到一半的时候就已经意识到实现太复杂了,不过考虑到这次的插入操作使用非递归的方式实现,打算试一试自己的是否能编写成功,就完成了编写,最后调试时一次通过。让我很满意大笑。(简单的方法是先调用Find,后面就很简单了)

/*插入功能实现的方式是先非递归的插入,再进行Splay操作,其实过于复杂,可以优化,先进行Splay操作,然后再把新值作为根节点和返回的树合并就可以*/SplayTree insert(ElementType X, SplayTree T){/*空树时直接创建新的树*/if(T == NULL){T = (SplayTree)malloc(sizeof(struct SplayNode));if(T == NULL){fprintf(stderr, "not enough memory");exit(1);}T->Element = X;T->Left = T->Right =T->Parent = NULL;return T;}/*X为根节点时直接返回*/if(X == T->Element)return T;/*X不为根节点,寻找到X之后进行Splay操作*/else{Position tempP = T;Position temp = T;while(temp != NULL){if(X < temp->Element){tempP =temp;temp = temp->Left;}else if(X >temp->Element){tempP= temp;temp = temp->Right;}//此时X等于temp的成员值elsebreak;}/*节点中不存在X*/if(temp == NULL){Position np= (SplayTree)malloc(sizeof(struct SplayNode));if(np == NULL){fprintf(stderr, "not enough memory");exit(1);}np->Element = X;np->Left = np->Right = NULL;np->Parent =tempP;if(X<tempP->Element)tempP->Left = np;elsetempP->Right = np;/*准备调到根节点*/temp = np;}T = Splay(temp, T);}return T;}

总结:

这一次的伸展树学习,主要是通过别人的博客,自己仔仔细细阅读了十几篇博客才总算清楚的弄明白整个过程。花了整整两天。这个过程也让我感受到写博客的难点。博客的定位很重要,有些人的博客上自贴出了代码,加上简单几行的描述,这样的博客对于没有阅读过相关教材的人太过简单,是铁定的看不懂的。只有找到了那种详细图解,仔细论证的博客,才能再阅读几遍后理解整个过程(还是老外的教材写的好,读教材基本一遍就懂了)。

所以我想以后自己的博客也要加上对于问题的简单介绍,不一定非要特别的详细,但是这个部分一定不能缺,只有这样的博客才能真正起到帮助别人的作用。


1 0
原创粉丝点击