splay的认识,由画图开始

版权声明:https://blog.csdn.net/qq_41730082 https://blog.csdn.net/qq_41730082/article/details/87926219

先说一下,这个大神讲的蛮好的,可以看看他的图哦

————可以跟着这个大佬的图来画,我这里讲的是学习他的讲解之后的学习的方法,以及一些笔记了。


  然后就是自己对于splay的观点了初识splay是在寒假集训的时候,但是压根就没听懂,于是痛定思痛,回家之后时常迷离的我直到返校开始学习的第一天才有了属于自己的大体思路(复习提纲吧)。

  现实从旋转开始认识自己对splay的初识,rotate()就是旋转的英文单词,我们将一个节点旋转到父节点的位置上去,也就是跟它的父节点换个位置,但是总不能丢下其他的节点吧,就按着yyb大神的方法来进行挪动然后列写了如下代码:

inline void rotate(int x)  //对x进行旋转操作
{
    int y = t[x].ff, z = t[y].ff;   //x的父亲、x的祖父
    int k = t[y].ch[1] == x;    //x是y的哪个父亲
    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];  //y的原x在y方向上的子节点变为x的反方向的儿子
    t[t[x].ch[k^1]].ff = y; //更新对应点的父节点
    t[x].ch[k^1] = y;   //y是x的k反方向的节点(恒成立)
    t[y].ff = x;    //更新y的父亲
    pushup(y);  pushup(x);  //x和y的旗下节点的个数会变————必须先更新y再更新x
}

  为什么需要pushup()呢?(这里yyb大神没有讲诶)是因为要向上推size,为了求类似第K大之类的问题。

  下面就接着讲讲由rotate转变过来的更加高深的东西——splay(),就是将一棵二叉树旋转为平衡二叉树,什么是平衡二叉树,就是所有的最深的节点的深度是最小的情况即可。

  那么就要引入splay函数了,我们可以将需要移动的点看成两种情况,将点设为x,x的父节点为y,x的祖父节点为z,那么如果x在y的儿子的方向与y在z的儿子的方向上是相同的话,就是情况一;若不相同就是情况二,于是我们对这两个情况进行判断。

  情况一:x和y分别是y和z的同一个方向上的儿子;

  情况二:x和y分别是y和z的不同一个方向上的儿子。

  对于这两个情况进行判断,我们可以发现对于这两种情况,想让他们都变成平衡的二叉树,需要这么做:

  对于情况一,我们先旋转y再旋转x即可;

  对于情况二,我们先旋转x两次,然后再旋转y,最后旋转y即可,但是会发现,第一次旋转完两次x之后,x和y的关系一定就是同一方向的了,所以我们就没有必要分作六次判断了。

那么,代码就是这样的了(我们将x旋转到goal节点的下方,若goal为0则将x旋转到根节点):

inline void splay(int x, int goal)  //将x旋转为goal的儿子,若goal为0,则将x旋转至根的位置
{
    while(t[x].ff != goal)  //一直旋转到x成为goal的儿子
    {
        int y = t[x].ff, z = t[y].ff;   //父节点、祖父节点
        if(z != goal) (t[z].ch[0] == y) ^ (t[y].ch[0] == x) ? rotate(x) : rotate(y);    //若z不是根节点的话,分两种情况讨论
        rotate(x);  //最后都是要旋转x
    }
    if(goal == 0) root = x; //如果goal是0,那个根就是x
}

这就是splay了,我们将其旋转到固定的节点下方然后就停止,若goal为0的时候,还要给root根节点附上x的值。

  了解到了splay之后,我们需要了解到几种操作了:

  先来讲讲最基础的查找操作,就是找到一个值为x的点的位置,然后将该点挪到根节点上去,但是要注意了,若是棵空树就没有这样的节点了,所以记得判一下空树,接下来就是要考虑,然后根据平衡的二叉树的性质,我们可以将时间复杂度缩减为O(logn),就是不断的跟目前所遍历到的节点比较大小,若小于就是在左子树,反之就是去右子树去找这样的点,直到找到树空,那么就是最节点的那个点了。

代码是这样的:

void find(int x)    //查找值为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];    //若存在儿子并且当前为孩子的值不等于x,跳转至儿子
    splay(u, 0);    //将u旋转至根节点
}

  与find操作相似,就是插入操作了,我们一样需要找到一个合适的点的位置,那么就是去找一个空的节点,然后放进这个点,或者就是去找到一个存在的该点,然后给它的个数上再+1。

void insert(int x)  //插入x
{
    int u = root, ff = 0;   //当前位置u与父节点ff
    while(u && t[u].val != x)   //当u存在并且还没有移动到当前值
    {
        ff = 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].init(x, ff);    //建立新节点故左右儿子为空
    }
    splay(u, 0);    //把当前位置移动到根,保证结构平衡
}

  再接下来就是找到一个数的前驱或者后继(这个数可能并不在这棵树上),对于前驱或者后继,如果我们找到的第一个点的值就是我们所要查询的点的话,那么举个栗子,我们要是想要找前驱的话,就是找到该根节点的左子树的右子树的最底下的那个值,同理,要是找的是后继的话,就是第一个右子树的最下的左子树的那个值。

int Next(int x, int f)  //0前驱、1后继
{
    find(x);    //找到x的位置并且带到根上去,但是find()找到的点不一定就是恰好等于x的点基本上是"<=x"的点,也有可能是空树
    int u = root;
    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;
}

  既然找到了前后继那么再接下去就可以去删除节点了,为什么一定要先找到前后继呢,因为找到一个值的前驱以及后继,那么,将它的前驱移动到根节点上去,将它的后继放在前驱的右儿子上面,那么后继的左儿子不就是我们需要删除的节点了吗,若是存在就删去即可,若不存在,就直接跳过了。

void Delet(int x)   //删除操作
{
    int last = Next(x, 0), next = Next(x, 1);   //找到前驱和后驱
    splay(last, 0); splay(next, last);      //将前驱放到根节点上去,将后驱链接到前驱上去
    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大的思想即可了,具体还是上代码来看一下。

int Kth(int x)
{
    int u = root;
    if(t[u].size < x) return 0;
    while(true)
    {
        pushdown(u);
        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 u;
        }
    }
}

最后,我们总结一下。

  对于splay的所需要的结构体,就可以反应出很多的东西,我们知道结构体里会放了这些东西(当然,根据题目,放的东西还可能会多一些),但是,至少会有这些:ff求的是父节点,在很多情况下都是有用处的,譬如旋转,再譬如求splay的时候,就是要用到父节点来进行转换操作的;再有呢,就是val,用以记录目前节点的值的问题的,就是用以搜索的时候来不断的找最适合的点来优化时间的;还有就是size,它是可以用来求区间第K大的;还有个最重要的就是ch[2],0表示左儿子,1表示右儿子的记录左右儿子的数组,在很多时候都能派上用处。

  然后,还可以在这棵二叉树上加些什么呢,譬如可以加个lazy标记,就是对于各种情况的判断,也可以做些别的,后面学到了,可以来补充哦。

猜你喜欢

转载自blog.csdn.net/qq_41730082/article/details/87926219