注:本文改自http://blog.csdn.net/changtao381/article/details/8936765与百度百科伸展树。
(代码为本人原创,有两百多行,第一次很难写,但是写多了就好了。)
博客原作者若有问题可联系本人(815344231@qq.com),多谢。
类别:二叉排序树
空间效率:O(n)
时间效率:O(log n)内完成插入、查找、删除操作
时间效率:O(log n)内完成插入、查找、删除操作
创造者:Daniel Sleator和Robert Tarjan
优点:每次查询会调整树的结构,使被查询频率高的条目更靠近树根。
注:所有图片来自wiki——http://en.wikipedia.org/wiki/Splay_tree(English)。
注:所有图片来自wiki——http://en.wikipedia.org/wiki/Splay_tree(English)。
树的旋转是splay的基础,对于二叉查找树来说,树的旋转不破坏查找树的结构。
Splaying是Splay Tree中的基本操作,为了让被查询的条目更接近树根,Splay Tree使用了树的旋转操作,同时保证二叉排序树的性质不变。
Splaying的操作受以下三种因素影响:
- 节点x是父节点p的左孩子还是右孩子
- 节点p是不是根节点,如果不是
- 节点p是父节点g的左孩子还是右孩子
同时有三种基本操作:
当p为根节点时,进行zip step操作。
当x是p的左孩子时,对x右旋;
当x是p的右孩子时,对x左旋。
当p不是根节点,且x和p同为左孩子或右孩子时进行Zig-Zig操作。
当x和p同为左孩子时,依次将p和x右旋;
当x和p同为右孩子时,依次将p和x左旋。
当p不是根节点,且x和p不同为左孩子或右孩子时,进行Zig-Zag操作。
当p为左孩子,x为右孩子时,将x左旋后再右旋。
当p为右孩子,x为左孩子时,将x右旋后再左旋。
其余操作
查找操作
Find(x,S):判断元素x是否在伸展树S表示的有序集中。
首先,与在二叉查找树中的查找操作一样,在伸展树中查找元素x。如果x在树中,则再执行Splaying(x,S)调整伸展树。
加入操作
Insert(x,S):将元素x插入伸展树S表示的有序集中。
首先,也与处理普通的二叉查找树一样,将x插入到伸展树S中的相应位置上,再执行Splaying(x,S)。
删除操作
Delete(x,S):将元素x从伸展树S所表示的有序集中删除。
首先,用在二叉查找树中查找元素的方法找到x的位置。如果x没有孩子或只有一个孩子,那么直接将x删去,并通过Splaying操作,将x节点的父节点调整
到伸展树的根节点处。否则,则向下查找x的后继y,用y替代x的位置,最后执行Splaying(y,S),将y调整为伸展树的根。
合并操作
join(S1,S2):将两个伸展树S1与S2合并成为一个伸展树。其中S1的所有元素都小于S2的所有元素。首先,我们找到伸展树S1中最大的一个元素x,再通过Splaying(x,S1)将x调整到伸展树S1的根。然后再将S2作为x节点的右子树。这样,就得到了新的伸展树S。
划分操作
Split(x,S):以x为界,将伸展树S分离为两棵伸展树S1和S2,其中S1中所有元素都小于x,S2中的所有元素都大于x。首先执行Find(x,S),将元素x调整为伸展树的根节点,则x的左子树就是S1,而右子树为S2。
其他操作
除了上面介绍的五种基本操作,伸展树还支持求最大值、求最小值、求前趋、求后继等多种操作,这些基本操作也都是建立在伸展操作的基础上的。
通常来说,每进行一种操作后都会进行一次Splaying操作,这样可以保证每次操作的平摊时间复杂度是O(logn)。
代码
#include <iostream> #include <cstring> #include <cstdio> #include <string> using namespace std; #define N 100000 struct Tree { int Le,Ri,Fa,s; }; Tree a[N]; int n=0,Gf=0; void RiTn(int x)//右旋 { int i=a[a[x].Fa].Fa; if (i==0) Gf=i;else { if (a[i].Le==a[x].Fa) a[i].Le=x;else a[i].Ri=x; } if (a[x].Ri!=0) a[a[x].Ri].Fa=a[x].Fa; a[a[x].Fa].Le=a[x].Ri; a[a[x].Fa].Fa=x; a[x].Ri=a[x].Fa; a[x].Fa=i; return; } void LeTn(int x)//左旋 { int i=a[a[x].Fa].Fa; if (i==0) Gf=i;else { if (a[i].Le==a[x].Fa) a[i].Le=x;else a[i].Ri=x; } if (a[x].Le!=0) a[a[x].Le].Fa=a[x].Fa; a[a[x].Fa].Ri=a[x].Le; a[a[x].Fa].Fa=x; a[x].Le=a[x].Fa; a[x].Fa=i; return; } int Find(int x,int y)//查找元素 { if (y==0) return 0; if (a[y].s==x) return y;else if (a[y].s>x) return Find(x,a[y].Le);else return Find(x,a[y].Ri); } void Insert(int x)//插入操作 { if (a[n].s<a[x].s) { if (a[x].Le==0) { a[x].Le=n; a[n].Fa=x; }else Insert(a[x].Le); }else { if (a[x].Ri==0) { a[x].Ri=n; a[n].Fa=x; }else Insert(a[x].Ri); } return; } int Fnext(int x)//查找后继 { if (a[x].Le==0) return x;else return Fnext(a[x].Le); } int main() { int i,j,k,l,q,w,e,m; string b; scanf("%d",&m); for (i=1;i<=m;i++) { cin >>b; if (b=="Find") { scanf("%d",&k); l=Find(k,Gf); if (l>0) { printf("Yes\n"); while (Gf!=l) if (l==a[a[l].Fa].Le) RiTn(l);else LeTn(l); }else printf("No\n"); }else if (b=="Insert") { scanf("%d",&k); n++; a[n].s=k; a[n].Fa=0; a[n].Le=0; a[n].Ri=0; if (n==1) Gf=1;else { Insert(Gf); while (Gf!=n) if (a[a[n].Fa].Le==n) RiTn(n);else LeTn(n); } }else if (b=="Delete") { scanf("%d",&k); l=Find(k,Gf); if (l==0) continue; if (a[l].Le*a[l].Ri==0) { if (l!=Gf) { if (a[a[l].Fa].Ri==l) a[a[l].Fa].Ri=a[l].Ri+a[l].Le;else a[a[l].Fa].Le=a[l].Ri+a[l].Le; }else Gf=a[l].Le+a[l].Ri; if (a[l].Le>0) a[a[l].Le].Fa=a[l].Fa;else if (a[l].Ri>0) a[a[l].Ri].Fa=a[l].Fa; a[l].Fa=0; a[l].Le=0; a[l].Ri=0; a[l].s=0; }else { q=Fnext(a[l].Ri); if (q==a[a[q].Fa].Le) { a[a[q].Fa].Le=a[q].Ri; if (a[q].Ri!=0) a[a[q].Ri].Fa=a[q].Fa; if (l!=Gf) { if (l==a[a[l].Fa].Ri) a[a[l].Fa].Ri=q;else a[a[l].Fa].Le=q; }else Gf=q; a[q].Ri=a[l].Ri; a[q].Fa=a[l].Fa; a[q].Le=a[l].Le; if (a[l].Le>0) a[a[l].Le].Fa=q; a[a[l].Ri].Fa=q; }else { if (l!=Gf) { if (l==a[a[l].Fa].Le) a[a[l].Fa].Le=q;else a[a[l].Fa].Ri=q; }else Gf=q; a[q].Fa=a[l].Fa; a[q].Le=a[l].Le; if (a[q].Le>0) a[a[q].Le].Fa=q; } a[l].Fa=0; a[l].Le=0; a[l].Ri=0; a[l].s=0; } } } return 0; }
Splay Tree可以方便的解决一些区间问题,根据不同形状二叉树先序遍历结果不变的特性,可以将区间按顺序建二叉查找树。
每次自下而上的一套splay都可以将x移动到根节点的位置,利用这个特性,可以方便的利用Lazy的思想进行区间操作。
对于每个节点记录size,代表子树中节点的数目,这样就可以很方便地查找区间中的第k小或第k大元素。
对于一段要处理的区间[x, y],首先splay x-1到root,再splay y+1到root的右孩子,这时root的右孩子的左孩子对应子树就是整个区间。
这样,大部分区间问题都可以很方便的解决,操作同样也适用于一个或多个条目的添加或删除,和区间的移动。