Splay tree Detailed

Better reading experience

Splay tree

This is a magnificent giant piece
First introduced BST, which is the beginning of all balanced tree, and his name China is a binary search tree.

About the nature of BST

Given a binary tree, each node has a right value named key , as to why that name, I do not know.
BST nature is, for any node in the tree, all to satisfy my nature.
\ 1. this node key is not less than its left subtree, node key of any
key \ 2. this node is not greater than its right sub-tree, a node is any key

QQ screenshot 20190222202301.png
Then we can find this tree preorder, is a key monotonically increasing sequence of nodes, said bluntly, is an array of sorted rows.
This is a great BST nature, a hatred of causing countless OIer nature.(Manual funny)

Splay background

What is Splay, it is a kind of balance can rotate the tree. It can solve BST tree tree an extreme situation, that is degradation.
As shown below.
QQ screenshot 20190222202419.png
We found that above this figure is a chain, this terrible data let time from O (log (n)) O (log (n)) degenerate to O (n) O (n) .
Thus a wide variety of scientists, began to think about life, he began a frenzied create a a variety of balanced tree method, then an exception famous scientist began its frenzied Tarjan algorithm Tarjan series.

Splay ideas

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


重点来了,以下为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节点下移.
移动后的图片
sply2
这就是一种情况,但是我们不能够局限一种情况,我们要找到通解.以下为通解
若节点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和它的祖先节点在同一条线段上的话,那么我们就需要再来一些特殊处理了,其实就是一些很容易的操作.
下面就是三点共线的一张图片
QQ screenshot 20190222211102.png
这张图片里面的最长链是Z->Y->X->A
如果说我们一直都是x旋转的话,那么就会得到下面这张图片.
QQ screenshot 20190222210851.png
而一直旋转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大

Starting from the current root node, check the left subtree size
as all smaller than the current position number are on the left
if the number of numbers on the left side of excess K, then the proof of the big K in the left subtree
Otherwise, the right subtree looking, looking for K- left subtree size - the number of the current position of the number
to remember special judge K exactly at the current location

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;
    }
}

The following is the code I have written,In fact yyb Gangster code, just modify a Diudiu it was.

#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
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Thank big brother yyb
Note: I learned to splay are successful at blog yyb chiefs, and then successfully write it again in detail, question head in addition to code and some pictures, all my other writing, it is also regarded as authentic original bar.

Guess you like

Origin www.cnblogs.com/gzh-red/p/11011557.html