第8章 高级搜索树
8.1 伸展树
与前一章的AVL树一样,伸展树(splay tree)也是平衡二叉搜索树的一种形式。相当于前者,后者的实现更为简捷。伸展树无需时刻都严格地保持全树的平衡,但却能够在任何足够长的真实操作序列中,保持分摊意义上的高效率。伸展树也不需要对基本的二叉树节点结构,做任何附加的要求或改动,更不需要记录平衡因子或高度之类的额外信息,故适用范围更广。
8.1.1 局部性
“数据局部性”(data locality),这包括两个方面的含义:
- 刚刚被访问过的元素,极有可能在不久之后再次被访问到
- 将被访问的下一元素,极有可能就处于不久之前被访问过的某个元素的附近
就二叉搜索树而言,数据局部性具体表现为:
- 刚刚被访问过的节点,极有可能在不久之后再次被访问到
- 将被访问的下一节点,极有可能就处于不久之前被访问过的某个节点的附近
8.1.2 逐层伸展
-
简易伸展树
一种直接方式是:每访问过一个节点之后,随即反复地以它的父节点为轴,经适当的旋转将其提升一层,直至最终成为树根。
-
最坏情况
如此分摊下来,每次访问平均需要Ω(n)时间。很遗憾,这一效率不仅远远低于AVL树,而且甚至与原始的二叉搜索树的最坏情况相当。
而且,图8.2(a)与(f)中二叉搜索树的结构完全相同!这意味着以上情况可以持续地再现!
8.1.3 双层伸展
为克服上述伸展调整策略的缺陷,一种简便且有效的方法就是:将逐层伸展改为双层伸展。具体地,每次都从当前节点v向上追溯两层(而不是仅一层),并根据其父亲p以及祖父g的相对位置,进行相应的旋转。以下分三类情况,分别介绍具体的处理方法。
-
zig-zig/zag-zag
-
zig-zag/zag-zig
-
zig/zag
-
效果与效率
即便每次都“恶意地”试图访问最底层节点,最坏情况也不会持续发生。可见,伸展树虽不能杜绝最坏情况的发生,却能有效地控制最坏情况发生的频度,从而在分摊意义下保证整体的高效率。
更准确地,Tarjan等人采用势能分析法(potential analysis)已证明,在改用“双层伸展”策略之后,伸展树的单次操作均可在分摊的O(logn)时间内完成。
8.1.4 伸展树的实现
-
伸展树接口定义
0001 #include "BST/BST.h" //基于BST实现Splay 0002 template <typename T> class Splay : public BST<T> { //由BST派生的Splay树模板类 0003 protected: 0004 BinNodePosi<T> splay ( BinNodePosi<T> v ); //将节点v伸展至根 0005 public: 0006 BinNodePosi<T> & search ( const T& e ); //查找(重写) 0007 BinNodePosi<T> insert ( const T& e ); //插入(重写) 0008 bool remove ( const T& e ); //删除(重写) 0009 };这里直接沿用二叉搜索树类,并根据伸展树的平衡规则,重写了三个基本操作接口search()、insert()和remove(),另外,针对伸展调整操作,设有一个内部保护型接口splay()。
-
伸展算法的实现
0001 template <typename NodePosi> inline //在节点*p与*lc(可能为空)之间建立父(左)子关系 0002 void attachAsLC ( NodePosi lc, NodePosi p ) { p->lc = lc; if ( lc ) lc->parent = p; } 0003 0004 template <typename NodePosi> inline //在节点*p与*rc(可能为空)之间建立父(右)子关系 0005 void attachAsRC ( NodePosi p, NodePosi rc ) { p->rc = rc; if ( rc ) rc->parent = p; } 0006 0007 template <typename T> //Splay树伸展算法:从节点v出发逐层伸展 0008 BinNodePosi<T> Splay<T>::splay ( BinNodePosi<T> v ) { //v为因最近访问而需伸展的节点位置 0009 if ( !v ) return NULL; BinNodePosi<T> p; BinNodePosi<T> g; //*v的父亲与祖父 0010 while ( ( p = v->parent ) && ( g = p->parent ) ) { //自下而上,反复对*v做双层伸展 0011 BinNodePosi<T> gg = g->parent; //每轮之后*v都以原曾祖父(great-grand parent)为父 0012 if ( IsLChild ( *v ) ) 0013 if ( IsLChild ( *p ) ) { //zig-zig 0014 attachAsLC ( p->rc, g ); attachAsLC ( v->rc, p ); 0015 attachAsRC ( p, g ); attachAsRC ( v, p ); 0016 } else { //zig-zag 0017 attachAsLC ( v->rc, p ); attachAsRC ( g, v->lc ); 0018 attachAsLC ( g, v ); attachAsRC ( v, p ); 0019 } 0020 else if ( IsRChild ( *p ) ) { //zag-zag 0021 attachAsRC ( g, p->lc ); attachAsRC ( p, v->lc ); 0022 attachAsLC ( g, p ); attachAsLC ( p, v ); 0023 } else { //zag-zig 0024 attachAsRC ( p, v->lc ); attachAsLC ( v->rc, g ); 0025 attachAsRC ( v, g ); attachAsLC ( p, v ); 0026 } 0027 if ( !gg ) v->parent = NULL; //若*v原先的曾祖父*gg不存在,则*v现在应为树根 0028 else //否则,*gg此后应该以*v作为左或右孩子 0029 ( g == gg->lc ) ? attachAsLC ( v, gg ) : attachAsRC ( gg, v ); 0030 updateHeight ( g ); updateHeight ( p ); updateHeight ( v ); 0031 } //双层伸展结束时,必有g == NULL,但p可能非空 0032 if ( p = v->parent ) { //若p果真非空,则额外再做一次单旋 0033 if ( IsLChild ( *v ) ) { attachAsLC ( v->rc, p ); attachAsRC ( v, p ); } 0034 else { attachAsRC ( p, v->lc ); attachAsLC ( p, v ); } 0035 updateHeight ( p ); updateHeight ( v ); 0036 } 0037 v->parent = NULL; return v; 0038 } //调整之后新树根应为被伸展的节点,故返回该节点的位置以便上层函数更新树根 -
查找算法的实现
0001 template <typename T> BinNodePosi<T> & Splay<T>::search ( const T & e ) { //在伸展树中查找e 0002 BinNodePosi<T> p = BST<T>::search ( e ); 0003 _root = splay ( p ? p : _hot ); //将最后一个被访问的节点伸展至根 0004 return _root; 0005 } //与其它BST不同,无论查找成功与否,_root都指向最后被访问的节点首先,调用二叉搜索树的通用算法searchIn()尝试查找具有关键码e的节点。无论查找是否成功,都继而调用splay()算法,将查找终止位置处的节点伸展到树根。
-
插入算法的实现
0001 template <typename T> BinNodePosi<T> Splay<T>::insert ( const T& e ) { //将关键码e插入伸展树中 0002 if ( !_root ) { _size = 1; return _root = new BinNode<T> ( e ); } //原树为空 0003 BinNodePosi<T> t = search( e ); if ( e == t->data ) return t; //目标节点t若存在,伸展至根 0004 if ( t->data < e ) { //在右侧嫁接 0005 t->parent = _root = new BinNode<T> ( e, NULL, t, t->rc ); //lc == t必非空 0006 if ( t->rc ) { t->rc->parent = _root; t->rc = NULL; } //rc或为空 0007 } else { //在左侧嫁接 0008 t->parent = _root = new BinNode<T> ( e, NULL, t->lc, t ); //rc == t必非空 0009 if ( t->lc ) { t->lc->parent = _root; t->lc = NULL; } //lc或为空 0010 } 0011 _size++; updateHeightAbove ( t ); //更新t及其祖先(实际上只有_root一个)的高度 0012 return _root; //新节点必然置于树根,返回之 0013 } //无论e是否存在于原树中,返回时总有_root->data == e -
删除算法的实现
如图8.9所示,为从伸展树T中删除关键码为e的节点,首先亦调用接口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的直接后继,故数据局部性也得到了利用。
0001 template <typename T> bool Splay<T>::remove ( const T& e ) { //从伸展树中删除关键码e 0002 if ( !_root || ( e != search ( e )->data ) ) return false; //若目标存在,则伸展至根 0003 BinNodePosi<T> L = _root->lc, R = _root->rc; release(_root); //记下左、右子树L、R后,释放之 0004 if ( !R ) { //若R空,则 0005 if ( L ) L->parent = NULL; _root = L; //L即是余树 0006 } else { //否则 0007 _root = R; R->parent = NULL; search( e ); //在R中再次查找e:注定失败,但其中的最小节点必 0008 if (L) L->parent = _root; _root->lc = L; //伸展至根(且无左孩子),故可令其以L作为左子树 0009 } 0010 if ( --_size ) updateHeight ( _root ); return true; //更新规模及树高,报告删除成功 0011 } //若目标节点存在且被删除,返回true;否则返回false
“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 15 天,点击查看活动详情”