首页 > 代码库 > 伸展树(splay tree)

伸展树(splay tree)

伸展树同样是一种平衡二叉搜索树,它的优势在于,在足够长的序列中能保证分摊意义上的高效率,同时也无需记录高度或者平衡因子等信息。

伸展树的高效前提是局部性:刚刚被访问到的数据,可能在短时间内被再次访问;将被访问的下一元素,可能就在不久之前刚刚访问过的元素的附近。因此,伸展树的策略,就是把刚刚访问到的节点,及时“伸展”到树根附近。

所谓“伸展”操作,其实就是BST中的旋转操作。如果每次经过适当旋转,将访问的节点提升一层,直到上升到树根,称为逐层伸展。可以验证,这种伸展方式可能会是低效的,而且会导致结构复原,因此,使用双层伸展代替。

顾名思义,双层伸展就是每次将当前节点v上升两层。根据当前节点v与父亲节点p和祖父g的关系,可以分为三类:

(1)v p g同侧的时候,此时进行zig-zig或者zag-zag即可。

技术分享

(2)v p g不同侧的时候,此时进行zig-zag或者zag-zig即可。

技术分享

(3)当v的祖父g已经不存在,即v需要上升的层数为奇数时,只需要简单的zig/zag即可。

技术分享

伸展树的代码实现:

 1 #include"BST.h"
 2 template<typename T> class Splay :public BST<T>
 3 {
 4 protected:
 5     BinNodePosi(T) splay(BinNodePosi(T) v);//将节点v伸展至根
 6 public:
 7     BinNodePosi(T)& search(const T& e) override;//查找
 8     BinNodePosi(T) insert(const T& e) override;
 9     bool remove(const T& e) override;
10 };
 1 template<typename T> BinNodePosi(T) Splay<T>::splay(BinNodePosi(T) v)
 2 {
 3     if (!v) return NULL; BinNodePosi(T) p; BinNodePosi(T) g;
 4     while ((p = v->parent) && (g = p->parent))
 5     {
 6         BinNodePosi(T) gg = g->parent;//每轮之后以原曾祖父为父亲
 7         if(IsLChild(*v))
 8             if (IsLChild(*p)) //zig-zig
 9             {
10                 attachAsLChild(g, p->rc); attachAsLChild(p, v->rc);
11                 attachAsRChild(p, g); attachAsRChild(v, p);
12             }
13             else//zig-zag
14             {
15                 attachAsLChild(p, v->rc); attachAsRChild(g, v->lc);
16                 attachAsLChild(v, g); attachAsRChild(v, p);
17             }
18         else if (IsLChild(*p))
19         {
20             attachAsRChild(g, p->lc); attachAsLChild(p, v->rc);
21             attachAsRChild(p, g); attachAsLChild(v, p);
22         }
23         else
24         {
25             attachAsRChild(p, v->lc); attachAsLChild(g, v->rc);
26             attachAsRChild(v, g); attachAsLChild(v, p);
27         }
28         if (!gg) v->parent = NULL;//不存在曾祖父,v已经是树根
29         else//否则,根据g是左子树还是右子树,连接曾祖父和v
30             (g == gg->lc) ? attachAsLChild(gg, v) : attachAsRChild(gg, v);
31         updateHegiht(g); updateHeight(p); updateHegiht(v);
32     }
33     if (p = v->parent)//如果v还存在父亲,做一次单旋
34     {
35         if (IsLChild(*v)) { attachAsLChild(p, v->rc); attachAsRChild(v, p); }
36         else { attachAsRChild(p, v->lc); attachAsLChild(v, p); }
37         updateHegiht(p); updateHegiht(v);
38     }
39     v->parent = NULL; return v;//此时v必然已经是树根了
40 }

可以看到,双旋的部分分为4种位置情况,之后再判断是否需要进行一次单旋,最后返回旋转到树根位置的v节点。

1 template<typename T> BinNodePosi(T)& Splay<T>::search(const T& e)
2 {
3     BinNodePosi(T) p = searchIn(_root, e, _hot = NULL);
4     _root = spaly(p ? p : _hot);//将最后一个被访问者延伸到树根
5     return _root;//_root指向最后被访问的节点,要么是查找目标,要么是_hot
6 }

查找操作,主体就是查找后进行伸展操作。

 1 template<typename T> BinNodePosi(T) Splay<T>::insert(const T&e)
 2 {
 3     if (!_root) { _size++; return _root = new BinNode<T>(e); }
 4     if (e == search(e)->data) return _root;
 5     _size++;
 6     BinNodePosi(T) t = _root;//创建新节点
 7     if (_root->data < e)//新插入节点大于原根,以t和t->rc作为孩子
 8     {
 9         t->parent = _root = new BinNode<T>(e, NULL, t, t->rc);//插入新节点以及设置父子
10         if (HasRChild(*t)) { t->rc->parent = _root; t->rc = NULL; }//原根的右子树接入新节点
11     }
12     else//...小于原根,把原根节点作为右孩子
13     {
14         t->parent = _root = new BinNode<T>(e, NULL, t->lc, t);
15         if (HasLChild(*t)) { t->lc->parent = _root; t->lc = NULL; }
16     }
17     return _root;//返回树根,也就是新插入的节点
18 }

插入操作,首先进行查找,判断是否已经存在,并且把查找的结果伸展到树根。再根据根节点与插入的节点大小关系,最终把插入的节点设置为树根。

 1 template<typename T> bool Splay<T>::remove(const T& e)//删除节点,将原节点的父亲升至树根
 2 {
 3     if (!_root || (e != search(e)->data)) return false;//失败时直接返回
 4     BinNodePosi(T) w = _root;
 5     if (!HasLChild(*_root))//如果没有左子树
 6     {
 7         _root = _root->rc;
 8         if (_root) _root->parent = NULL;
 9     }
10     else if (!HasRChild(*_root))//没有右子树
11     {
12         _root = _root->lc;
13         if (_root) _root->parent = NULL;
14     }
15     else //左右子树都存在
16     {
17         BinNodePosi(T) lTree = _root->lc;//记录左子树
18         lTree->parent = NULL;//分离左子树
19         _root->lc = NULL;
20         _root = _root->rc; _root->parent = NULL;////保留右子树
21         search(w->data);//必定失败,但是能把后继(右子树中最小的节点)升至根
22         _root->lc = lTree; lTree->parent = _root;//接入原左子树
23     }
24     _size--;
25     if(_root) updateHeight(_root);
26     return true;
27 }

删除操作,把被删除节点的父亲升为树根。具体地,如果两个子树有一个不存在,只需要改为左右孩子然后释放原树根节点。如果两个孩子均存在,就需要寻找后继,并升到树根位置。

 

最后,分析一下伸展树的优缺点。伸展树的优点,在于分摊效率高,单次操作在O(logn)时间内完成,且对于局部利用率高的情况效率高。缺点也很明显,就是树的结构变化非常大,而且最坏情况下,单次操作最好也需要n的时间。

伸展树(splay tree)