◆学时·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

(Tab:如果我有没讲清楚的地方可以直接在邮箱lucky_glass@foxmail.com email我,在周末我会尽量解答并完善博客~)

【学时总结】◆学时·VI◆ SPLAY伸展树的更多相关文章

  1. Splay伸展树学习笔记

    Splay伸展树 有篇Splay入门必看文章 —— CSDN链接 经典引文 空间效率:O(n) 时间效率:O(log n)插入.查找.删除 创造者:Daniel Sleator 和 Robert Ta ...

  2. Splay 伸展树

    废话不说,有篇论文可供参考:杨思雨:<伸展树的基本操作与应用> Splay的好处可以快速分裂和合并. ===============================14.07.26更新== ...

  3. [Splay伸展树]splay树入门级教程

    首先声明,本教程的对象是完全没有接触过splay的OIer,大牛请右上角.. 首先引入一下splay的概念,他的中文名是伸展树,意思差不多就是可以随意翻转的二叉树 PS:百度百科中伸展树读作:BoGa ...

  4. Splay伸展树入门(单点操作,区间维护)附例题模板

    Pps:终于学会了伸展树的区间操作,做一个完整的总结,总结一下自己的伸展树的单点操作和区间维护,顺便给未来的自己总结复习用. splay是一种平衡树,[平均]操作复杂度O(nlogn).首先平衡树先是 ...

  5. Codeforces 675D Tree Construction Splay伸展树

    链接:https://codeforces.com/problemset/problem/675/D 题意: 给一个二叉搜索树,一开始为空,不断插入数字,每次插入之后,询问他的父亲节点的权值 题解: ...

  6. UVA 11922 Permutation Transformer —— splay伸展树

    题意:根据m条指令改变排列1 2 3 4 … n ,每条指令(a, b)表示取出第a~b个元素,反转后添加到排列尾部 分析:用一个可分裂合并的序列来表示整个序列,截取一段可以用两次分裂一次合并实现,粘 ...

  7. [算法] 数据结构 splay(伸展树)解析

    前言 splay学了已经很久了,只不过一直没有总结,鸽了好久来写一篇总结. 先介绍 splay:亦称伸展树,为二叉搜索树的一种,部分操作能在 \(O( \log n)\) 内完成,如插入.查找.删除. ...

  8. ZOJ3765---Lights (Splay伸展树)

    Lights Time Limit: 8 Seconds      Memory Limit: 131072 KB Now you have N lights in a line. Don't wor ...

  9. codeforces 38G - Queue splay伸展树

    题目 https://codeforces.com/problemset/problem/38/G 题意: 一些人按顺序进入队列,每个人有两个属性,地位$A$和能力$C$ 每个人进入时都在队尾,并最多 ...

随机推荐

  1. Lua 遍历Linux目录下的文件夹

    代码如下,里面有注释,应该能看懂. function getFile(file_name) local f = assert(io.open(file_name, 'r')) local string ...

  2. hql基础入门

    [转]进入HQL世界 一个ORM框架是建立在面向对象的基础上的.最好的例子是Hibernate如何提供类SQL查询.虽然HQL的语法类似于SQL,但实际上它的查询目标是对象.HQL拥有面向对象语言的所 ...

  3. hdu 4044 树形DP 炮台打怪 (好题)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=4044 题目大意:给定n个节点组成的树,1为敌方基地,叶子结点为我方结点.我们可以在每个结点安放炮台,至 ...

  4. 最小白的webpack+react环境搭建

    本文也同步发表在我的公众号“我的天空” 从零开始,用最少的配置.最少的代码.最少的依赖来搭建一个最简单的webpack+react环境. 最近在玩webpack+react+移动端,那么第一步自然是搭 ...

  5. ArcGIS数据存储的方式

    工作空间文件夹和地理数据库为存储和管理ArcGIS地理信息提供了主要容器.工作空间文件夹只是磁盘上的普通文件夹,它可保存大量的文件地理数据库.个人地理数据库.基于文件的数据集以及一系列ArcGIS文档 ...

  6. Gremlin--一种支持对图表操作的语言

    Gremlin 是操作图表的一个非常有用的图灵完备的编程语言.它是一种Java DSL语言,对图表进行查询.分析和操作时使用了大量的XPath. Gremlin可用于创建多关系图表.因为图表.顶点和边 ...

  7. android selector中使用shape

    <shape> <!-- 实心 -->     <solid android:color="#ff9d77"/> <!-- 渐变 --&g ...

  8. 通过adb获取应用的Activity堆栈信息

    获取所用应用 adb shell dumpsys activity 获取自己的应用 adb shell dumpsys activity | grep 应用的package 获取处于栈顶的activi ...

  9. JavaMail 的简单使用

    概述 邮件功能模块在大多数网站中,都是必不可少的功能模块.无论是用户注册还是重置密码,邮件都是比较常用的一个方式.本文主要介绍 JavaMail 的简单使用,方便大家快速开发,供大家参考.完整的 de ...

  10. 怎样解决putty终端乱码的方法

    原文地址:https://jingyan.baidu.com/article/3aed632e5f00ae701080913a.html?qq-pf-to=pcqq.c2c 终端输入:echo $LA ...