数据结构 | 第8章 - 伸展树

175 阅读8分钟

第8章 高级搜索树

8.1 伸展树

与前一章的AVL树一样,伸展树(splay tree)也是平衡二叉搜索树的一种形式。相当于前者,后者的实现更为简捷。伸展树无需时刻都严格地保持全树的平衡,但却能够在任何足够长的真实操作序列中,保持分摊意义上的高效率。伸展树也不需要对基本的二叉树节点结构,做任何附加的要求或改动,更不需要记录平衡因子或高度之类的额外信息,故适用范围更广。

8.1.1 局部性

“数据局部性”(data locality),这包括两个方面的含义:

  • 刚刚被访问过的元素,极有可能在不久之后再次被访问到
  • 将被访问的下一元素,极有可能就处于不久之前被访问过的某个元素的附近

就二叉搜索树而言,数据局部性具体表现为:

  • 刚刚被访问过的节点,极有可能在不久之后再次被访问到
  • 将被访问的下一节点,极有可能就处于不久之前被访问过的某个节点的附近

8.1.2 逐层伸展

  • 简易伸展树

    一种直接方式是:每访问过一个节点之后,随即反复地以它的父节点为轴,经适当的旋转将其提升一层,直至最终成为树根。

    image-20220721150918627.png

  • 最坏情况

    image-20220721151752511.png

    如此分摊下来,每次访问平均需要Ω(n)时间。很遗憾,这一效率不仅远远低于AVL树,而且甚至与原始的二叉搜索树的最坏情况相当。

    而且,图8.2(a)与(f)中二叉搜索树的结构完全相同!这意味着以上情况可以持续地再现!

8.1.3 双层伸展

为克服上述伸展调整策略的缺陷,一种简便且有效的方法就是:将逐层伸展改为双层伸展。具体地,每次都从当前节点v向上追溯两层(而不是仅一层),并根据其父亲p以及祖父g的相对位置,进行相应的旋转。以下分三类情况,分别介绍具体的处理方法。

  • zig-zig/zag-zag

    image.png

  • zig-zag/zag-zig

    image.png

  • zig/zag

    image.png

  • 效果与效率

image-20220721154255795.png

image-20220721154603314.png

即便每次都“恶意地”试图访问最底层节点,最坏情况也不会持续发生。可见,伸展树虽不能杜绝最坏情况的发生,却能有效地控制最坏情况发生的频度,从而在分摊意义下保证整体的高效率。

更准确地,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()算法,将查找终止位置处的节点伸展到树根。

  • 插入算法的实现

    image-20220721160707316.png

     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
    
  • 删除算法的实现

    image-20220721161536799.png

    如图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 天,点击查看活动详情