splay tree(伸展树)学习小结(一)
2014-11-21 22:40
330 查看
最近学习了splay的一些基本操作,写下这篇文章希望对初学者可以有所帮助~
Q:splay是什么?
A:他是一种二叉排序树,能在O(log n)的时间内完成插入、查找、删除、找结点的前驱后继、找区间第k大等的操作。他的一般操作都基于伸展操作【即splay(x),一会儿会说到】
Q:splay长什么样子?
A:首先他是一棵树,并且他是一棵二叉树;这棵树满足父亲节点的值大于左儿子,并且小于右儿子(等于可以自己定)。
现在,问题来了:splay的基本操作有哪些?怎么实现呢?
注:
1.我不太喜欢用指针。。所以代码全部用数组实现。
2.以下代码中a[i].fa表示父亲,a[i].size表示以i为根的子树的大小(包含i),a[i].l表示左儿子的结点编号,a[i].r表示右儿子的结点编号。
(一)zig zag操作
zig操作是将左儿子移到父亲节点,zag操作是将右儿子移到父亲节点。
![](http://img.blog.csdn.net/20141121145918714)
为什么是这样转的呢?大家可以自己动手画一下。【注意这棵树满足左儿子<=父亲<右儿子的性质】
这里简要介绍一下zig操作:
x是y的左儿子,所以y比x大;当x成为y的父亲后,y就应该变成x的右儿子,那x的右儿子去哪里呢?
因为x的右儿子一定比y小,所以x的右儿子变成y的左儿子就可以,那么y的左儿子呢?
y的左儿子是x啊!
他已经成为y的父亲了,所以这样树就完成了zig(x)的旋转!
zag操作完全类似。
(二)splay操作(关键!!)
splay(x)就是把x旋转到根节点。
在旋转的过程中要分三种情况处理:
(1)zig(x)或zag(x):此时x已经是根节点的儿子,只要旋转一次就可以达到目的。
![](http://img.blog.csdn.net/20141121145918714)
(2)zig(y)-zig(x)或zag(y)-zag(x)(一字型旋转):x为y的左儿子,y为z父亲的左儿子。那么现将y旋转到z(此时x仍然为y的左儿子),再将x旋转到y。
![](http://img.blog.csdn.net/20141121145931787)
(反之同理)
(3)zig(x)-zag(x)或zag(x)-zig(x)(之字型旋转):x为y的左儿子,y为z的右儿子。那么先将x旋转到y,再将y旋转到z。
为什么不能zag(y)-zig(x)?因为把y旋转到z后,y的左儿子变成了z,此时再旋转就会出错。
![](http://img.blog.csdn.net/20141121145947465)
(反之同理)
但是!!为什么一定要双旋?每次只旋转一个点不可以吗?
不可以!!如果当前的树是一个长度为n的链,将最下面的旋转到根结点要旋转n-1次,并且旋转完成后仍是一条链,最终时间复杂度变成O(nm)了!因此双旋虽然比单旋要难写一点,但是能够保证树的平衡(深度为logn左右),从而保证时间复杂度。
(三)search操作
search(w)是为了方便下文的insert(w):找w在树中应该插入到哪个结点的儿子。(当然稍作修改就可以变成查询w这个值所在的位置)
方法是从根节点开始,如果w小于根结点的值,那么顺着这左子树继续找,否则顺着右子树继续找。
(四)insert操作
insert(w)就是把值为w的数插入到树中。
分两种情况讨论:
(1)当前根是0,即树中没有结点:直接把这个值插到根结点下
(2)当前树中有结点:先search(w)(见上文),然后如果w大于找到的结点的值,把w作为右儿子,反之同理。最后一定要记住把插入的w旋转到根结点。
为什么一定要旋转到根结点?因为你插入一个数之后,与他相关的结点的size值就会改变,而splay操作可以将修改update,使整棵树维护的数据不会出错。
(五)delete操作
delete(k)表示删除树中小于k的所有值。
方法是插入一个k值,并把他旋转到根结点,然后直接把root赋值为a[root].r,此时就把小于k的树全部“砍”掉了。
(六)findkth操作
寻找第k大的数。
方法是通过每个结点的size递归寻找即可,分三种情况:
(1)当前的结点x就是第k个数,直接返回当前结点的值
(2)第k个数在左子树中,递归寻找左子树中第k大的值
(3)第k个数在右子树中,递归寻找右子树中第(k-1-a[a[x].l].size大的值)
本文的操作都是基于bzoj1503[noi2004]郁闷的出纳员 来写的,下面贴上本题完整代码。大家可以用这道题作为自己的splay第一题,练习基本操作。
Submit: 6373 Solved: 2229
[Submit][Status]
![](http://www.lydsy.com/JudgeOnline/images/1503_1.jpg)
I 60
I 70
S 50
F 2
I 30
S 15
A 5
F 1
F 2
20
-1
2
如果对本文有疑问或者有修改意见的欢迎在文章下面发表评论~
参考:
杨思雨《伸展树的基本操作和应用》
Q:splay是什么?
A:他是一种二叉排序树,能在O(log n)的时间内完成插入、查找、删除、找结点的前驱后继、找区间第k大等的操作。他的一般操作都基于伸展操作【即splay(x),一会儿会说到】
Q:splay长什么样子?
A:首先他是一棵树,并且他是一棵二叉树;这棵树满足父亲节点的值大于左儿子,并且小于右儿子(等于可以自己定)。
现在,问题来了:splay的基本操作有哪些?怎么实现呢?
注:
1.我不太喜欢用指针。。所以代码全部用数组实现。
2.以下代码中a[i].fa表示父亲,a[i].size表示以i为根的子树的大小(包含i),a[i].l表示左儿子的结点编号,a[i].r表示右儿子的结点编号。
(一)zig zag操作
zig操作是将左儿子移到父亲节点,zag操作是将右儿子移到父亲节点。
为什么是这样转的呢?大家可以自己动手画一下。【注意这棵树满足左儿子<=父亲<右儿子的性质】
这里简要介绍一下zig操作:
x是y的左儿子,所以y比x大;当x成为y的父亲后,y就应该变成x的右儿子,那x的右儿子去哪里呢?
因为x的右儿子一定比y小,所以x的右儿子变成y的左儿子就可以,那么y的左儿子呢?
y的左儿子是x啊!
他已经成为y的父亲了,所以这样树就完成了zig(x)的旋转!
zag操作完全类似。
void update(int x) //zigzag后树的size就会改变,所以维护一下 { a[x].size=a[a[x].l].size+a[a[x].r].size+1; } void zig(int x) { int y=a[x].fa; int z=a[y].fa; a[y].l=a[x].r,a[y].fa=x,a[x].fa=z; a[a[x].r].fa=y; a[x].r=y; if (a[z].l==y) a[z].l=x; else a[z].r=x; //写的时候要注意修改的顺序,否则有的结点你已经修改过了但是你却想用的是修改前的数据 update(y); update(x); } void zag(int x) { int y=a[x].fa; int z=a[y].fa; a[y].r=a[x].l,a[x].fa=z,a[y].fa=x; a[a[x].l].fa=y; a[x].l=y; if (a[z].l==y) a[z].l=x; else a[z].r=x; update(y); update(x); }
(二)splay操作(关键!!)
splay(x)就是把x旋转到根节点。
在旋转的过程中要分三种情况处理:
(1)zig(x)或zag(x):此时x已经是根节点的儿子,只要旋转一次就可以达到目的。
(2)zig(y)-zig(x)或zag(y)-zag(x)(一字型旋转):x为y的左儿子,y为z父亲的左儿子。那么现将y旋转到z(此时x仍然为y的左儿子),再将x旋转到y。
(反之同理)
(3)zig(x)-zag(x)或zag(x)-zig(x)(之字型旋转):x为y的左儿子,y为z的右儿子。那么先将x旋转到y,再将y旋转到z。
为什么不能zag(y)-zig(x)?因为把y旋转到z后,y的左儿子变成了z,此时再旋转就会出错。
(反之同理)
但是!!为什么一定要双旋?每次只旋转一个点不可以吗?
不可以!!如果当前的树是一个长度为n的链,将最下面的旋转到根结点要旋转n-1次,并且旋转完成后仍是一条链,最终时间复杂度变成O(nm)了!因此双旋虽然比单旋要难写一点,但是能够保证树的平衡(深度为logn左右),从而保证时间复杂度。
void splay(int x) { while (a[x].fa) { int y=a[x].fa; int z=a[y].fa; if (!z) //第一种情况 { if (a[y].l==x) zig(x); else zag(x); break; } else { if (y==a[z].l) //第二种情况 { if (x==a[y].l) zig(y),zig(x); else zag(x),zig(x); } else //第三种情况 { if (x==a[y].r) zag(y),zag(x); else zig(x),zag(x); } } } root=x; //注意最后不要忘了root变成了x }
(三)search操作
search(w)是为了方便下文的insert(w):找w在树中应该插入到哪个结点的儿子。(当然稍作修改就可以变成查询w这个值所在的位置)
方法是从根节点开始,如果w小于根结点的值,那么顺着这左子树继续找,否则顺着右子树继续找。
int search(int w) { int x=root,p; //p就是所求的位置 while (x) { p=x; if (a[x].data>=w) x=a[x].l; else x=a[x].r; } return p; }
(四)insert操作
insert(w)就是把值为w的数插入到树中。
分两种情况讨论:
(1)当前根是0,即树中没有结点:直接把这个值插到根结点下
(2)当前树中有结点:先search(w)(见上文),然后如果w大于找到的结点的值,把w作为右儿子,反之同理。最后一定要记住把插入的w旋转到根结点。
为什么一定要旋转到根结点?因为你插入一个数之后,与他相关的结点的size值就会改变,而splay操作可以将修改update,使整棵树维护的数据不会出错。
<pre name="code" class="cpp">void insert(int w) { if (root==0) //第一种情况 { a[++tot].fa=0,a[tot].size=1,a[tot].data=w,root=tot; return; } int i=search(w); //第二种情况 tot++; a[tot].data=w,a[tot].fa=i,a[tot].size=1; if (a[i].data>=w) a[i].l=tot; else a[i].r=tot; splay(tot); }
(五)delete操作
delete(k)表示删除树中小于k的所有值。
方法是插入一个k值,并把他旋转到根结点,然后直接把root赋值为a[root].r,此时就把小于k的树全部“砍”掉了。
void delete(int k) { insert(k); root=a[root].r; a[root].fa=0; }
(六)findkth操作
寻找第k大的数。
方法是通过每个结点的size递归寻找即可,分三种情况:
(1)当前的结点x就是第k个数,直接返回当前结点的值
(2)第k个数在左子树中,递归寻找左子树中第k大的值
(3)第k个数在右子树中,递归寻找右子树中第(k-1-a[a[x].l].size大的值)
int findkth(int x,int k) { if (k==a[a[x].l].size+1) return a[x].data; //第一种情况 if (k<=a[a[x].l].size) return findkth(a[x].l,k); //第二种情况 else return findkth(a[x].r,k-1-a[a[x].l].size); //第三种情况 }
本文的操作都是基于bzoj1503[noi2004]郁闷的出纳员 来写的,下面贴上本题完整代码。大家可以用这道题作为自己的splay第一题,练习基本操作。
1503: [NOI2004]郁闷的出纳员
Time Limit: 5 Sec Memory Limit: 64 MBSubmit: 6373 Solved: 2229
[Submit][Status]
Description
OIER公司是一家大型专业化软件公司,有着数以万计的员工。作为一名出纳员,我的任务之一便是统计每位员工的工资。这本来是一份不错的工作,但是令人郁闷的是,我们的老板反复无常,经常调整员工的工资。如果他心情好,就可能把每位员工的工资加上一个相同的量。反之,如果心情不好,就可能把他们的工资扣除一个相同的量。我真不知道除了调工资他还做什么其它事情。工资的频繁调整很让员工反感,尤其是集体扣除工资的时候,一旦某位员工发现自己的工资已经低于了合同规定的工资下界,他就会立刻气愤地离开公司,并且再也不会回来了。每位员工的工资下界都是统一规定的。每当一个人离开公司,我就要从电脑中把他的工资档案删去,同样,每当公司招聘了一位新员工,我就得为他新建一个工资档案。老板经常到我这边来询问工资情况,他并不问具体某位员工的工资情况,而是问现在工资第k多的员工拿多少工资。每当这时,我就不得不对数万个员工进行一次漫长的排序,然后告诉他答案。好了,现在你已经对我的工作了解不少了。正如你猜的那样,我想请你编一个工资统计程序。怎么样,不是很困难吧?Input
![](http://www.lydsy.com/JudgeOnline/images/1503_1.jpg)
Output
输出文件的行数为F命令的条数加一。对于每条F命令,你的程序要输出一行,仅包含一个整数,为当前工资第k多的员工所拿的工资数,如果k大于目前员工的数目,则输出-1。输出文件的最后一行包含一个整数,为离开公司的员工的总数。Sample Input
9 10I 60
I 70
S 50
F 2
I 30
S 15
A 5
F 1
F 2
Sample Output
1020
-1
2
HINT
I命令的条数不超过100000 A命令和S命令的总条数不超过100 F命令的条数不超过100000 每次工资调整的调整量不超过1000 新员工的工资不超过100000Source
Splay#include <iostream> #include <algorithm> #include <cstdio> #include <cstring> #include <cstdlib> #include <cmath> using namespace std; struct node { int l,r,size,data,fa; }a[1000000]; int n,m,temp=0,ans=0; int tot=0,root; void update(int x) { a[x].size=a[a[x].l].size+a[a[x].r].size+1; } void zig(int x) { int y=a[x].fa; int z=a[y].fa; a[y].l=a[x].r,a[y].fa=x,a[x].fa=z; a[a[x].r].fa=y; a[x].r=y; if (a[z].l==y) a[z].l=x; else a[z].r=x; update(y); update(x); } void zag(int x) { int y=a[x].fa; int z=a[y].fa; a[y].r=a[x].l,a[x].fa=z,a[y].fa=x; a[a[x].l].fa=y; a[x].l=y; if (a[z].l==y) a[z].l=x; else a[z].r=x; update(y); update(x); } void splay(int x) { while (a[x].fa) { int y=a[x].fa; int z=a[y].fa; if (!z) { if (a[y].l==x) zig(x); else zag(x); break; } else { if (y==a[z].l) { if (x==a[y].l) zig(y),zig(x); else zag(x),zig(x); } else { if (x==a[y].r) zag(y),zag(x); else zig(x),zag(x); } } } root=x; } int search(int w) { int x=root,an; while (x) { an=x; if (a[x].data>=w) x=a[x].l; else x=a[x].r; } return an; } void insert(int w) { if (root==0) { a[++tot].fa=0,a[tot].size=1,a[tot].data=w,root=tot; return; } int i=search(w); tot++; a[tot].data=w,a[tot].fa=i,a[tot].size=1; if (a[i].data>=w) a[i].l=tot; else a[i].r=tot; splay(tot); } void delet(int k) { insert(k); root=a[root].r; a[root].fa=0; } int findkth(int x,int k) { if (k==a[a[x].l].size+1) return a[x].data; if (k<=a[a[x].l].size) return findkth(a[x].l,k); else return findkth(a[x].r,k-1-a[a[x].l].size); } int main() { scanf("%d%d",&n,&m); while (n--) { char c; int t; scanf("%s %d",&c,&t); if (c=='I'&&t>=m) insert(t-temp),ans++; if (c=='A') temp+=t; if (c=='S') temp-=t,delet(m-temp); if (c=='F') { if (t>a[root].size) printf("-1\n"); else { printf("%d\n",findkth(root,a[root].size-t+1)+temp); } } } printf("%d\n",ans-a[root].size); return 0; }
如果对本文有疑问或者有修改意见的欢迎在文章下面发表评论~
参考:
杨思雨《伸展树的基本操作和应用》
相关文章推荐
- 伸展树(Splay tree)学习小结 ---by---cxlove
- 伸展树(Splay tree)学习小结
- 伸展树模板小结(Splay Tree)
- 树学习 ---------伸展树(splay Tree)
- 伸展树(Splay Tree)小结
- 学习总结:Splay Tree 伸展树
- 伸展树学习小结
- c++学习小结
- 学习Python知识小结 杂记二
- Dev-C++下基本数据类型学习小结
- Page 的生命周期学习小结
- 学习阶段小结
- Page 的生命周期学习小结
- [持续更新] 学习历程 迭代计划和小结
- 民航学概论学习小结
- vi编辑器的学习使用(小结)
- 学习C++小结
- 近期学习小结(2005-2-13)
- 学习小结(2005-2-22)
- STL学习小结(原创:桑英硕 )