浅谈splay(点的操作)

一、基本概念

splay本质:二叉查找树

特点:结点x的左子树权值都小于x的权值,右子树权值都大于x的权值

维护信息:

整棵树:root 当前根节点  sz书上所有结点编号

结点:f[] 父节点编号    ch[][2] 孩子结点编号,0左1右

siz[] 以结点为根的子树大小(包括自己)   cnt[]自己出现的次数

key[] 结点权值

二、基本操作

插入insert、删除del、查询x的排名findpos、查询排名为x的数findx、查找前驱pre、查找后继nex

核心操作:伸展操作splay

part 1:这么多操作难免会更改节点信息,我们先思考如何维护这些信息

siz,cnt 可以这样维护

void update(int x)
{
    siz[x]=cnt[x];
    if(ch[x][0]) siz[x]+=siz[ch[x][0]];
    if(ch[x][1]) siz[x]+=siz[ch[x][1]];
}

f,ch,root 要在splay操作中修改

splay操作:就是讲结点x不断旋转至根节点

旋转过程谁成为谁的左右孩子,自己根据大小关系判断总结即可

旋转代码:

int getson(int x)
{
    return ch[f[x]][1]==x;
}
void rotate(int x)
{
    int fa=f[x],fafa=f[fa],k=getson(x);
    ch[fa][k]=ch[x][k^1];f[ch[fa][k]]=fa;//对应蓝色线,调整x另一方向的孩子和x父节点的关系
    ch[x][k^1]=fa;f[fa]=x;//对应红色线 ,调整x和父节点的关系
    f[x]=fafa;
    if(fafa) ch[fafa][ch[fafa][1]==fa]=x;//对应紫色线 ,调整x和父节点的父节点的关系
    update(fa);update(x);
}

小细节:为什么先update(fa),再update(x) ,因为旋转前,fa是x的父节点,经旋转后,fa变为x的孩子节点,update操作是根据左右孩子子树大小更新的

调用:(双旋) 个人对于双旋的一点理解:http://www.cnblogs.com/TheRoadToTheGold/p/6372344.html

void splay(int x)
{
    for(int fa;fa=f[x];rotate(x))
     if(f[fa]) rotate(getson(x)==getson(fa) ? fa :x);
    root=x;
}

小细节:fa=f[x],1、每执行一次旋转,更新一次da   2、fa==true时才进行

part 2:

A、插入数x  insert(int x)

分为3种情况

1、树为空,直接插入x,并让x成为根节点

2、树不为空  ①树中已有x,x出现次数+1,旋转x至根节点

②树中没有x,在适当位置插入x,旋转x至根节点

void create(int x)
{
    sz++;key[sz]=x;
    cnt[sz]=siz[sz]=1;
    ch[sz][0]=ch[sz][1]=f[sz]=0;
}
void insert(int x)//插入结点x
{
    if(!root) create(x),root=sz;//splay为空
    else
    {
        int now=root,fa=0;
        while(1)
        {
            if(key[now]==x)//树中有x
            {
                cnt[now]++;
                splay(now);
                break;
            }
            fa=now;
            now=ch[fa][x>key[fa]];
            if(!now)
            {
                create(x);
                f[sz]=fa;
                ch[fa][x>key[fa]]=sz;
                splay(sz);
                break;
            }
        }
    }
}

小细节:为什么要splay?仅仅是插入不是插进去就行吗?成不成为根节点有什么关系?

这是为了查找比x小/大的第一个数做铺垫,因为有可能x在树中没有出现过,所以先插入x,再找前驱/后继,这就可以直接从根节点找起,不用再找一次x的位置,最后删除x

(下面的E、F)

B、查询排名为x的数(从小到大)

记得平衡树怎么查找第k小吗?——如果左子树大小<k,找左孩子,否则找右孩子。

类比一下可以得出

1、如果x<当前点左子树大小,找左孩子,这里注意一个小细节是要先判断当前点是否有左孩子

2、否则  定义变量temp=当前结点出现次数+结点左子树大小

①、如果x<=temp 那么这个结点就是答案

因为既然x>=当前点左子树大小,那么他要么是当前点,要么在当前点的右子树

又因为x<=当前点+左子树大小+当前点出现次数,那么他是当前点

②、如果x>temp 那么x减去temp,找右孩子

int findx(int x)
{
    int now=root;
    while(1)
    {
        if(ch[now][0]&&x<=siz[ch[now][0]]) now=ch[now][0];//千万不要漏了ch[now][0]==true
        else
        {
            int temp=(ch[now][0] ? siz[ch[now][0]] : 0)+cnt[now];
            if(x<=temp) return key[now];
            x-=temp;
            now=ch[now][1];
        }
    }
}

C、查询x的排名

想想splay本质是二叉查找树,不难得出

1、如果x<当前节点权值  查找左孩子

2、否则 ,先令ans加上当前节点左子树大小

①、如果x=当前节点权值,旋转当前节点至根节点,返回ans+1

因为此时ans不包括当前节点,所以要+1

②、如果x>当前节点权值,ans加上当前节点出现次数,查找右孩子

int findpos(int x)
{
    int now=root,ans=0;
    while(1)
    {
        if(x<key[now]) now=ch[now][0];
        else
        {
            ans+=ch[now][0] ? siz[ch[now][0]] : 0;
            if(x==key[now])
            {
                splay(now);
                return ans+1;
            }
            ans+=cnt[now];
            now=ch[now][1];
        }
    }
}

小细节:为什么要splay?

为了下面的删除操作做铺垫,删除数x需要先找到x的位置,删除操作是在x是根节点的基础上进行的(下面的F)

D、查找比x小的第一个数

这就有2种可能:x在树中,x不在树中

x在树中就是查找x的前驱,那么不在树中呢?

我们可以向在树中插入x,在查找前驱,最后再删除x

如何查找前驱? 转向x的左孩子l,然后在l的子树里一直往右找

调用代码:

insert(x);printf("%d\n",key[pre()]);del(x);break;

查找前驱代码:

int pre()
{
    int now=ch[root][0];
    while(ch[now][1]) now=ch[now][1];
    return now;
}

E、查询比x大的第一个数

同理D

直接给代码

insert(x);printf("%d\n",key[nex()]);del(x);break;
int nex()
{
    int now=ch[root][1];
    while(ch[now][0]) now=ch[now][0];
    return now;
}

F、删除数x

分为5种情况

首先,你要先找到x在哪儿,将其旋转至根节点,这里可以直接调用findpos函数

然后,分类讨论(此时根节点就是数x,所以此后操作变为删除根节点)

删除:结点所有信息清0即可

void clear(int x)
{
    ch[x][0]=ch[x][1]=cnt[x]=siz[x]=f[x]=key[x]=0;
}

1、根节点在splay树中出现次数>1  根节点的出现次数-1,子树大小-1

if(cnt[root]>1)
    {
        cnt[root]--;siz[root]--;
        return;
    }

2、否则 ①  根节点既没有左孩子又没有右孩子,说明树中只有这一个结点,直接删去,并 root=0

if(!ch[root][0]&&!ch[root][1])
    {
        clear(root);
        root=0;//千万不要漏了这一句
        return;
    }

② 根节点没有左孩子,说明树左边为空,那么只需把根节点的右孩子提为根节点,删除原根节点  小细节:新根节点的父节点置为0

    if(!ch[root][0])
    {
        int tmp=root;
        root=ch[root][1];
        f[root]=0;//不要漏了它
        clear(tmp);
        return;
    }

③ 根节点没有右孩子,与②同理

 if(!ch[root][1])
    {
        int tmp=root;
        root=ch[root][0];
        f[root]=0;
        clear(tmp);
        return;
    }

④ 根节点既有左孩子又有右孩子

我们可以先把x的前驱l旋转为根节点

手动模拟一下过程可以发现:

在l成为根节点的前一步,一定是x的左孩子 ,这说明了l成为根节点后,x不会有左孩子

那么我们就可以直接把x的右孩子提到x的位置,删除x即可

int pre1=pre(),tmp=root;//tmp现在相当于x的位置
    splay(pre1);//x前驱旋转为根节点 ,经过此操作后,根节点变为x的前驱
    ch[root][1]=ch[tmp][1];//x的右孩子提到x的位置
    f[ch[tmp][1]]=root;//更新父节点
    clear(tmp);//删除x
    update(root);

为什么要在x是根节点的基础上执行删除操作?

因为splay要维护cnt、siz等信息

如果x不是根节点,x删除,x以上所有结点关于个数之类的信息都要更改

而如果x是根节点,x删除,不会影响其他结点

splay  插入insert、删除del、查询x的排名findpos、查询排名为x的数findx、查找前驱pre(第一个比x小的数)、查找后继nex(第一个比x大的数)完整代码

题目描述

您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:

1.插入x数

2.删除x数(若有多个相同的数,因只删除一个)

3.查询x数的排名(若有多个相同的数,因输出最小的排名)

4.查询排名为x的数

5.求x的前驱(前驱定义为小于x,且最大的数)

6.求x的后继(后继定义为大于x,且最小的数)

输入输出格式

输入格式:
第一行为n,表示操作的个数,下面n行每行有两个数opt和x,opt表示操作的序号(1<=opt<=6)

输出格式:
对于操作3,4,5,6每行输出一个数,表示对应答案

代码背景简述

题目来源:https://www.luogu.org/problem/show?pid=3369

#include<cstdio>
#define N 1000000
using namespace std;
int f[N],ch[N][2],key[N],cnt[N],siz[N],sz,root;
void update(int x)
{
    siz[x]=cnt[x];
    if(ch[x][0]) siz[x]+=siz[ch[x][0]];
    if(ch[x][1]) siz[x]+=siz[ch[x][1]];
}

int pre()
{
    int now=ch[root][0];
    while(ch[now][1]) now=ch[now][1];
    return now;
}
int nex()
{
    int now=ch[root][1];
    while(ch[now][0]) now=ch[now][0];
    return now;
}
int getson(int x)
{
    return ch[f[x]][1]==x;
}
void rotate(int x)
{
    int fa=f[x],fafa=f[fa],k=getson(x);
    ch[fa][k]=ch[x][k^1];f[ch[fa][k]]=fa;
    ch[x][k^1]=fa;f[fa]=x;
    f[x]=fafa;
    if(fafa) ch[fafa][ch[fafa][1]==fa]=x;
    update(fa);update(x);
}
void splay(int x)
{
    for(int fa;fa=f[x];rotate(x))
     if(f[fa]) rotate(getson(x)==getson(fa) ? fa :x);
    root=x;
}
int findpos(int x)
{
    int now=root,ans=0;
    while(1)
    {
        if(x<key[now]) now=ch[now][0];
        else
        {
            ans+=ch[now][0] ? siz[ch[now][0]] : 0;
            if(x==key[now])
            {
                splay(now);
                return ans+1;
            }
            ans+=cnt[now];
            now=ch[now][1];
        }
    }
}
int findx(int x)
{
    int now=root;
    while(1)
    {
        if(ch[now][0]&&x<=siz[ch[now][0]]) now=ch[now][0];//千万不要漏了ch[now][0]==true
        else
        {
            int temp=(ch[now][0] ? siz[ch[now][0]] : 0)+cnt[now];
            if(x<=temp) return key[now];
            x-=temp;
            now=ch[now][1];
        }
    }
}
void clear(int x)
{
    ch[x][0]=ch[x][1]=cnt[x]=siz[x]=f[x]=key[x]=0;
}
void create(int x)
{
    sz++;key[sz]=x;
    cnt[sz]=siz[sz]=1;
    ch[sz][0]=ch[sz][1]=f[sz]=0;
}
void insert(int x)
{
    if(!root) create(x),root=sz;
    else
    {
        int now=root,fa=0;
        while(1)
        {
            if(key[now]==x)
            {
                cnt[now]++;
                splay(now);
                break;
            }
            fa=now;
            now=ch[fa][x>key[fa]];
            if(!now)
            {
                create(x);
                f[sz]=fa;
                ch[fa][x>key[fa]]=sz;
                splay(sz);
                break;
            }
        }
    }
}

void del(int x)
{
    int t=findpos(x);
    if(cnt[root]>1)
    {
        cnt[root]--;siz[root]--;
        return;
    }
    if(!ch[root][0]&&!ch[root][1])
    {
        clear(root);
        root=0;
        return;
    }
    if(!ch[root][0])
    {
        int tmp=root;
        root=ch[root][1];
        f[root]=0;
        clear(tmp);
        return;
    }
    if(!ch[root][1])
    {
        int tmp=root;
        root=ch[root][0];
        f[root]=0;
        clear(tmp);
        return;
    }
    int pre1=pre(),tmp=root;
    splay(pre1);
    ch[root][1]=ch[tmp][1];
    f[ch[tmp][1]]=root;
    clear(tmp);
    update(root);
}
int main()
{
    int n,opt,x;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d%d",&opt,&x);
        switch(opt)
        {
            case 1 :insert(x);break; //插入x
            case 2 :del(x);break;//删除x
            case 3 :printf("%d\n",findpos(x));break;//查询x的排名
            case 4 :printf("%d\n",findx(x));break;//查询排名为x的数
            case 5 :insert(x);printf("%d\n",key[pre()]);del(x);break;//查找第一个小于x的数
            case 6 :insert(x);printf("%d\n",key[nex()]);del(x);break;//查找第一个大于x的数
        }
    }
}

时间: 2024-10-05 23:05:38

浅谈splay(点的操作)的相关文章

浅谈.net中数据库操作事务

.net中的事务 关键几点 概念:1:什么是事务 2:什么时候用事务 3:基本的语法 (1): 事务(Transaction)是访问并可能更新数据库中各种数据项的一个程序执行单元(unit).事务通常由高级数据库操纵语言或编程语言(如SQL,C++或Java)书写的用户程序的执行所引起,并用形如begin transaction和end transaction语句(或函数调用)来界定.事务由事务开始(begin transaction)和事务结束(end transaction)之间执行的全体操

浅谈splay(双旋)

今天刚刚学习完splay,讲一下自己的想法吧 首先splay和treap不一样,treap通过随机数来调整树的形态.但splay不一样,再每插入或操作一次后,你都会把他旋转到根,再旋转过程中树的形态会不断改变,这样来达到均摊效果 常数据大. 来看看具体实现吧 首先定义数组,\(size\) 子树大小(包括自己),\(fa\) 节点的父亲,\(key\) 该节点的权值,\(cnt\) 该节点权值出现次数,$ch $表示儿子 0表左二子,1表右儿子 首先看几个简单函数 inline void upd

浅谈Notepad++选中行操作+快捷键+使用技巧【超详解】

Notepad++选中行操作 快捷键 使用技巧 用Notepad++写代码,要是有一些重复的代码想copy一下,还真不容易,又得动用鼠标,巨烦人.... 有木有简单的方法呢,确实还是有的不过也不算太好用. 主要是应用键盘上的 Home 键 和 End 键. 鼠标光标停留在一行的某处,按 Home 键光标会跳到行首,按End键光标会跳到行尾. 鼠标光标停留在行尾,按 Shift + Home 选中一行. 鼠标光标停留在行首,按 Shift + End 选中一行. 鼠标光标停留在类中某处,按 Shi

浅谈C++的文件操作(上)

今天正在做C++的课程设计,碰到文件操作问题,自己琢磨了半天,现在总结一下,有需要的可以参考一下(特别是新手) C++ 通过以下几个类支持文件的输入输出: ofstream: 写操作(输出)的文件类ifstream: 读操作(输入)的文件类fstream: 可同时读写操作的文件类 1.打开文件 对这些类的一个对象所做的第一个操作通常就是将它和一个真正的文件联系起来,也就是说打开一个文件.被打开的文件在程序中由一个流对象(stream object)来表示 (这些类的一个实例) ,而对这个流对象所

浅谈JavaScript中数组操作常用方法

JavaScript中数组操作常用方法 1.检测数组 1)检测对象是否为数组,使用instanceof 操作符 if(value instanceof Array) { //对数组执行某些操作 } 2)获取对象的类型,比较是否为object类型(此方法只能检测是否为Object,不推荐) if(typeof(value)=="Object") { //对数组执行某些操作 } 3)检测对象是否为数组,使用Array.isArray()方法(只支持ie9+,firefox 4+,safar

浅谈MySQL多表操作

字段操作 create table tf1( id int primary key auto_increment, x int, y int ); # 修改 alter table tf1 modify x char(4) default ''; alter table tf1 change y m char(4) default ''; # 增加 mysql>: alter table 表名 add 字段名 类型[(长度) 约束]; # 末尾 eg>: alter table tf1 add

浅谈“Mysql”的基础操作语句

/*-------------------------------------------读者可以补充内容到下面--------------------------------------------------*/ //修改表名alter table qq_user rename user;//修改字段数据类型alter table user modify qq_id int;//修改字段名alter table user change qq_id Id int;//修改字段名和字段数据类型a

【内存类操作】浅谈内存拷贝异常

结合本人在实际项目中所积累的经验,以及曾经犯过的错误,堆内存操作类函数做一个简单的剖析,抛砖引玉,欢迎大家吐槽. 首先,讲一下内存使用异常发生的几种场景. 1.野指针的使用,使用已经释放的指针,如果向野指针中写内容,就极有可能导致设备重启或任务挂死.因为,正在运行的任务的地址被意外的改写. [避免策略]函数入参要判空,指针使用(包括释放)之前一定要释放. 2.内存函数的错误使用: void *memset(void *s, int ch, size_t n); c语言中在<memory.h>或

Js之浅谈dom操作

JavaScript之浅谈dom操作 1.理解dom: DOM(Document Object Model ,文档对象模型)一种独立于语言,用于操作xml,html文档的应用编程接口. 怎么说,我从两个角度理解: 对于JavaScript,为了能够使JavaScript操作Html,JavaScript就有了一套自己的dom编程接口. 对于Html,dom使得html形成一棵dom树,类似于一颗家族树一样,一层接一层,子子孙孙. 所以说,有了DOM,在我看来就是相当于JavaScript拿到了钥