引言:
假设想要对一个二叉查找树执行一系列的查找操作。为了使整个查找时间更小,被查频率高的那些条目就应当经常处于靠近树根的位置。于是想到设计一个简单方法,在每次查找之后对树进行重构,把被查找的条目搬移到离树根近一些的地方。伸展树应运而生。
(由于没有时间对自己搭建的博客进行维护,所以将自己之前的博客全部搬过来,备忘)
概念:
【伸展树】:保证从空树开始任意连续M次对树的操作最多花费O(MlogN)时间。一次展开树操作的摊还时间界为O(logN)。
伸展树的基本想法是:当一个节点被访问后,它就要经过一系列的AVL树的旋转被放到根上。并且伸展树不要求保留高度或平衡信息,因此它在某种程度上节省空间并简化代码。
伸展树的概念要求我们对伸展树操作后需要进行展开来恢复其性质。(我们也可以像之前一样进行若干次单旋转来恢复性质,但是这样将被访问的节点移到根处的同时也将另外的节点几乎推向和被访问节点以前一样的深度。因此我们采用不同于之前旋转的展开操作)
【展开】:思路类似于前面的旋转,只是在旋转的实施上有所不同。伸展树判断是什么类型的旋转一般需要看访问路径上的三个节点就可以了。(旋转是沿着访问路径(也就是从根开始到需要访问的节点)进行的。)
伸展树的展开操作分为自底向上展开和自顶向下展开两种方式。
- 自底向上展开
需要先从根沿树往下的一次遍历,先找到需要操作的节点或者相近节点(若被访问节点不在树中)而且需要通过保存父指针或者将访问路径存储到一个栈中来完成后续的展开操作,伴随有大量的开销,程序设计也不简单。因此我们这里采用自顶向下的展开方式。
- 自顶向下展开
这种伸展方式可以在搜索的过程中同时完成splay操作。会把树切成三份,L树,M树,R树,其中M树维护当前还未被访问到的节点,L树中所有节点的值都小于M树中的任何节点值,R树中所有节点的值都大于M树中的任何节点值。考虑的情况有:单旋转,“一字型”旋转,“之字形”旋转。我们减少格外空间的使用:在访问的任一时刻,我们都有一个当前节点X,它是其子树的根;被表示为中间树(即M树)。初始时,X为T的根,而R和L是空树。
单旋转:
一字型:
之字形:双旋转
因为之字形旋转本质上是没有旋转被执行的,只是拆分组合,所以我们可以将其化简。
伸展树的Find操作、Insert操作和Remove操作都涉及到树的展开,展开操作不仅将访问的节点移动到根处,而且还将访问路径上的大部分节点的深度大致减少一半的效果(某些浅的节点最多向下推后两个层次),在查找期间执行展开操作,如果一项也没有找到,那么就要对访问路径上的最后的节点实施一次展开。
程序:
#include "splay.h"
#include <stdlib.h>
#include "fatal.h"
struct SplayNode//SplayNode是结构体,SplayTree是指向SplayNode结构体的指针
{
ElementType Element;
SplayTree Left;
SplayTree Right;
};
typedef struct SplayNode *Position;
static Position NullNode = NULL; /* Needs initialization *///静态全局变量,方便后续节点的初始化
SplayTree
Initialize( void )
{
if( NullNode == NULL )
{
NullNode = malloc( sizeof( struct SplayNode ) );
if( NullNode == NULL )
FatalError( "Out of space!!!" );
NullNode->Left = NullNode->Right = NullNode;
}
return NullNode;
}
static SplayTree Splay( ElementType Item, Position X );
SplayTree
MakeEmpty( SplayTree T )
{
if( T != NullNode )
{
MakeEmpty( T->Left );
MakeEmpty( T->Right );
free( T );
}
return NullNode;
}
void
PrintTree( SplayTree T )
{
if( T != NullNode )
{
PrintTree( T->Left );
printf( "%d ", T->Element );
PrintTree( T->Right );
}
}
SplayTree
Find( ElementType X, SplayTree T )
{
return Splay( X, T );
}
SplayTree
FindMin( SplayTree T )
{
return Splay( NegInfinity, T );
}
SplayTree
FindMax( SplayTree T )
{
return Splay( Infinity, T );
}
/* This function can be called only if K2 has a left child */
/* Perform a rotate between a node (K2) and its left child */
/* Update heights, then return new root */
static Position
SingleRotateWithLeft( Position K2 )//单旋转,和AVL树一样
{
Position K1;
K1 = K2->Left;
K2->Left = K1->Right;
K1->Right = K2;
return K1; /* New root */
}
/* This function can be called only if K1 has a right child */
/* Perform a rotate between a node (K1) and its right child */
/* Update heights, then return new root */
static Position
SingleRotateWithRight( Position K1 )
{
Position K2;
K2 = K1->Right;
K1->Right = K2->Left;
K2->Left = K1;
return K2; /* New root */
}
SplayTree
Splay( ElementType Item, Position X )
{
static struct SplayNode Header;//静态结构体变量
Position LeftTreeMax, RightTreeMin;
Header.Left = Header.Right = NullNode;//初始化Header.Left和Header.Right
LeftTreeMax = RightTreeMin = &Header;//LeftTreeMax、RightTreeMin是Header的引用,但是只会对Header赋值一次,因为后面LeftTreeMax、RightTreeMin指针的地址发生了变化(不断的下移)
NullNode->Element = Item;//这里注意将 NullNode->Element值赋为Item。 NullNode是静态全局变量,我们用 NullNode来表示一个NULL指针。
//当不断向下直到叶子节点时,叶子节点的左或右子树均为空,而我们将空节点的元素值赋值为Item,所以当判断到空节点时,就会Break了
while( Item != X->Element )
{
if( Item < X->Element )
{
if( Item < X->Left->Element )
X = SingleRotateWithLeft( X );
if( X->Left == NullNode )
break;
/* Link right *///以下两句主要是将RightTreeMin这个指针不断的下移
RightTreeMin->Left = X;
RightTreeMin = X;//RightTreeMin指针的地址发生了变化
X = X->Left;
}
else
{
if( Item > X->Right->Element )
X = SingleRotateWithRight( X );
if( X->Right == NullNode )
break;
/* Link left */
LeftTreeMax->Right = X;
LeftTreeMax = X;//LeftTreeMax指针的地址发生了变化
X = X->Right;
}
} /* while Item != X->Element */
/* Reassemble */
LeftTreeMax->Right = X->Left;//x的left在左子树中仍然是最大的,所以合并时,X->Left被放在左边树的最大左子树的右子树位置上
RightTreeMin->Left = X->Right;
X->Left = Header.Right;
X->Right = Header.Left;
return X;
}
SplayTree
Insert( ElementType Item, SplayTree T )
{
static Position NewNode = NULL;
if( NewNode == NULL )
{
NewNode = malloc( sizeof( struct SplayNode ) );
if( NewNode == NULL )
FatalError( "Out of space!!!" );
}
NewNode->Element = Item;
if( T == NullNode )//T为NULL直接插入
{
NewNode->Left = NewNode->Right = NullNode;
T = NewNode;
}
else
{
T = Splay( Item, T );
if( Item < T->Element )
{
NewNode->Left = T->Left;
NewNode->Right = T;
T->Left = NullNode;
T = NewNode;
}
else
if( T->Element < Item )
{
NewNode->Right = T->Right;
NewNode->Left = T;
T->Right = NullNode;
T = NewNode;
}
else
return T; /* Already in the tree */
}
NewNode = NULL; /* So next insert will call malloc */
return T;
}
/* END */
/* START: fig12_8.txt */
SplayTree
Remove( ElementType Item, SplayTree T )
{
Position NewTree;
if( T != NullNode )
{
T = Splay( Item, T );//要删除Item,就需要将树T展开
if( Item == T->Element )
{
/* Found it! */
if( T->Left == NullNode )//如果左子树为空,则将Item删除,新树就为右子树。
NewTree = T->Right;
else
{
NewTree = T->Left;
NewTree = Splay( Item, NewTree );//左子树不为空,通过Splay把左子树中的最大的节点放到根处(因为左子树中最大节点没有右子树方便后续处理),新树的右子树任然是原树的右子树。
NewTree->Right = T->Right;
}
free( T );
T = NewTree;
}
}
return T;
}
/* END */
ElementType
Retrieve( SplayTree T )
{
return T->Element;
}
完整程序代码可以从https://github.com/mazilaile下载。
程序分析:
我们结合实例来分析关键程序代码 SplayTree Splay( ElementType Item, Position X )。
访问元素19:
初始:
调用函数 SplayTree Splay( ElementType Item, Position X )此时,Item为19,X指向树根节点12。初始化Header节点、LeftTreeMax和RightTreeMin指针。
1:
开始判断Item不等于位置X处的元素,Item大于X位置处的元素,Item小于X->Right位置上的元素,所以进行简化的之字形旋转。 LeftTreeMax->Right指向树的根节点12(即Header.Right指向树的根节点12)。LeftTreeMax指针下移,指向Header.Right。X指针下移指向X->Right(即25)。
2:
再次进行判断Item的值不等于X位置处元素的值,Item元素值小于X位置处的元素,Item小于X->Left位置上的元素,所以进行一字型旋转,而一字型旋转在程序中的实现分为两步,第一步是对X进行单旋转,第二步是进行拆分。X=SingleRotateWithLeft( 25 );单旋转过后X指向20。
单旋转结果如图:
再进行拆分:RightTreeMin->Left 指向20(即Header.Left指向20),RightTreeMin指针下移,指向Header.Left(20)。【这里注意,右边树R的指针RightTreeMin并没有一直向下移动到右边树的叶子节点,这是因为沿访问路径从上向下一直到达被访问节点,接到左边树上的元素一定比左边树已有的元素都大,而接到右边树上的元素一定比右边树已有的元素都大。因此,我们一直将小于被访问节点的子树接到左边树的右子树上,将大于被访问节点的子树接到右边树的左子树上】X指针下移指向X->Left(即15)。
3:
再次进行判断Item的值不等于X位置处元素的值,Item元素值大于X位置处的元素,Item大于X->Right位置上的元素,所以进行右单旋转【这里需要注意,单旋转和一字型旋转第一步都是进行单旋转,只是在第二步时单旋转经过判断不需要进行拆分】。
X=SingleRotateWithRight( 15 );旋转完后X指向18。
然后判断X->Right == NullNode,所以Break。
4:
最后进行合并,将 LeftTreeMax->Right = X->Left; //x的left在左子树中仍然是最大的,所以合并时,X->Left被放在左边树的最大左子树的右子树位置上
RightTreeMin->Left = X->Right;
X->Left = Header.Right; //因为Header.Right之前指向左边树。
X->Right = Header.Left;