Splay学习笔记(保姆级)

记录我对于Splay的学习和理解。node

首先介绍Splay学习

伸展树(Splay Tree),也叫分裂树,是一种二叉排序树,它能在O(log n)内完成插入、查找和删除操做。它由丹尼尔·斯立特Daniel Sleator 和 罗伯特·恩卓·塔扬Robert Endre Tarjan 在1985年发明的。 
spa

在伸展树上的通常操做都基于伸展操做:假设想要对一个二叉查找树执行一系列的查找操做,为了使整个查找时间更小,被查频率高的那些条目就应当常常处于靠近树根的位置。因而想到设计一个简单方法, 在每次查找以后对树进行重构,把被查找的条目搬移到离树根近一些的地方。伸展树应运而生。伸展树是一种自调整形式的二叉查找树,它会沿着从某个节点到树根之间的路径,经过一系列的旋转把这个节点搬移到树根去。设计

它的优点在于不须要记录用于平衡树的冗余信息。(源自百度百科)code

好了,最好的理解就是先画一幅图来表示orm

 

 这就是一个简单的平衡树,知足左儿子比父亲小,右儿子比父亲大,而且任何一个子树也都是平衡树。但这个平衡树可能会出现比较极端的状况变成一条链,即2 3 4 5 6,为了解决这个问题咱们能够用到Splay。具体原理即咱们能够用修改结点的顺序来避免这种状况,好比上面这幅图能够旋转结点2变为:blog

这就是将2旋转(rotate)。咱们能够发现旋转以后它仍是一个平衡树,那么咱们再旋转一下结点2排序

 

 

经过这三幅图咱们能够发现旋转的一些规律了:get

1.旋转的结点位置变到它的父结点的位置,父结点到该结点原来相对的位置(即原来是左儿子则父结点到右儿子的位置,反之亦然)。it

2.父结点的另外一个儿子不变,原旋转的结点的位置变为旋转的结点的相对的儿子(即原来是左儿子,则它的右儿子变为父结点的左儿子)。

用代码表示会更加直观。(root记录根结点,sum记录结点的数量)

 

struct node
{
    int ch[2], ff, val, size, cnt;//左右儿子 父结点 数值大小 树的大小 val的数量 
}tree[N];
int root, sum;
void pushup(int x) { tree[x].size = tree[tree[x].ch[0]].size + tree[tree[x].ch[1]].size + tree[x].cnt;//size为左儿子的size加右儿子的size加cnt } inline void rotate(int x)//x为要旋转的结点 { int y = tree[x].ff, z = tree[y].ff;//y为x的父结点 z为x的祖父结点 int k = tree[y].ch[1] == x;//k表明x是y的哪一个儿子 0为左儿子 1为右儿子 tree[y].ff = x;//y的父亲变为x tree[x].ff = z;//x的父亲变为z tree[tree[x].ch[!k]].ff = y; //x的原来的与x的位置相对的那个儿子的父亲变为y tree[z].ch[tree[z].ch[1] == y] = x;//z的原来的y的位置变为x tree[y].ch[k] = tree[x].ch[!k];//y的原来x的位置变为x的与x相对的那个儿子变为y的儿子(我在说什么) tree[x].ch[!k] = y;//x的原来的与x的位置相对的那个儿子变为y pushup(x), pushup(y);//更新 }

 

 

这就是一个基本的旋转操做,是否是很是的简单(?)。

接下来就能够看看关于旋转的一些问题了了,假如咱们只旋转一个点的话会发现一条长链始终都会存在,如何解决呢?咱们能够旋转Y,但如何旋转,这个时候须要讨论的状况比较多,可是在看了一个大佬的讲解以后有一种更加简单的写法。

 

void Splay(int x, int goal)//将x旋转为goal的儿子,goal为0的时候即将x旋转为根结点
{
    int y, z;
    while (tree[x].ff != goal)
    {
        y = tree[x].ff;//父结点
        z = tree[y].ff;//祖父结点
        if (z != goal)
        {
            (tree[z].ch[0] == y) ^ (tree[y].ch[0] == x) ? rotate(x) : rotate(y);//假如x和y分别是y和z的同一个儿子则旋转y不然旋转x
        }
        rotate(x);//最后必须旋转x
    }
    if (goal == 0)//即x为根结点
    {
        root = x;
    }
}

 

这样写就十分简洁(感谢大佬)

而后是Find操做,若是理解了平衡树的话应该能独立写出find,即利用左儿子小右儿子大的性质,作到相似于二分查找。咱们能够将要Find的数字旋转到根结点,这样操做会更加方便,也能够直接return找到的位置。为了方便看懂下面代码会写更复杂一点(顺带一提,要查出x的排行便可在find以后利用子树的size求出)

inline void find(int x)//x为要寻找的数值
{
    int u = root;//u为根结点
    while (true)
    {
        if (tree[u].ch[x > tree[u].val])//避免树里面不存在x
        {
            break;
        }
        if (tree[u].val > x)//假如val大于x则x在u的左儿子
        {
            u = tree[u].ch[0];
        }
        if (tree[u].val < x)//假如val小于x则x在u的右儿子
        {
            u = tree[u].ch[1];
        }
        if (tree[u].val == x)//找到x
        {
            break;
        }
    }
    Splay(u, 0);//把查找到的位置旋转到根结点
}

接下来是insert,相似于find操做,但与find不一样的是假如找到其父结点以后不存在儿子的话能够生成一个新儿子。

inline void insert(int x)//插入x
{
    int u = root, ff = 0;
    while (tree[u].val != x && u)//若u为0则表明不存在
    {
        ff = u;//记录父结点
        u = tree[u].ch[tree[u].val < x];
    }
    if (u != 0)//即本就存在u
    {
        tree[u].cnt++;//数量加一
    }
    else
    {
        sum++;
        if (ff)//假如ff不为0
        {
            tree[ff].ch[tree[ff].val < x] = sum;
        }
        tree[sum].ff = ff;
        tree[sum].cnt = 1;
        tree[sum].size = 1;
        tree[sum].val = x;
    }
    Splay(sum, 0);//把该点旋转为根结点,同时能pushup
}

 写完insert以后咱们能够再了解一下前驱和后继,利用find将x移动到根节点上,前驱即在x的左子树,后继在x的右子树,还需考虑到x不存在的状况。具体可见代码注释(最后的if else能够简化在一块儿,为方便理解分开写)

inline int Next(int x, int k)//k=0表明前驱,k=1表明后继
{
    find(x);
    if (tree[root].val > x&& k == 1)//假如x不存在且要找的是后继
    {
        return root;//此时根结点即知足要求
    }
    if (tree[root].val < x && k == 0)//假如x不存在且要找的是前驱
    {
        return root;//此时根结点即知足要求
    }
    if (k)//找后继
    {
        int u = tree[root].ch[1];//令u为x的右子树,即大于x
        while (tree[u].ch[0])//要找的大于x的最小的数即找出左子树的值
        {
            u = tree[u].ch[0];//左子树必定小于右子树
        }
        return u;
    }
    else//找前驱
    {
        int u = tree[root].ch[0];//同理先令u为x的左子树
        while (tree[u].ch[1])//找最大值在右子树找
        {
            u = tree[u].ch[1];//右子树必定大于左子树
        }
        return u;
    }
}

在写出Next以后咱们能够拓展一下Next的应用,假如咱们要删除x,find(x)再删除的话是很麻烦的,由于还会牵连到x的子树,但咱们有没有办法将x的子树全都去掉呢?利用Next和平衡树的性质是能够的,倘若咱们将x的后继变为根结点,再将x的前驱变为x的后继的左儿子的话,此时x就为x的前驱的右儿子且绝对没有儿子。此时咱们就能够直接将其删除(同理也能够将x的前驱变为根结点,能够本身画一下)。注意考虑到x的数量,具体也能够根据问题来修改。具体见代码:

inline void Delete(int x)
{
    int last = Next(x, 0);//找到x的前驱
    int next = Next(x, 1);//找到x的后继
    Splay(next, 0);//将x的后继变为根结点
    Splay(last, next);//将x的前驱变为x的后继的左儿子
    int del = tree[last].ch[1];//del为x的位置
    if (tree[del].cnt > 1)//若是x的数量大于一
    {
        tree[del].cnt--;
        Splay(del, 0);//将del移动到根结点同时能够更新树
    }
    else
    {
        tree[last].ch[1] = 0;//直接删除
        Splay(last, 0);//将last移动到根结点同时更新树
    }
}

最后咱们能够尝试求出kth,注意是求出第k小的而不是第k大= =,kth能够利用size来求出,根据排名和size能够判断x是在左子树仍是右子树或者结点上,一路循环便可

inline int kth(int x)
{
    int u = root;
    if (tree[u].size < x)//即x大于树的大小此时不存在
    {
        return 0;
    }
    while (true)
    {
        if (tree[tree[u].ch[0]].size + tree[u].cnt >= x)//假如x在左子树或根结点上
        {
            if (tree[tree[u].ch[0]].size >= x)//假如左子树的大小大于x
            {
                u = tree[u].ch[0];//即x在左子树里面找
            }
            else//即x在根结点上
            {
                return tree[u].val;
            }
        }
        else//x在右子树上
        {
            x -= tree[tree[u].ch[0]].size + tree[u].cnt;
            u = tree[u].ch[1];
        }
    }
}

这些是根据洛谷P3369学习的一些基本操做,还有一些操做后续填坑。

相关文章
相关标签/搜索