伸展树
伸展树也是平衡二叉搜索树的一种形式。相对于AVL树,伸展树的实现更为简捷。
伸展树无需时刻都严格地保持全树的平衡,但却能够在任何足够长的真实操作序列中,保持分摊意义上的高效率。
伸展树也不需要对基本的二叉树节点结构,做任何附加的要求或改动,更不需要记录平衡因子或高度之类的额外信息,故适用范围更广。
数据局部
1)刚刚被访问过的元素, 极有可能在不久之后再次被访问到;
2)将被访问的下一元素 ,极有可能就处于不久之前被访问过的某个元素的附近。
二叉搜索树而言,数据局部性具体表现为:
1)刚刚被访问过的 节点,极有可能在不久之后再次被访问到;
2)将被访问的下一 节点,极有可能就处于不久之前被访问过的某个节点的附近。
伸展树:逐层伸展
每访问过一个节点之后,随即反复地以它的父节点为轴,经适当的旋转将其提升一层,直至最终成为树根。
随着节点E的逐层上升,两侧子树的结构也不断地调整,故这一过程也形象地称作伸展。
若节点总数为n
,则旋转操作的总次数应为:
( n − 1 ) + { ( n − 1 ) + ( n − 2 ) + . . . + 1 } = ( n 2 + n − 2 ) / 2 = O ( n 2 ) (n - 1) + \{ (n - 1) + (n - 2) + ... + 1 \} = (n ^2 + n - 2)/2 = \mathcal O (n^2 ) (n−1)+{(n−1)+(n−2)+...+1}=(n2+n−2)/2=O(n2)如此分摊下来,每次访问平均需要O(n)
时间。
伸展树:双层伸展
将逐层伸展改为双层伸展。具体地,每次都从当前节点v
向上追溯两层(而不是仅一层),并根据其父亲p
以及祖父g
的相对位置,进行相应的旋转。
zig-zig / zag-zag
设v
是p
的左孩子,且p
也是g
的左孩子;设W
和X
分别是v
的左、右子树,Y
和Z
分别是p
和g
的右子树。
通过 zig-zig 操作,将节点 v
上推两局。
zig-zag / zag-zig
设v
是p
的左孩子,而p
是g
的右孩子;设W
是g
的左子树,X
和Y
分别是v
的左、右子树,Z
是p
的右子树
通过 zig-zag 操作,将节点 v
上推两局。
zig / zag
若v
最初的深度为奇数,则经过若干次双层调整至最后一次调整时,v
的父亲p
即是树根r
。将v
的左、右子树记作X
和Y
,节点p = r
的另一子树记作Z
。
通过 zig 操作,将节点 v
上推一局,成为树根。
双层伸展策略优于逐层伸展策略的关键在于 zig-zig / zag-zag
。
效果与效率
每经过一次双层调整操作,节点v
都会上升两层。
若v
的初始深度depth(v)
为偶数,则最终v
将上升至树根;
若depth(v)
为奇数,则当v
上升至深度为1
时,最后再相应地做一次zig或zag单旋操作,v
上升至树根。
无论如何,经过depth(v)
次旋转后,v
最终总能成为树根。
就树的形态而言,双层伸展策略可"智能"地"折叠"被访问的子树分支,从而有效地避免对长分支的连续访问。这就意味着,即便节点v的深度为O(n)
,双层伸展策略既可将v
推至树根,亦可令对应分支的长度以几何级数(大致折半)的速度收缩。单次操作均可在分摊的O(logn)
时间内完成
伸展树:算法实现
伸展树接口定义
cpp
#include "../BST/BST.h" //基于BST实现splay
template <typename T> class Splay : public BST<T>{ //由BST派生的Splay树模板类
protected:
BinNodePosi(T) splay ( BinNodePosi(T) v ); //将节点v伸展至根
public:
BinNodePosi(T) & search ( const T& e ); //查找(重写)
BinNodePosi(T) insert ( const T&e ); //插入(重写)
bool remove ( const T& e ); //删除(重写)
};
伸展算法
cpp
template <typename NodePosi> inline //在节点*p与*lc((可能为空)之间建立父(左)子关系
void attachAsLChild ( NodePosi p,NodePosi lc ) { p->lc = lc; if ( lc ) lc->parent = p; }
template <typename NodePosis inline //在节点*p与*rc(可能为空)之间建立父(右)子关系
void attachAsRChild ( NodePosi p,NodePosi rc ) { p->rc = rc; if ( rc ) rc->parent = p; }
template <typename T> //splay树伸展算法:从节点v出发逐层伸展
BinNodePosi(T) Splay<T>::splay ( BinNodePosi(T)v ) { //v为因最近访问而需伸展的节点位置
if ( !v ) return NULL; BinNodePosi(T) p; BinNodePosi(T) g;//*v的父亲与祖父
while ( ( p = v->parent ) &8 ( g = p->parent ) ){ //自下而上,反复对*v做双层伸展
BinNodePosi(T) gg = g->parent;//每轮之后*v都以原曾祖父( great-grand parent )为父if ( IsLChild ( *v ) )
if ( IsLChild ( *p ) ) { //zig-zig
attachAsLChild ( g, p->rc ); attachAsLChild ( p, v->rc );
attachAsRChild ( p, g ); attachAsRChild ( v, p );
}else { llzig-zag
attachAsLChild ( p, v->rc ); attachAsRChild ( g, v->1c );
attachAsLChild ( v, g ); attachAsRChild ( v,p );
else if ( IsRChild ( *p ) ) { //zag-zag
attachAsRChild ( g, p->lc ); attachAsRChild ( p, v->lc );
attachAsLChild ( p, g ); attachAsLChild ( v, p );
}else { //zag-zig
attachAsRChild ( p, v->1c ); attachAsLChild ( g, v->rc );
attachAsRChild ( v,g ); attachAsLChild ( v, p );
}
if ( !gg ) v->parent = NULL; //若*v原先的曾祖父*gg不存在,则*v现在应为树根
else //否则,*gg此后应该以*v作为左或右孩子
( g == gg->lc ) ? attachAsLChild ( gg, v ) : attachAsRChild ( gg, v );updateHeight ( g );
updateHeight ( p ); updateHeight ( v );
} //双层伸展结束时,必有g == NULL,但p可能非空
if ( p = v->parent ) { //若p果真非空,则额外再做一次单旋
if ( IsLChild ( *v ) )
{ attachAsLChild ( p, v->rc ); attachAsRChild ( v, p ); }
else
{ attachAsRChild ( p, v->lc ); attachAsLChild ( v, p ); }
updateHeight ( p ); updateHeight ( v );
}
v->parent = NULL; return v;
}//调整之后新树根应为被伸展的节点,故返回该节点的位置以便上层函数更新树根
查找算法
cpp
//伸展树中查找任一关键码e的过程
template typename T> BinNodePosi(T) & Splay<T>::search ( const T& e ) { //在伸展树中查找e
BinNodePosi(T) p = searchIn ( _root,e,_hot = NULL );
_root = splay ( p ? p : _hot ); //将最后一个被访问的节点伸展至根
return _root;
} //与其它BST不同,无论查找成功与否,_root都指向最后被访问的节点
调用二叉搜索树的通用算法searchIn()
尝试查找具有关键码e
的节点。无论查找是否成功,都继而调用splay()
算法,将查找终止位置处的节点伸展到树根。
插入算法
首先调用伸展树查找接口Splay::search(e)
,查找该关键码(图(a))。于是,其中最后被访问的节点t
,将通过伸展被提升为树根,其左、右子树分别记作TL
和TR
(图(b))。、接下来,根据e
与t
的大小关系(不妨排除二者相等的情况),以t
为界将T
分裂为两棵子树。比如,不失一般性地设e
大于t
。于是,可切断t与其右孩子之间的联系(图©),再将以e
为关键码的新节点v
作为树根,并以t
作为其左孩子,以TR
作为其右子树(图(d))。
cpp
template <typename T> BinNodePosi(T) Splay<T>::insert ( const T& e ) { //将关键码e插入伸展树中
if ( !_root ) i _size++; return _root = new BinNode<T> ( e );} //处理原树为空的退化情况
if ( e == search ( e )->data ) return _root; //确认目标节点不存在
_size++; BinNodePosi(T) t = _root; //创建新节点。以下调整<=7个指针以完成局部重构
if ( _root->data < e ) {//插入新根,以t和t->rc为左、右孩子
t->parent = _root = new BinNode<T> ( e,NULL,t, t->rc ); //2 + 3个
if ( HasRChild ( *t ) ) { t->rc->parent = _root; t->rc = NULL; } //<= 2个
}else {//插入新根,以t->1c和t为左、右孩子
t->parent = _root = new BinNode<T> ( e,NULL, t->lc, t ); //2 + 3个
if ( HasLChild ( *t ) ) { t->lc->parent = _root; t->lc = NJLL; } //<= 2个
}
updateHeightAbove ( t ); //更新t及其祖先(实际上只有_root一个)的高度
return _root; //新节点必然置于树根,返回之
} //无论e是否存在于原树中,返回时总有_root->data == e
删除算法
在实施删除操作之前,通常都需要调用Splay::search()
定位目标节点,而该接口已经集成了splay()
伸展功能,从而使得在成功返回后,树根节点恰好就是待删除节点。
cpp
template <typename T> bool Splay<T>: : remove ( const T& e ) { //从伸展树中删除关键码e
if ( !_root ll ( e != search ( e )->data ) ) return false; //若树空或目标不存在,则无法删除
BinNodePosi(T) w = _root; //assert:经search()后节点e已被伸展至树根
if ( !HasLChild ( *_root ) ) {//若无左子树,则直接删除
_root = _root->rc; if ( _root ) _root->parent = NULL;
}else if ( !HasRChild ( *_root ) ) {//若无右子树,也直接删除
_root = _root->lc; if ( _root ) _root->parent = NULL;
} else { //若左右子树同时存在,则
BinNodePosi(T) lTree = _root->lc;
lTree->parent = NULL; _root->lc = NULL; //暂时将左子树切除
_root = _root->rc; _root->parent = NULL; //只保留右子树
search ( w->data ); //以原树根为目标,做一次(必定失败的)查找
/ assert: 至此,右子树中最小节点必伸展至根,且(因无雷同节点)其左子树必空,于是
_root->lc = lTree; lTree->parent = _root;//只需将原左子树接回原位即可
}
release ( w->data ); release ( w ); _size--; //释放节点,更新规模
if ( _root ) updateHeight ( _root ); //此后,若树非空,则树根的高度需要更新return true;//返回成功标志
}//若目标节点存在且被删除,返回true ;否则返回false
首先调用接口Splay::search(e)
,查找该关键码,且不妨设命中节点为v
(图(a))。于是,v
将随即通过伸展被提升为树根,其左、右子树分别记作TL
和TR
(图(b))。接下来,将v
摘除(图(c ))。
然后,在TR
中再次查找关键码e
。尽管这一查找注定失败,却可以将TR
中的最小节点m
,伸展提升为该子树的根。得益于二叉搜索树的顺序性,此时节点m
的左子树必然为空;同时,TL
中所有节点都应小于m
(图(d))。于是,只需将 TL
作为左子树与m
相互联接,即可得到一棵完整的二叉搜索树(图(e))。如此不仅删除了v
,而且既然新树根m
在原树中是v
的直接后继,故数据局部性也得到了利用。