Splay树详解

更好的阅读体验

Splay树

这是一篇宏伟的巨篇
首先介绍BST,也就是所有平衡树的开始,他的China名字是二叉查找树.

BST性质简介

给定一棵二叉树,每一个节点有一个权值,命名为 关键码 ,至于为什么叫这个名字,我也不知道.
BST性质也就是,对于树中任何一个节点,都满足一下性质.
\1. 这个节点的关键码不小于它的左子树上,任意一个节点的关键码
\2. 这个节点的关键码不大于它的右子树上,任意一个节点的关键码


然后我们就可以发现这棵树的中序遍历,就是一个关键码单调递增的节点序列,说的直白点,就是一个排好序的数组.
这就是伟大的BST性质,一个引起无数OIer憎恨的性质.(手动滑稽)

Splay的背景

什么是Splay,它就是一种可以旋转的平衡树.它可以解决BST树这棵树一个极端情况,也就是退化情况.
如下图所示.

我们发现上面这个图是一条链,这种恐怖的数据让时间从O(log(n))O(log(n))退化到O(n)O(n).
于是各种各样的科学家们,就开始了思考人生,开始了丧心病狂地创造出了各种平衡树方法,然后一个异常有名的科学家Tarjan开始了它丧心病狂的Tarjan算法系列.

Splay思路

这是一棵特殊的BST树,或者说平衡树基本都是改变树结构样式,但是却不改变最后得出的排列序列.
来一个yyb大佬的美图

这张图片大致意思如下所示:正方形部分表示一棵子树,然后圆形表示节点.(当然了其实你也可以都看作成为节点,可能更好理解吧,看个人看法)
对于这样一棵树,我们可以做一些特殊的操作,来让它变换树的形态结构,但是最后的答案却是正确的.平衡树的精髓就是这个,就是改变树的形态结构,但是不改变最后的中序遍历,也就是答案数组.


重点来了,以下为splay精华所在之处,一定要全神贯注地看,并且手中拿着笔和草稿纸,一步步跟着我一起做,那么我保证你看一遍就可以懂.相信自己一定可以明白的,不要因为文字太多而放弃.

现在我们的现在目标只有一个x节点往上面爬,爬到它原本的父亲节点y,然后让y节点下降
首先思考BST性质,那就是右子树上的点,统统都比父亲节点大对不对,现在我们的x节点是父亲节点y的左节点,也就是比y节点小. 那么我们为了不改变答案顺序,我们可以让y节点成为x节点的右儿子,也就是y节点仍然大于我们的x节点
这么做当然是没有问题的,那么我们现在又有一个问题了,x节点的右子树原来是有子树B的,那么如果说现在y节点以及它的右子树(没有左子树,因为曾经x节点是它的左子树),放到了x节点的右子树上,那么岂不是多了什么吗?
我们知道 x节点的右子树必然是大于x节点的,然后y节点必然是大于x节点的右子树和x节点的,因为x节点和它的右子树都是在y节点的左子树,都比它小
既然如此的话,我们为什么不把x节点原来的右子树,放在节点y的左子树上面呢?这样的话,我们就巧妙地避开了冲突,达成了x节点上移,y节点下移.
移动后的图片

这就是一种情况,但是我们不能够局限一种情况,我们要找到通解.以下为通解
若节点x为y节点的位置z.( z为0,则为左节点,1则为右节点 )
\1. 那么y节点就放到x节点的z^1的位置.(也就是,x节点为y节点的右子树,那么y节点就放到左子树,x节点为y节点左子树,那么y节点就放到右子树位置,这样就完美满足条件)
\2. 如果说x节点的z^1的位置上,已经有节点,或者一棵子树,那么我们就将原来x节点z^1位置上的子树,放到y节点的位置z上面.(自己可以画图理解一下)
\3. 移动完毕.

yyb大佬的代码,个人认为最精简的代码&最适合理解的代码.主要是因为注释多,不要打我脸
t是树上节点的结构体,ch数组表示左右儿子,ch[0]是左儿子,ch[1]是右儿子,ff是父节点

void update(int x)
{
    t[x].size=t[t[x].ch[0]].size+t[t[x].ch[1]].size+t[x].cnt;//左子树+右子树+本身多少个,cnt记录重复个数.
}
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;//更新父节点
    update(y);update(x);//yyb大佬忘记写了,这里是上传修改.
}

这里面用到了t[y].ch[1]==x
t[y].ch[1]是y的右儿子,如果x是右儿子,那么这个式子是1,否则是0,也正好对应着左右儿子,同样的k^1,表示相对的儿子,左儿子0^1=1 右儿子1^1=0.其实上面我的文字们已经讲述清楚了,不过yyb大佬的代码,写得很好看这是真心话!!!


如果你已经读到了这里,那么恭喜你,现在的你成功完成了splay大部分了,但是你发现这条链表结构还是会卡死你,是不是感觉被耍了,不要气恼&气馁,因为你只需要再来一个splay函数就好了.你已经完成了85%,接下来的很容易的.

如果说x,y,z这三个节点共线,也就是x和它的父亲节点y和它的祖先节点在同一条线段上的话,那么我们就需要再来一些特殊处理了,其实就是一些很容易的操作.
下面就是三点共线的一张图片

这张图片里面的最长链是Z->Y->X->A
如果说我们一直都是x旋转的话,那么就会得到下面这张图片.

而一直旋转x的最长链是X->Z->y->B
我们发现旋转和不旋转似乎并没有任何区别,实际上也没有区别,也就是这些旋转操作后的树和不旋转的树,其实是没有多大区别的.
算法失败了,不过不用害怕,其实我们还有办法.
\1. 如果当前处于共线状态的话,那么先旋转y,再旋转x.这样可以强行让他们不处于共线状态,然后平衡这棵树.
\2. 如果当前不是共线状态的话,那么只要旋转x即可.

当你看懂这个以后恭喜你,你已经成功学会了splay的双旋操作了,然后你就可以看yyb大佬的代码了.

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
}
查找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);//把当前位置移到根,保证结构的平衡
}
前驱/后继操作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;
    }
}

以下为我自己编写的代码,其实就是yyb大佬的代码,只不过修改了一丢丢而已了.

#include <bits/stdc++.h>
using namespace std;
const int N=201000;
struct splay_tree
{
    int ff,cnt,ch[2],val,size;
} t[N];
int root,tot;
void update(int x)
{
    t[x].size=t[t[x].ch[0]].size+t[t[x].ch[1]].size+t[x].cnt;
}
void rotate(int x)
{
    int y=t[x].ff;
    int z=t[y].ff;
    int k=(t[y].ch[1]==x);
    t[z].ch[(t[z].ch[1]==y)]=x;
    t[x].ff=z;
    t[y].ch[k]=t[x].ch[k^1];
    t[t[x].ch[k^1]].ff=y;
    t[x].ch[k^1]=y;
    t[y].ff=x;
    update(y);update(x);
}
void splay(int x,int s)
{
    while(t[x].ff!=s)
    {
        int y=t[x].ff,z=t[y].ff;
        if (z!=s)
            (t[z].ch[0]==y)^(t[y].ch[0]==x)?rotate(x):rotate(y);
        rotate(x);
    }
    if (s==0)
        root=x;
}
void find(int x)
{
    int u=root;
    if (!u)
        return ;
    while(t[u].ch[x>t[u].val] && x!=t[u].val)
        u=t[u].ch[x>t[u].val];
    splay(u,0);
}
void insert(int x)
{
    int u=root,ff=0;
    while(u && t[u].val!=x)
    {
        ff=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);
}
int Next(int x,int f)
{
    find(x);
    int u=root;
    if (t[u].val>x && f)
        return u;
    if (t[u].val<x && !f)
        return u;
    u=t[u].ch[f];
    while(t[u].ch[f^1])
        u=t[u].ch[f^1];
    return u;
}
void Delete(int x)
{
    int last=Next(x,0);
    int Net=Next(x,1);
    splay(last,0);
    splay(Net,last);
    int del=t[Net].ch[0];
    if (t[del].cnt>1)
    {
        t[del].cnt--;
        splay(del,0);
    }
    else
        t[Net].ch[0]=0;
}
int kth(int x)
{
    int u=root;
    while(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;
    }
}
int main()
{
    int n;
    scanf("%d",&n);
    insert(1e9);
    insert(-1e9);
    while(n--)
    {
        int opt,x;
        scanf("%d%d",&opt,&x);
        if (opt==1)
            insert(x);
        if (opt==2)
            Delete(x);
        if (opt==3)
        {
            find(x);
            printf("%d\n",t[t[root].ch[0]].size);
        }
        if (opt==4)
            printf("%d\n",kth(x+1));
        if (opt==5)
            printf("%d\n",t[Next(x,0)].val);
        if (opt==6)
            printf("%d\n",t[Next(x,1)].val);
    }
    return 0;
}
/*
插入数值x。
删除数值x(若有多个相同的数,应只删除一个)。
查询数值x的排名(若有多个相同的数,应输出最小的排名)。
查询排名为x的数值。
求数值x的前驱(前驱定义为小于x的最大的数)。
求数值x的后继(后继定义为大于x的最小的数)。
*/

作者:秦淮岸灯火阑珊
链接:https://www.acwing.com/activity/content/code/content/24072/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

感谢yyb大佬
注:我学会splay就是在yyb大佬的blog下成功的,然后成功地将它详细写了一遍,这道题目除了代码和一些图片,其他都是我写的,所以来说也算得上原汁原味的原著吧.

原文地址:https://www.cnblogs.com/gzh-red/p/11011557.html

时间: 2024-11-05 19:40:58

Splay树详解的相关文章

《ACM/ICPC 算法训练教程》读书笔记 之 数据结构(线段树详解)

依然延续第一篇读书笔记,这一篇是基于<ACM/ICPC 算法训练教程>上关于线段树的讲解的总结和修改(这本书在线段树这里Error非常多),但是总体来说这本书关于具体算法的讲解和案例都是不错的. 线段树简介 这是一种二叉搜索树,类似于区间树,是一种描述线段的树形数据结构,也是ACMer必学的一种数据结构,主要用于查询对一段数据的处理和存储查询,对时间度的优化也是较为明显的,优化后的时间复杂为O(logN).此外,线段树还可以拓展为点树,ZWK线段树等等,与此类似的还有树状数组等等. 例如:要将

查找(二)简单清晰的B树、Trie树详解

查找(二) 散列表 散列表是普通数组概念的推广.由于对普通数组可以直接寻址,使得能在O(1)时间内访问数组中的任意位置.在散列表中,不是直接把关键字作为数组的下标,而是根据关键字计算出相应的下标. 使用散列的查找算法分为两步.第一步是用散列函数将被查找的键转化为数组的一个索引. 我们需要面对两个或多个键都会散列到相同的索引值的情况.因此,第二步就是一个处理碰撞冲突的过程,由两种经典解决碰撞的方法:拉链法和线性探测法. 散列表是算法在时间和空间上作出权衡的经典例子. 如果没有内存限制,我们可以直接

线段树详解 (原理,实现与应用)

线段树详解 By 岩之痕 目录: 一:综述 二:原理 三:递归实现 四:非递归原理 五:非递归实现 六:线段树解题模型 七:扫描线 八:可持久化 (主席树) 九:练习题 一:综述 假设有编号从1到n的n个点,每个点都存了一些信息,用[L,R]表示下标从L到R的这些点. 线段树的用处就是,对编号连续的一些点进行修改或者统计操作,修改和统计的复杂度都是O(log2(n)). 线段树的原理,就是,将[1,n]分解成若干特定的子区间(数量不超过4*n),然后,将每个区间[L,R]都分解为 少量特定的子区

trie树--详解

前几天学习了并查集和trie树,这里总结一下trie.     本文讨论一棵最简单的trie树,基于英文26个字母组成的字符串,讨论插入字符串.判断前缀是否存在.查找字符串等基本操作:至于trie树的删除单个节点实在是少见,故在此不做详解. l        Trie原理 Trie的核心思想是空间换时间.利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的. l        Trie性质 好多人说trie的根节点不包含任何字符信息,我所习惯的trie根节点却是包含信息的,而且认为这样也

伸展树详解及实现

我们讨论过,树的搜索效率与树的深度有关.二叉搜索树的深度可能为n,这种情况下,每次搜索的复杂度为n的量级.AVL树通过动态平衡树的深度,单次搜索的复杂度为log(n).我们下面看伸展树(splay tree),它对于m次连续搜索操作有很好的效率.伸展树会在一次搜索后,对树进行一些特殊的操作.这些操作的理念与AVL树有些类似,即通过旋转,来改变树节点的分布,并减小树的深度.但伸展树并没有AVL的平衡要求,任意节点的左右子树可以相差任意深度.与二叉搜索树类似,伸展树的单次搜索也可能需要n次操作.但伸

字典树详解

字典树概述    字典树,又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种.典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计.它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高. 例题: NKOJ 1934 外地人     你考入大城市沙坪坝的学校, 但是沙坪坝的当地人说着一种很难懂的方言, 你完全听不懂. 幸好你手中有本字典可以帮你. 现在你有若干个听不懂的方言需要查询字典.输入

Trie树详解(转)

特别声明 本文只是一篇笔记类的文章,所以不存在什么抄袭之类的. 以下为我研究时参考过的链接(有很多,这里我只列出我记得的): Trie(字典树)的应用--查找联系人 trie树 Trie树:应用于统计和排序 牛人源码,研究代码来源 1.字典树的概念 字典树,因为它的搜索快捷的特性被单词搜索系统使用,故又称单词查找树.它是一种树形结构的数据结构.之所以快速,是因为它用空间代替了速度. 2.字典树的特点: 字典树有三个基本性质: 1.根节点不包含字符,除根节点外每一个节点都只包含一个字符2.从根节点

B树、Trie树详解

查找(二) 散列表 散列表是普通数组概念的推广.由于对普通数组可以直接寻址,使得能在O(1)时间内访问数组中的任意位置.在散列表中,不是直接把关键字作为数组的下标,而是根据关键字计算出相应的下标. 使用散列的查找算法分为两步.第一步是用散列函数将被查找的键转化为数组的一个索引. 我们需要面对两个或多个键都会散列到相同的索引值的情况.因此,第二步就是一个处理碰撞冲突的过程,由两种经典解决碰撞的方法:拉链法和线性探测法. 散列表是算法在时间和空间上作出权衡的经典例子. 如果没有内存限制,我们可以直接

组播学习笔记(五)源树+共享树详解

一.组播路由表主要内容: 1.源 2.目的 3.入接口 4.RPF接口 5.RPF邻居 二.pim协议 pim(协议无关协议),此处协议无关是指单播协议无关,PIM可以基于任意单播协议工作.注意,组播是基于单播进行工作的,虽有组播表但是最终是查找单播路由表寻找出口. IP协议号为103 不必发送组播更新,通过hello和hold,join报文来维持邻居关系,因此开销小. hello时间30s,发向224.0.0.13(所有开启pim的功能的交换机都会监听此地址) hold时间:3.5*30s=1