数据结构和算法(9):伸展树

伸展树

伸展树也是平衡二叉搜索树的一种形式。相对于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

vp的左孩子,且p也是g的左孩子;设WX分别是v的左、右子树,YZ分别是pg的右子树。

通过 zig-zig 操作,将节点 v 上推两局。

zig-zag / zag-zig

vp的左孩子,而pg的右孩子;设Wg的左子树,XY分别是v的左、右子树,Zp的右子树

通过 zig-zag 操作,将节点 v 上推两局。

zig / zag

v最初的深度为奇数,则经过若干次双层调整至最后一次调整时,v的父亲p即是树根r。将v的左、右子树记作XY,节点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,将通过伸展被提升为树根,其左、右子树分别记作TLTR(图(b))。、接下来,根据et的大小关系(不妨排除二者相等的情况),以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将随即通过伸展被提升为树根,其左、右子树分别记作TLTR(图(b))。接下来,将v摘除(图(c ))。

然后,在TR 中再次查找关键码e。尽管这一查找注定失败,却可以将TR 中的最小节点m,伸展提升为该子树的根。得益于二叉搜索树的顺序性,此时节点m的左子树必然为空;同时,TL 中所有节点都应小于m(图(d))。于是,只需将 TL 作为左子树与m相互联接,即可得到一棵完整的二叉搜索树(图(e))。如此不仅删除了v,而且既然新树根m在原树中是v的直接后继,故数据局部性也得到了利用。

相关推荐
乐之者v3 分钟前
leetCode43.字符串相乘
java·数据结构·算法
A懿轩A1 小时前
C/C++ 数据结构与算法【数组】 数组详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·数组
古希腊掌管学习的神1 小时前
[搜广推]王树森推荐系统——矩阵补充&最近邻查找
python·算法·机器学习·矩阵
云边有个稻草人1 小时前
【优选算法】—复写零(双指针算法)
笔记·算法·双指针算法
半盏茶香1 小时前
在21世纪的我用C语言探寻世界本质 ——编译和链接(编译环境和运行环境)
c语言·开发语言·c++·算法
忘梓.2 小时前
解锁动态规划的奥秘:从零到精通的创新思维解析(3)
算法·动态规划
️南城丶北离2 小时前
[数据结构]图——C++描述
数据结构··最小生成树·最短路径·aov网络·aoe网络
✿ ༺ ོIT技术༻2 小时前
C++11:新特性&右值引用&移动语义
linux·数据结构·c++
tinker在coding4 小时前
Coding Caprice - Linked-List 1
算法·leetcode
XH华8 小时前
初识C语言之二维数组(下)
c语言·算法