码迷,mamicode.com
首页 > 其他好文 > 详细

Splay入门

时间:2019-01-13 12:23:02      阅读:222      评论:0      收藏:0      [点我收藏+]

标签:next   存在   直接   更新   ast   理解   插入   删掉   splay   

Splay入门

  • write by:BigYellowDog
  • 大部分资料整理于小蒟蒻yyb的博客

引入:

  • 首先要学习Splay之前,你需要知道Splay的前世今生!
  • 世上有这么个东西,叫平衡树。它的定义是对于任意一个节点,左儿子的值比它小,右儿子的值比它大
    并且任意一棵子树单独拎出来也是一棵平衡树。如下图:

技术分享图片

  • Look at this!这就是一棵平衡树。

  • 它现在很平衡,是一棵满二叉树,高度正好是logn
  • 如果这棵树极端一点,它就会变成这样 ↓

技术分享图片

  • 现在看起来,这个东西一点都不平衡
  • 但其实它只是退化成了一条链
  • 所以这时如果要查询的话,最坏情况下就变成了O(n)
  • 这就很难受了!
  • 所以为了解决这个难题。各路大神们就弄出了各种树来解决问题。而我们的Tarjan大佬就弄出了Splay这玩意
  • Splay就这样产生了!

原理的探究:

  • 前文中提到了Splay的产生是因解决平衡树尴尬的查询问题。那么它的实现原理又是什么呢?
  • 顾名思义,Splay译为 “伸展、旋转”

  • 所以它的原理就是:每次查询都会调整树的结构,使被查询频率高的条目更靠近树根。

  • 那么,如何旋转呢?我们先做一个简单的旋转模拟:

技术分享图片

  • 任务:将X旋转到Y的位置,并保持这个东西还是一棵平衡树

  • 思路:

    • 通过观察,我们发现这样的关系:

    Y < Z

    X < Y < C

    A < X < B

    嗯... ...通过上述关系,我们可以通过人脑画出旋转后的图,问题解决(图如下)

技术分享图片

  • 我们检查一下,原来的大小关系为:

    A < X < B < Y < C < Z

  • 旋转之后大小关系为:

    A < X < B < Y < C < Z

  • 诶,大小关系也没有变。所以之前那棵平衡树就可以通过旋转变成这个样子,并且这个时候还是一棵平衡树

  • 注意!前文说了,这只是一个简单的旋转模拟。在实际中,X和Y的位置不只这些,有以下几种情况:

    1. 有Y是Z的左儿子 X是Y的左儿子
    2. 有Y是Z的左儿子 X是Y的右儿子
    3. 有Y是Z的右儿子 X是Y的左儿子
    4. 有Y是Z的右儿子 X是Y的右儿子
  • 那么现在,我们就来正式探究Splay的一般情况:

  • 从古至今,解决一般性问题都是从特殊性中解决。所以还是上面那个图,我们可以发现这些规律:

  1. Y是Z的哪个儿子,旋转之后X就是Z的哪个儿子
  2. X是Y的哪个儿子,那么旋转完之后,X的那个儿子就不会变
    • 证明:
    • 如果X是Y的左儿子,A是X的左儿子
      那么A < X < Y旋转完之后A还是X的左儿子
      如果X是Y的右儿子,A是X的右儿子
      那么A > X > Y 只是把不等式反过来了而已
  3. 如果原来X是Y的哪一个儿子,那么旋转完之后Y就是X的另外一个儿子
    • 证明:
    • 如果X是右儿子 X > Y,所以旋转后Y是X的左儿子
      如果X是左儿子 Y > X,所以旋转后Y是X的右儿子
  • 把每个规律看懂,自己再理理思路,还会发现一个规律:
Z 位置始终不变
X、Y 相互交换位置
B 原位置改变了
A、C 与自己爸爸的相对位置不变
  • 于是“旋转”的代码就顺理成章的得出:
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; //更新父节点
}
  • 好了如果你坚持看到这里。恭喜你,Splay的原理你看懂了!

细谈“旋转”:

  • 通过前文我们了解到Splay为了解决O(n)查询的问题。使用到了“旋转”这一操作。但实际中旋转不仅仅那么简单,它还有一些约束条件。我们继续来学习

技术分享图片

  • 现在考虑一个问题,如果要把一个节点旋转到根节点(比如上面的Z节点)。我们是不是可以做两步,先把X转到Y再把X转到Z呢?
  • 好,我们试试看
  • 如下,将X旋转到Y位置的时候:

技术分享图片

  • 再接着把X旋转到Z之后:

技术分享图片

  • 好了,旋转完了。看起来似乎没啥毛病。

  • 真的没问题吗?原图中有一条瓜皮的链: Z->Y->X->B

技术分享图片

  • 当我们旋转完后,发现这条瓜皮的链还在... ...

技术分享图片

  • 也就是说,如果你只对X进行旋转的话,有一条链依旧存在,如果是这样的话,splay很可能会被卡。
  • (PS:这里笔者自己都有点懵,但笔者的理解是这样旋转并没有满足“查询频率高的条目更靠近树根”的宗旨)
  • 所以,我们可以发现:对于XYZ的不同情况,有不同的旋转方式!

  • 那么这里我直接把这几种情况总结了起来:

    1. 第一种,X和Y分别是Y和Z的同一个儿子
      • 对于情况一,也就是类似上面给出的图的情况,就要考虑先旋转Y再旋转X
    2. 第二种,X和Y分别是Y和Z不同的儿子
      • 对于情况二,就是对X旋转两次,先旋转到Y再旋转到Z
    3. 不存在Z节点,也就是Y节点就是Splay的根
      • 此时无论怎么样都是对于X向上进行一次旋转
    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
    }

基本操作:

  1. find操作
  2. insert操作
  3. eraser操作(学这个前先学第4点)
  4. 前驱/后继操作
  5. 合并操作
  6. 分离操作
  7. 第K大操作
  • 是不是看到这些想砸键盘。没错,默默接受把Orz。我们一个一个来看

1. find操作

  • 步骤:从根节点开始,左侧都比他小,右侧都比他大。所以只需要相应的往左/右递归。如果当前位置的val已经是要查找的数,那么直接把他Splay到根节点,方便接下来的操作。
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);//把当前位置旋转到根节点
}

2. insert操作

  • 步骤:类似于Find操作,只是如果是已经存在的数,就可以直接在查找到的节点的进行计数。如果不存在,在递归的查找过程中,会找到他的父节点的位置,然后就会发现底下没有,所以这个时候新建一个节点就可以了
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);//把当前位置移到根,保证结构的平衡
}

3. eraser操作

  • 步骤:首先找到这个数的前驱,把他Splay到根节点,然后找到这个数后继,把他旋转到前驱的底下。比前驱大的数是后继,在右子树比后继小的且比前驱大的有且仅有后继的左儿子。因此直接把当前根节点的后继的左儿子删掉就可以了
void eraser(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;//这个节点直接丢掉(不存在了) 
}

4. 前继/后继操作

  • 步骤:首先就要执行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;//返回位置
}
  • 还有几个操作没有写。今天就先写到这里了(2018.12.4)

Splay入门

标签:next   存在   直接   更新   ast   理解   插入   删掉   splay   

原文地址:https://www.cnblogs.com/BigYellowDog/p/10262121.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!