【学时总结】◆学时·VI◆ SPLAY伸展树
◆学时·VI◆ SPLAY伸展树
平衡树之多,学之不尽也……
◇算法概述
二叉排序树的一种,自动平衡,由 Tarjan 提出并实现。得名于特有的 Splay 操作。
Splay操作:将节点u通过单旋、双旋移动到某一个指定位置。
主要目的是将访问频率高的节点在不改变原顺序的前提下移动到尽量靠近根节点的位置,以此来解决同一个(相似)问题的多次查询。
但是在非降序查询每一个节点后,Splay 树会变为一条链,降低运算效率。
◇原理&细解
(1)旋转操作
二叉排序树必须满足 左儿子<根节点<右儿子 ,即使在旋转过后也是如此。因此旋转操作(Rotate)是Splay平衡树的一个重要组成部分。而在Splay操作中,旋转分单旋和双旋。
单旋:
由于Rotate分成两种情况,许多OIer直接把两种情况分类讨论写在程序里,这样就使得Rotate()函数及其之长。但是老师教了我们一个不错的俭省代码的方法(~^o^~):
首先我们定义x的左儿子为 tree[x].ch[0],右儿子为 ch[1],再在Rotate()函数的参数表中加上"d",d=1表示右旋,0表示左旋。
void Rotate(Node *x,int d)
{
Node *y=x->fa;
y->ch[!d]=x->ch[d];x->fa=y->fa;
if(x->ch[d]!=NULL) x->ch[d]->fa=y;
if(y->fa!=NULL) y->fa->ch[y->fa->ch[]==y]=x;
y->fa=x;x->ch[d]=y;
if(y==root) root=x;
Update(y);
}
完美地契合了上图的规律,从而达到简短代码的目的!
双旋:
不用怎么解释……其实就是3个点(儿子X,父亲Y,祖父Z)之间将儿子X转移到祖父Z位置的2次旋转操作。第一次旋转能够将儿子X旋转到父亲Y位置,此时的旋转和祖父Z没有关系,就看成X,Y的旋转;第一次旋转后,Y就成了X的一棵子树,所以第二次旋转是Z和X之间的……总而言之就是两次单旋,只是注意旋转方向,保证原有关系不变。
举个例子:
reader 们可以把剩下的3种自己试一试,有什么不懂的可以在文末的邮箱处ask我 (^.^)~
(2)SPLAY操作
实质是旋转的组合……
作为Splay树的核心,它能够实现将指定节点旋转到某一个位置(或某一个节点的儿子的位置)的操作。通过Splay操作,我们可以每一次将查询的节点向高处提,从而下一次访问该节点时速度加快。
设当前需要转移的节点为x,节点y,z分别是它的父亲,祖父,x需要转移到节点rt的下方。由于每一次Rotate操作每一次可以使节点上移一层(目的一般不会是下移),如果z就是rt,就说明y是x要到达的地方(因为z的下面就是y),而x到y只需要一次Rotate,因此调用单旋。
其他情况下至少需要两次Rotate操作,即双旋。直到到达目标位置为止。
如何判断是左旋还是右旋?
我们很容易发现一个规律——如果要使V上移到U(U是V的父亲),当V是U的左儿子时,我们需要右旋,而V是U的右儿子时,需要左旋……也就是说儿子的左右和旋转方向的左右是恰好相反的。
(3)查找树中是否存在某个节点
这是所有操作中最简单的一个,只用到了二叉排序树的性质。
设查找点的值为val,从根节点开始查找,设当前查找到的点值为u。由于根的左子树小于根,而右子树大于根,所以u>val时向左子树查询,否则向右子树查询,直到查找到值或者当前节点为空NULL。
(4)插入一个特定值的节点
基本思想和查找节点很像,也是根据二叉排序树来确定位置。
当我们找到一个值恰好为特定值的节点,则将该节点的个数+1,不再插入节点了。与查找不同的是它如果按顺序查找节点,发现该节点为NULL,就说明没有值为val的节点,此时我们会新建一个值为val的节点插入到那个为NULL的节点。
(5)查询点排名以及特定排名的点
这里的排名不包括并列(2,3,3,4 3的排名为2或3,4的排名为4)。其实就是比他小(严格小于)的元素个数+1,而比他小的元素恰好就是他的左子树,因此也就是它的左子树的个数+1。
查找特定排名的点要麻烦一些……设当前节点为u,当u的左子树+1大于排名,则说明当前数过大,向左子树查询,否则向右子树查询。若查询右子树,则先将特定排名减去当前节点的左子树大小,表示在右子树中需要找到第"特定排名减去当前节点的左子树大小"大的元素。
换句话说,当前节点为u,向u的右子树查询,则目标节点在u的右子树中的排名为 (以u为根的子树中的排名 - u的左子树大小)。
(6)删除特定值的点
还是先像查找特定值的节点的思路,先找到要删除的节点的位置。由于我把值相同的点压缩在了一个点上,值相同的点的个数为cnt。当cnt>1时,即不止一个点值为特定值,我们可以直接cnt--;如果cnt=1,则删除该点后,该点就没了……这时候我们需要处理节点与其前驱后继的关系。我们可以把前驱通过Splay移动到根节点,而把后继移到前驱的右儿子。我们会发现后继的左儿子就是要删除的节点,且它没有儿子(叶结点),所以我们直接把左儿子改为NULL,再Update更新节点个数,好像就完了(=^-ω-^=)
◇手打简单模板
(PS.下面这个模板实现了插入Insert,删除Delete(无法判断是否存在该元素),查找节点GetKey,正反向查询排名Find_Count/Get_Num,查找前驱后继FrontBehind)
/*Lucky_Glass*/
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
struct Node{
Node *ch[],*fa; //ch[0]左儿子,ch[1]右儿子
int v,cnt,size; //v点权,cnt点权为v的点数量,子树大小(包括根节点)
Node(int v):v(v){ //初始化
fa=ch[]=ch[]=NULL;
cnt=;
}
int cmp(int x)const { //某时候极其方便的比较函数
if(x==v) return -;
return x<v? :;
}
}*root; //树根
int Get_Size(Node *p) //避免点为NULL时访问size错误
{
return p==NULL? :p->size;
}
void Update(Node *x) //上传子树大小
{
x->size=+Get_Size(x->ch[])+Get_Size(x->ch[]);
}
void Rotate(Node *x,int d) //旋转,d=0左旋,d=1右旋
{
Node *y=x->fa;
y->ch[!d]=x->ch[d];x->fa=y->fa;
if(x->ch[d]!=NULL) x->ch[d]->fa=y;
if(y->fa!=NULL) y->fa->ch[y->fa->ch[]==y]=x;
y->fa=x;x->ch[d]=y;
if(y==root) root=x;
Update(y);
}
void Splay(Node *x,Node *rt)
{
while(x->fa!=rt) //直到到达目标位置为止
{
Node *y=x->fa;Node *z=y->fa;
if(z==rt) //只旋转一次即到目标位置
if(x==y->ch[]) Rotate(x,);
else Rotate(x,);
else //双旋
if(y==z->ch[])
if(x==y->ch[])
Rotate(y,),Rotate(x,);
else
Rotate(x,),Rotate(x,);
else
if(x==y->ch[])
Rotate(y,),Rotate(x,);
else
Rotate(x,),Rotate(x,);
}
Update(x);
}
void Insert(int val) //插入值为val的节点
{
if(root==NULL) {root=new Node(val);return;}
//插入节点
Node *y=root;
while(true)
{
if(val==y->v) {y->cnt++;Splay(y,NULL);return;}
//如果已经存在值为val的节点,则该节点个数+1
Node *&ch=(val<y->v? y->ch[]:y->ch[]);
if(ch==NULL) break;
y=ch;
}
Node *x=new Node(val);
(val<y->v? y->ch[]:y->ch[])=x;
x->fa=y;
Splay(x,NULL);
}
Node *Find(Node *x,int d) //寻找前驱后继(d=0前驱,d=1后继),只能寻找已存在于树中的值
{
while(x && x->ch[d]) x=x->ch[d];
return x;
}
void Delete(int num) //删除一个值为num的节点
{
Node *p=root;
while(true)
{
if(!p) return;
if(p->v==num)
{
Splay(p,NULL);
if(p->cnt==) //单个节点
{
Node *Front=Find(p->ch[],),
*Behind=Find(p->ch[],); //处理前驱后继
if(!Front && !Behind) root=NULL;
else if(!Front) root=root->ch[],root->fa=NULL;
else if(!Behind) root=root->ch[],root->fa=NULL;
else
{
Splay(Front,NULL);
Splay(Behind,root);
root->ch[]->ch[]=NULL;
root->ch[]->size--;
}
}
else p->cnt--; //减少个数
return;
}
p=p->v>num? p->ch[]:p->ch[];
}
}
Node *GetKey(Node *o,int x) //根据二叉排序树关系查找值为x的节点
{
int d=o->cmp(x);
if(d==-) return o;
return GetKey(o->ch[d],x);
}
int Find_count(int val) //找到值为val的节点在树上的排名
{
Node *x=GetKey(root,val);
Splay(x,NULL);
return Get_Size(x->ch[])+;
}
int Get_Num(int num) //找到排名为num的数
{
Node *now=root;
while(now)
{
if(num>=Get_Size(now->ch[])+ && num<=Get_Size(now->ch[])+now->cnt)
break;
if(Get_Size(now->ch[])>=num) now=now->ch[];
else
{
num-=Get_Size(now->ch[])+now->cnt;
now=now->ch[];
}
}
Splay(now,NULL);
return now->v;
}
int FrontBehind(int num,int d) //找前驱后继(不一定在树上)
{
Insert(num);
int res=Find(root->ch[d^],d)->v;
Delete(num);
return res;
}
int main()
{
while(true)
{
int cmd,x;
scanf("%d%d",&cmd,&x);
switch(cmd)
{
case : Insert(x);break;
case : Delete(x);break;
case : printf("%d\n",Find_count(x));break;
case : printf("%d\n",Get_Num(x));break;
case : printf("%d\n",FrontBehind(x,));break;
case : printf("%d\n",FrontBehind(x,));break;
}
}
return ;
}
这个代码风格可能比较奇怪,因为是从几个不同的代码裁剪修改,然后组合起来的……(∩╹□╹∩)
The End
Thanks for reading!
- Lucky_Glass
【学时总结】◆学时·VI◆ SPLAY伸展树的更多相关文章
- Splay伸展树学习笔记
Splay伸展树 有篇Splay入门必看文章 —— CSDN链接 经典引文 空间效率:O(n) 时间效率:O(log n)插入.查找.删除 创造者:Daniel Sleator 和 Robert Ta ...
- Splay 伸展树
废话不说,有篇论文可供参考:杨思雨:<伸展树的基本操作与应用> Splay的好处可以快速分裂和合并. ===============================14.07.26更新== ...
- [Splay伸展树]splay树入门级教程
首先声明,本教程的对象是完全没有接触过splay的OIer,大牛请右上角.. 首先引入一下splay的概念,他的中文名是伸展树,意思差不多就是可以随意翻转的二叉树 PS:百度百科中伸展树读作:BoGa ...
- Splay伸展树入门(单点操作,区间维护)附例题模板
Pps:终于学会了伸展树的区间操作,做一个完整的总结,总结一下自己的伸展树的单点操作和区间维护,顺便给未来的自己总结复习用. splay是一种平衡树,[平均]操作复杂度O(nlogn).首先平衡树先是 ...
- Codeforces 675D Tree Construction Splay伸展树
链接:https://codeforces.com/problemset/problem/675/D 题意: 给一个二叉搜索树,一开始为空,不断插入数字,每次插入之后,询问他的父亲节点的权值 题解: ...
- UVA 11922 Permutation Transformer —— splay伸展树
题意:根据m条指令改变排列1 2 3 4 … n ,每条指令(a, b)表示取出第a~b个元素,反转后添加到排列尾部 分析:用一个可分裂合并的序列来表示整个序列,截取一段可以用两次分裂一次合并实现,粘 ...
- [算法] 数据结构 splay(伸展树)解析
前言 splay学了已经很久了,只不过一直没有总结,鸽了好久来写一篇总结. 先介绍 splay:亦称伸展树,为二叉搜索树的一种,部分操作能在 \(O( \log n)\) 内完成,如插入.查找.删除. ...
- ZOJ3765---Lights (Splay伸展树)
Lights Time Limit: 8 Seconds Memory Limit: 131072 KB Now you have N lights in a line. Don't wor ...
- codeforces 38G - Queue splay伸展树
题目 https://codeforces.com/problemset/problem/38/G 题意: 一些人按顺序进入队列,每个人有两个属性,地位$A$和能力$C$ 每个人进入时都在队尾,并最多 ...
随机推荐
- (转)AIX 5.3安装SSH .
AIX 5.3安装SSH . 原文:http://blog.csdn.net/chunhua_love/article/details/12004845 环境:OS:AIX 5.3SSH: opens ...
- pat04-树8. Complete Binary Search Tree (30)
04-树8. Complete Binary Search Tree (30) 时间限制 100 ms 内存限制 65536 kB 代码长度限制 8000 B 判题程序 Standard 作者 CHE ...
- SQLite的使用
通常在大型系统和网站一般使用的数据基本上就是Oracle,MySQL,MSSqlServer三种.但是在某些情况下会相对麻烦,如果仅仅需要在客户端保存一些数据.SQLite无疑是最佳选择之一.他是一种 ...
- python的数字
1.整数 与其它语言不同点两个乘号(**)代表 乘方运算 .前面代表底数,后面代表指数. 2.浮点数 它的小数位数可能是不确定的,需要注意 3.使用str()避免错误 在输出字符串时,可以避免类型错误 ...
- UrShop 商城系统介绍
UrShop能够帮助企业快速构建个性.高效.稳定.安全的网上商城并减少二次开发带来的成本.对于网店来说,UrShop除了安装便捷,功能上强大以外,操作上也非常方便快捷.优社电商秉承设身处地为客户着想的 ...
- js数组Array方法
1. indexOf indexOf()方法返回在该数组中第一个找到的元素位置,如果它不存在则返回-1. var fruits = ["Banana", "Orange& ...
- Python的历史与基本知识入门
一.Python简介 1.1989年由"龟叔"Guido van Rossum在圣诞节期间打发无聊时间编写. 2.Python是一门弱类型解释性语言. 3.优点:代码简洁,明确,优 ...
- 在 CentOS7 上安装 swftools
1.从官网下载 swftools,这里下载的是 0.9.2 版本: wget http://www.swftools.org/swftools-0.9.2.tar.gz 2.下载后得到 swftool ...
- SourceTree 跳过登陆
当前只有Win的版本,Mac自行百度(笑) 很多人用git命令行不熟练,那么可以尝试使用sourcetree进行操作. 然鹅~~sourcetree又一个比较严肃的问题就是,很多人不会跳过注册或者操作 ...
- CSS中的鼠标样式明细
<INPUT TYPE="submit" style="cursor: hand" value="hand"> ...