伸展树(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的时间。

时间: 2024-10-12 21:46:32

伸展树(splay tree)的相关文章

树-伸展树(Splay Tree)

伸展树概念 伸展树(Splay Tree)是一种二叉排序树,它能在O(log n)内完成插入.查找和删除操作.它由Daniel Sleator和Robert Tarjan创造. (01) 伸展树属于二叉查找树,即它具有和二叉查找树一样的性质:假设x为树中的任意一个结点,x节点包含关键字key,节点x的key值记为key[x].如果y是x的左子树中的一个结点,则key[y] <= key[x]:如果y是x的右子树的一个结点,则key[y] >= key[x]. (02) 除了拥有二叉查找树的性质

【BBST 之伸展树 (Splay Tree)】

最近“hiho一下”出了平衡树专题,这周的Splay一直出现RE,应该删除操作指针没处理好,还没找出原因. 不过其他操作运行正常,尝试用它写了一道之前用set做的平衡树的题http://codeforces.com/problemset/problem/675/D,运行效果居然还挺好的,时间快了大概10%,内存少了大概30%. 1 #include <cstdio> 2 #include <cstring> 3 #include <string> 4 #include

# 伸展树 Splay

伸展树 Splay 维基百科上称为伸展树,但是国内好像一般叫平衡树,是众多平衡树中比较优秀的一种. 平衡树左旋右旋不会影响中序遍历顺序. 一棵平衡树的中序遍历顺序是值递增排序的,相当于从小到大到大排了一次序. 平衡树的作用: 平衡树其实就是一棵二叉搜索树,set和map都是平衡树实现. 一棵二叉搜索树理论深度是\(O(log(n))\),但是当二叉树退化成链表的时候,深度就变成了\(O(n)\),很多\(O(log)\)级别从操作会退化成\(O(n)\)线性级别的操作.平衡树就是在不改变二叉搜索

伸展树 Splay 模板

学习Splay的时候参考了很多不同的资料,然而参考资料太杂的后果就是模板调出来一直都有问题,尤其是最后发现网上找的各种资料均有不同程度的错误. 好在啃了几天之后终于算是啃下来了. Splay也算是平衡树的一种,但是跟AVL树.SBT不同的是,Splay并不是一直保持严格的平衡,因此在速度上可能要慢一些,但是统计学上仍能保证Splay具有O(logn)的均摊复杂度. Splay原生不需要附加任何空间,它的先天优势是其特有的Splay操作可以非常灵活地改变整棵树的结构形态,完成一般线段树.平衡树做不

浅谈伸展树(Splay)

//本文是一个暂时的小记,有不对的请大佬们指出~ 真正大佬的在这http://blog.csdn.net/clove_unique/article/details/50630280 伸展树(Splay Tree),也叫分裂树,是一种二叉排序树,它能在O(log n)内完成插入.查找和删除操作.它由丹尼尔·斯立特Daniel Sleator和罗伯特·恩卓·塔扬Robert Endre Tarjan在1985年发明的. 在伸展树上的一般操作都基于伸展操作:假设想要对一个二叉查找树执行一系列的查找操作

POJ 3580 - SuperMemo - [伸展树splay]

题目链接:http://poj.org/problem?id=3580 Your friend, Jackson is invited to a TV show called SuperMemo in which the participant is told to play a memorizing game. At first, the host tells the participant a sequence of numbers, {A1, A2, ... An}. Then the h

模板——伸展树 splay 实现快速分裂合并的序列

伸展操作:将treap中特定的结点旋转到根 //将序列中从左数第k个元素伸展到根,注意结点键值保存的是原序列id void splay(Node* &o, int k) { int s = o->ch[0] == NULL ? 0 : o->ch[0]->s; int d = k <= s ? 0 : (k == s+1 ? -1 : 1); if(d == 1) k -= s+1; if(d != -1) { splay(o->ch[d], k); rotate(o

伸展树Splay

新学的,其实吧,就那么回事.... 看了几天,splay处理序列问题,真的非常厉害,翻转,插入,删除,线段树实现不了的功能,splay用起来很方便. POJ 3580 SuperMemo 这题基本就是检验模板的题,各种操作都有,错了好多次,发现以前写的代码有错了的,数据水点,给水过了,注意pushup. Splay模板 #include <cstdio> #include <cstring> #include <map> #include <algorithm&g

UVA11922--Permutation Transformer (伸展树Splay)

题意:m条操作指令,对于指令 a  b 表示取出第a~b个元素,翻转后添加到排列的尾部. 水题卡了一个小时,一直过不了样例.  原来是 dfs输出的时候 忘记向下传递标记了. 1 #include <cstdio> 2 #include <cstdlib> 3 #include <cstring> 4 #include <algorithm> 5 using namespace std; 6 const double eps = 1e-8; 7 const