来自两年后的提示

本篇文章只是娱乐向的介绍性文章,可以进行初步理解。

\(\text{Splay}\)如果需要严格的证明均摊复杂度参考势能分析。

另外\(\text{Splay}\)依靠\(rotate\)来维护\(size\)等节点维护的值。

如果代码中没有体现请不要忘记上面这句话。

另外本文中很多内容经不起推敲,然而我懒得改了。。。

QwQ......


BST真是神奇的东西。。。

而且种类好多呀。。。

我这个蒟蒻只学会了splay

orzCJ老爷,各种树都会

好好好,不说了,直接说splay。


不知道splay是啥,,你也要知道平衡树是啥。。。

平衡树是一个神奇的数据结构,

对于任意一个节点,左儿子的值比它小,右儿子的值比它大

并且任意一棵子树单独拎出来也是一棵平衡树

就像这样。。。。

各位大佬请原谅我丑陋无比的图

上面这个丑陋的东西就是一棵平衡树,他现在很平衡,是一棵满二叉树,高度正好是logn。。。

但是。。

如果这个丑陋的东西极端一点,他就会变成这样。。。

这张图依然很丑

现在看起来,这个东西一点都不平衡。。。

二叉树退化成了一条链

如果要查询的话,,,最坏情况下就变成了O(n)

这就很尴尬了。。。


各位大佬们为了解决平衡树这个尴尬的问题,想出了各种方法。。

也就是弄出了各种树。。。。(然而cj大佬都会)

然后有一个注明的大佬叫做Tarjan,弄出了splay这个玩意。。。


这个玩意怎么解决上面的问题呢???

你是一个平衡树是吧。。。

我把你的节点的顺序修改一下,让你还是一棵平衡树,在这个过程中你的结构就变化了,就可能不再是一条链了。

诶,这个看起来很厉害的感觉。。。

但是,,我怎么说也说不清呀。。

弄张丑陋的图过来

这是一个丑陋的平衡树的一部分

其中XYZ三个是节点,ABC三个是三棵子树

现在这个玩意,我如果想把X弄到Y那个地方去要怎么办,这样的话我就经过了旋转,重构了这棵树的结构,就可能让他变得更加平衡

恩,我们来看看怎么办。。。

X是Y的左儿子,所以X < Y

Y是Z的左儿子,所以Y < Z

所以X < Z,所以如果要把X弄到Y的上面去的话,X就应该放到Y的那个位置

继续看,现在Y > X那么Y一定是X的右儿子

但是X已经有了右儿子B,

根据平衡树我们可以知道X < B < Y

所以我们可以把X的右儿子B丢给Y当做左儿子

而X的左儿子A有A < X < Y < Z显然还是X的左儿子

综上,我们一顿乱搞,原来的平衡树被我们搞成了这个样子

在检查一下

原来的大小关系是

A < X < B < Y < C < Z

把X旋转一下之后大小关系

A < X < B < Y < C < Z

诶,大小关系也没有变

所以之前那棵平衡树就可以通过旋转变成这个样子

并且这个时候还是一棵平衡树

好神奇诶。。。

但是,XYZ的关系显然不仅仅只有这一种

有Y是Z的左儿子 X是Y的左儿子

有Y是Z的左儿子 X是Y的右儿子

有Y是Z的右儿子 X是Y的左儿子

有Y是Z的右儿子 X是Y的右儿子

一共4种情况,大家可以自己画画图,转一转。


如果把上面的图画完了,我们就可以正式的来玩一玩splay了

转完了上面四种情况,我们来找找规律

最明显的一点,我们把X转到了原来Y的位置

也就是说,原来Y是Z的哪个儿子,旋转之后X就是Z的哪个儿子

继续看一看

我们发现,X是Y的哪个儿子,那么旋转完之后,X的那个儿子就不会变

什么意思?

看一看我上面画的图

X是Y的左儿子,A是X的左儿子,旋转完之后,A还是X的左儿子

这个应该不难证明

如果X是Y的左儿子,A是X的左儿子

那么A < X < Y旋转完之后A还是X的左儿子

如果X是Y的右儿子,A是X的右儿子

那么A > X > Y 只是把不等式反过来了而已

再看一下,找找规律

如果原来X是Y的哪一个儿子,那么旋转完之后Y就是X的另外一个儿子

再看看图

如果原来X是Y的左儿子,旋转之后Y是X的右儿子

如果原来X是Y的右儿子,旋转之后Y是X的左儿子

这个应该也很好证明吧。。。

如果X是右儿子 X > Y,所以旋转后Y是X的左儿子

如果X是左儿子 Y > X,所以旋转后Y是X的右儿子

所以总结一下:

1.X变到原来Y的位置

2.Y变成了 X原来在Y的 相对的那个儿子

3.Y的非X的儿子不变 X的 X原来在Y的 那个儿子不变

4.X的 X原来在Y的 相对的 那个儿子 变成了 Y原来是X的那个儿子

啊,,,写出来真麻烦,用语言来写一下

其中t是树上节点的结构体,ch数组表示左右儿子,ch[0]是左儿子,ch[1]是右儿子,ff是父节点

void rotate(int x)//X是要旋转的节点
{
int y=t[x].ff;//X的父亲
int z=t[y].ff;//X的祖父
int k=t[y].ch[1]==x;//X是Y的哪一个儿子 0是左儿子 1是右儿子
t[z].ch[t[z].ch[1]==y]=x;//Z的原来的Y的位置变为X
t[x].ff=z;//X的父亲变成Z
t[y].ch[k]=t[x].ch[k^1];//X的与X原来在Y的相对的那个儿子变成Y的儿子
t[t[x].ch[k^1]].ff=y;//更新父节点
t[x].ch[k^1]=y;//X的 与X原来相对位置的儿子变成 Y
t[y].ff=x;//更新父节点
}

上面的代码用了很多小小小技巧

比如t[y].ch[1]==x

t[y].ch[1]是y的右儿子,如果x是右儿子,那么这个式子是1,否则是0,也正好对应着左右儿子

同样的k1,表示相对的儿子,左儿子01=1 右儿子1^1=0

好了,这就是一个基本的旋转操作(别人讲的


继续看接下来的东西

现在考虑一个问题

如果要把一个节点旋转到根节点(比如上面的Z节点呢)

我们是不是可以做两步,先把X转到Y再把X转到Z呢?

我们来看一看

一个这样的Splay

把X旋转到Y之后

再接着把X旋转到Z之后

好了,这就是对X连着旋转两次之后的Splay,看起来似乎没有什么问题。

但是,我们现在再来看一看



原图中的Splay有一条神奇链: Z->Y->X->B

然后再来看一看旋转完之后的Splay



也有一条链X->Z->Y->B

也就是说

如果你只对X进行旋转的话,

有一条链依旧存在,

如果是这样的话,splay很可能会被卡。

好了,

显然对于XYZ的不同情况,可以自己画图考虑一下,

如果要把X旋转到Z的位置应该如何旋转

归类一下,其实还是只有两种:

第一种,X和Y分别是Y和Z的同一个儿子

第二种,X和Y分别是Y和Z不同的儿子

对于情况一,也就是类似上面给出的图的情况,就要考虑先旋转Y再旋转X

对于情况二,自己画一下图,发现就是对X旋转两次,先旋转到Y再旋转到X

这样一想,对于splay旋转6种情况中的四种就很简单的分了类

其实另外两种情况很容易考虑,就是不存在Z节点,也就是Y节点就是Splay的根了

此时无论怎么样都是对于X向上进行一次旋转

那么splay的旋转也可以很容易的简化的写出来

void splay(int x,int goal)//将x旋转为goal的儿子,如果goal是0则旋转到根
{
while(t[x].ff!=goal)//一直旋转到x成为goal的儿子
{
int y=t[x].ff,z=t[y].ff;//父节点祖父节点
if(z!=goal)//如果Y不是根节点,则分为上面两类来旋转
(t[z].ch[0]==y)^(t[y].ch[0]==x)?rotate(x):rotate(y);
//这就是之前对于x和y是哪个儿子的讨论
rotate(x);//无论怎么样最后的一个操作都是旋转x
}
if(goal==0)root=x;//如果goal是0,则将根节点更新为x
}

这样写多简单,比另外一些人写得分6种情况讨论要简单很多。


应SYC大佬要求,继续补充内容。


先是查找find操作

从根节点开始,左侧都比他小,右侧都比他大,

所以只需要相应的往左/右递归

如果当前位置的val已经是要查找的数

那么直接把他Splay到根节点,方便接下来的操作

类似于二分查找,

所以时间复杂度O(logn)

inline void find(int x)//查找x的位置,并将其旋转到根节点
{
int u=root;
if(!u)return;//树空
while(t[u].ch[x>t[u].val]&&x!=t[u].val)//当存在儿子并且当前位置的值不等于x
u=t[u].ch[x>t[u].val];//跳转到儿子,查找x的父节点
splay(u,0);//把当前位置旋转到根节点
}

下一个Insert操作

往Splay中插入一个数

类似于Find操作,只是如果是已经存在的数,就可以直接在查找到的节点的进行计数

如果不存在,在递归的查找过程中,会找到他的父节点的位置,

然后就会发现底下没有啦。。。

所以这个时候新建一个节点就可以了

inline void insert(int x)//插入x
{
int u=root,ff=0;//当前位置u,u的父节点ff
while(u&&t[u].val!=x)//当u存在并且没有移动到当前的值
{
ff=u;//向下u的儿子,父节点变为u
u=t[u].ch[x>t[u].val];//大于当前位置则向右找,否则向左找
}
if(u)//存在这个值的位置
t[u].cnt++;//增加一个数
else//不存在这个数字,要新建一个节点来存放
{
u=++tot;//新节点的位置
if(ff)//如果父节点非根
t[ff].ch[x>t[ff].val]=u;
t[u].ch[0]=t[u].ch[1]=0;//不存在儿子
t[tot].ff=ff;//父节点
t[tot].val=x;//值
t[tot].cnt=1;//数量
t[tot].size=1;//大小
}
splay(u,0);//把当前位置移到根,保证结构的平衡。注意前面因为更改了子树大小,所以这里必须Splay上去进行pushup保证size的正确。
}

继续,,,

前驱/后继操作Next

首先就要执行Find操作

把要查找的数弄到根节点

然后,以前驱为例

先确定前驱比他小,所以在左子树上

然后他的前驱是左子树中最大的值

所以一直跳右结点,直到没有为止

找后继反过来就行了

inline int Next(int x,int f)//查找x的前驱(0)或者后继(1)
{
find(x);
int u=root;//根节点,此时x的父节点(存在的话)就是根节点
if(t[u].val>x&&f)return u;//如果当前节点的值大于x并且要查找的是后继
if(t[u].val<x&&!f)return u;//如果当前节点的值小于x并且要查找的是前驱
u=t[u].ch[f];//查找后继的话在右儿子上找,前驱在左儿子上找
while(t[u].ch[f^1])u=t[u].ch[f^1];//要反着跳转,否则会越来越大(越来越小)
return u;//返回位置
}

还有操作呀/。。。

删除操作

现在就很简单啦

首先找到这个数的前驱,把他Splay到根节点

然后找到这个数后继,把他旋转到前驱的底下

比前驱大的数是后继,在右子树

比后继小的且比前驱大的有且仅有当前数

在后继的左子树上面,

因此直接把当前根节点的右儿子的左儿子删掉就可以啦

inline void Delete(int x)//删除x
{
int last=Next(x,0);//查找x的前驱
int next=Next(x,1);//查找x的后继
splay(last,0);splay(next,last);
//将前驱旋转到根节点,后继旋转到根节点下面
//很明显,此时后继是前驱的右儿子,x是后继的左儿子,并且x是叶子节点
int del=t[next].ch[0];//后继的左儿子
if(t[del].cnt>1)//如果超过一个
{
t[del].cnt--;//直接减少一个
splay(del,0);//旋转
}
else
t[next].ch[0]=0;//这个节点直接丢掉(不存在了)
}

忽然发现我连第K大都没有写,随口口胡一下

从当前根节点开始,检查左子树大小

因为所有比当前位置小的数都在左侧

如果左侧的数的个数多余K,则证明第K大在左子树中

否则,向右子树找,找K-左子树大小-当前位置的数的个数

记住特判K恰好在当前位置

inline int kth(int x)//查找排名为x的数
{
int u=root;//当前根节点
if(t[u].size<x)//如果当前树上没有这么多数
return 0;//不存在
while(1)
{
int y=t[u].ch[0];//左儿子
if(x>t[y].size+t[u].cnt)
//如果排名比左儿子的大小和当前节点的数量要大
{
x-=t[y].size+t[u].cnt;//数量减少
u=t[u].ch[1];//那么当前排名的数一定在右儿子上找
}
else//否则的话在当前节点或者左儿子上查找
if(t[y].size>=x)//左儿子的节点数足够
u=y;//在左儿子上继续找
else//否则就是在当前根节点上
return t[u].val;
}
}

还剩下一些splay的基本操作

先留个坑,以后再慢慢补。。。

Splay入门解析【保证让你看不懂(滑稽)】的更多相关文章

  1. 小师妹学JVM之:深入理解JIT和编译优化-你看不懂系列

    目录 简介 JIT编译器 Tiered Compilation分层编译 OSR(On-Stack Replacement) Deoptimization 常见的编译优化举例 Inlining内联 Br ...

  2. 如何写出同事看不懂的Java代码?

    原创:微信公众号 码农参上,欢迎分享,转载请保留出处. 哈喽大家好啊,我是没更新就是在家忙着带娃的Hydra. 前几天,正巧赶上组里代码review,一下午下来,感觉整个人都血压拉满了.五花八门的代码 ...

  3. 学java入门到精通,不得不看的15本书

    学java入门到精通,不得不看的15本书 一.Java编程入门类1.<Java编程思想>2.<Agile Java>中文版 二.Java编程进阶类1.<重构 改善既有代码 ...

  4. 还看不懂同事的代码?超强的 Stream 流操作姿势还不学习一下

    Java 8 新特性系列文章索引. Jdk14都要出了,还不能使用 Optional优雅的处理空指针? Jdk14 都要出了,Jdk8 的时间处理姿势还不了解一下? 还看不懂同事的代码?Lambda ...

  5. 小白学习React官方文档看不懂怎么办?

    最近在上React课程的时候,发现好多同学不会看文档,所以在这里写一篇文章,希望能给同学们一点点启发. 我们首先打开React官方网站——https://react.docschina.org/doc ...

  6. 不要写很酷但同事看不懂的Java代码

    你好呀,我是沉默王二,一个和黄家驹一样身高,和刘德华一样颜值的程序员.为了提高 Java 编程的技艺,我最近在 GitHub 上学习一些高手编写的代码.下面这一行代码(出自大牛之手)据说可以征服你的朋 ...

  7. 对于挑战书上的很久之前都看不懂的DP看懂的突破

    突破一..牢记问题概念 并且牢记dp状态方程 突破二..一直有一个求和dp转化成O1dp递推的式子看不懂.. 看不懂的原因是..没有分清求和符号作用的范围 提醒:以后遇到求和符号一定明确其求和的式子的 ...

  8. QQ地图api里的 地址解析函数 看不懂 javascript_百度知道

    QQ地图api里的 地址解析函数 看不懂 javascript_百度知道     QQ地图api里的 地址解析函数 看不懂 javascript    2011-09-18 12:18     匿名 ...

  9. thinkphp学习笔记10—看不懂的路由规则

    原文:thinkphp学习笔记10-看不懂的路由规则 路由这部分貌似在实际工作中没有怎么设计过,只是在用默认的设置,在手册里面看到部分,艰涩难懂. 1.路由定义 要使用路由功能需要支持PATH_INF ...

随机推荐

  1. Python高级用法总结

    Python很棒,它有很多高级用法值得细细思索,学习使用.本文将根据日常使用,总结介绍Python的一组高级特性,包括:列表推导式.迭代器和生成器.装饰器. 列表推导(list comprehensi ...

  2. CSS3动画中的animation-timing-function效果演示

    CSS3动画(animation)属性有如下几个: 属性 值 说明 animation-name name 指定元素要使用的keyframes名称 animation-duration time(ms ...

  3. nginx上支持.htaccess伪静态的配置实例

    本文介绍下,在nginx上配置.htaccess伪静态的方法,有需要的朋友参考下吧. 在apache上.htaccess转向,只要apache编译的时候指明支持rewrite模块即可. 但是换到ngi ...

  4. MySQL统计函数记录——按月、按季度、按日、时间段统计

    按年汇总,统计:select sum(mymoney) as totalmoney, count(*) as sheets from mytable group by date_format(col, ...

  5. CUP、内存、磁盘是如何在一起工作的

    IT技术发展到今天,计算机能做的事情可谓复杂的多.那么计算机是如何做出如此复杂的运算的呢? 不准确的说,计算机主要做两件事,数据计算和数据存储. 第一先说说计算机是如何计算的吧. 我们平时见到的所有计 ...

  6. Java GC分析记录

    Java GC记录 近来.项目没有特别忙碌的时候,抽空看了下生产环境的项目运行状况,我们的项目一直运行速度不是很快,偶尔会出现卡顿的现象,这点给人的体验感觉也就不那么好了.先抛个测试环境截图(生产环境 ...

  7. linux中权限对文件和目录的作用

    chmod 755 a.txt 文件: r:读取文件内容(cat more head tail) w:编辑,新增,修改文件的内容(vi,echo) 不包括删除文件:原因是只能对文件内容进行修改,而在l ...

  8. web2 - JavaScript

    JavaScript 知识要点 参考教材一 参考教材二 参考教材三 1.JavaScript 和 Java 的关系? 2.JavaScript 在编程中可以做什么? 3.如何在 html 中使用 Ja ...

  9. 【推荐】开源项目minapp-重新定义微信小程序的开发

    minapp 重新定义微信小程序的开发 官网:https://qiu8310.github.io/minapp/ 作者:Mora minapp 重新定义微信小程序的开发 使用 用 npm 安装命令行工 ...

  10. java的mac自动化-自动运行java程序

    本文旨在帮助读者介绍,如果一个测试工程师拿到了mac本,该如何在本地自动运行java代码 首先如图所示写下如下一段代码 package zlr;import org.junit.Test;public ...