zoukankan      html  css  js  c++  java
  • Splay伸展树学习笔记

    Splay伸展树

    有篇Splay入门必看文章 —— CSDN链接

    经典引文

    空间效率:O(n)
    时间效率:O(log n)插入、查找、删除
    创造者:Daniel Sleator 和 Robert Tarjan
    优点:每次查询会调整树的结构,使被查询频率高的条目更靠近树根。

    Tree Rotation


     
    树的旋转是splay的基础,对于二叉查找树来说,树的旋转不破坏查找树的结构。
     

    Splaying

     
    Splaying是Splay Tree中的基本操作,为了让被查询的条目更接近树根,Splay Tree使用了树的旋转操作,同时保证二叉排序树的性质不变。
    Splaying的操作受以下三种因素影响:
    • 节点x是父节点p的左孩子还是右孩子
    • 节点p是不是根节点,如果不是
    • 节点p是父节点g的左孩子还是右孩子
    同时有三种基本操作:
     

    Zig Step


    当p为根节点时,进行zip step操作。
    当x是p的左孩子时,对x右旋;
    当x是p的右孩子时,对x左旋。
     

    Zig-Zig Step

    当p不是根节点,且x和p同为左孩子或右孩子时进行Zig-Zig操作。
    当x和p同为左孩子时,依次将p和x右旋;
    当x和p同为右孩子时,依次将p和x左旋。
     
     

    Zig-Zag Step

    当p不是根节点,且x和p不同为左孩子或右孩子时,进行Zig-Zag操作。
    当p为左孩子,x为右孩子时,将x左旋后再右旋。
    当p为右孩子,x为左孩子时,将x右旋后再左旋。
     
     

    应用

     
    Splay Tree可以方便的解决一些区间问题,根据不同形状二叉树先序遍历结果不变的特性,可以将区间按顺序建二叉查找树。
    每次自下而上的一套splay都可以将x移动到根节点的位置,利用这个特性,可以方便的利用Lazy的思想进行区间操作。
    对于每个节点记录size,代表子树中节点的数目,这样就可以很方便地查找区间中的第k小或第k大元素。
    对于一段要处理的区间[x, y],首先splay x-1到root,再splay y+1到root的右孩子,这时root的右孩子的左孩子对应子树就是整个区间。
    这样,大部分区间问题都可以很方便的解决,操作同样也适用于一个或多个条目的添加或删除,和区间的移动。

    自学笔记

    今天开始自己动手写Splay。身边的小伙伴大多是用下标和数组来维系各个结点的联系,但是我还是一如既往的喜欢C++的指针(❤ ω ❤)。

    结点以一个struct结构体的形式存在。

    struct node {
        int value;
        node *father;
        node *son[2];
        
        node (int v = 0, node *f = NULL) {
            value = v;
            father = f;
            son[0] = NULL;
            son[1] = NULL;
        }
    };

    其中,son[0]代表左儿子,son[1]代表右儿子。

    用一个函数来判断子节点是父节点的哪个儿子

    inline bool son(node *f, node *s) {
        return f->son[1] == s;
    }

    返回值就是son[]数组的下标,这个函数很方便。

    最关键的是旋转操作,有别于常见的zig,zag旋转,我喜欢用一个函数实现其两者的功能,即rotate(x)代表将x旋转到其父节点的位置上。

    inline void rotate(node *t) {
        node *f = t->father;
        node *g = f->father;
        bool a = son(f, t), b = !a;
        f->son[a] = t->son[b];
        if (t->son[b] != NULL)
            t->son[b]->father = f;
        t->son[b] = f;
        f->father = t;
        t->father = g;
        if (g != NULL)
            g->son[son(g, f)] = t;
        else
            root = t;
    }

    函数会自行判断x实在父节点的左儿子上还是右儿子上,并自动左旋或右旋。这里要注意改变祖父结点的儿子指针,以及结点的父亲指针,切忌马虎漏掉。同时还要事先做好特判,放止访问非法地址。这里用指针相较于用下标的一个好处就是,如果你不小心访问了NULL即空指针,也是下标党常用的0下标,指针写法一定会RE,而下标写法可能就不会崩溃,因而不易发现错误,导致一些较为复杂而智障的错误。

    然后是核心函数——Splay函数,貌似也有人叫Spaly的样子,然而我并没有考证什么。Splay(x,y)用于将x结点旋转到y结点的某个儿子上。特别地,Splay(x,NIL)代表将x旋转到根节点的位置上。根节点的父亲一般是NIL或0。

    inline void spaly(node *t, node *p) {
        while (t->father != p) {
            node *f = t->father;
            node *g = f->father;
            if (g == p)
                rotate(t);
            else {
                if (son(g, f) ^ son(f, t))
                    rotate(t), rotate(t);
                else
                    rotate(f), rotate(t);
            }
        }
    }

    这里值得注意的是两种双旋。如果t(该节点),f(父亲节点),g(祖父节点)形成了一条单向的链,即[右→右]或[左→左]这样子,那么就先对父亲结点进行rotate操作,再对该节点进行rotate操作;否则就对该节点连续进行两次rotate操作。据称单旋无神犇,双旋O(logN),这句话我也没有考证,个人表示不想做什么太多的探究,毕竟Splay的复杂度本来就挺玄学的了,而且专门卡单旋Splay的题也没怎么听说过。对了,这个双旋操作和AVL的双旋是不是有那么几分相似啊,虽然还是不太一样的吧,好吧其实也不怎么像╮(╯-╰)╭。

    接下来谈谈插入操作。插入操作就和普通的二叉搜索树类似,先找到合适的叶子结点,然后在空着的son[]上新建结点,把值放入。不同的是需要把新建的结点Splay到根节点位置,复杂度需要,不要问为什么。

    inline void insert(int val) {
        if (root == NULL)
            root = new node(val, NULL);
        for (node *t = root; t; t = t->son[val >= t->value]) {
            if (t->value == val) { spaly(t, NULL); return; }
            if (t->son[val >= t->value] == NULL)
                t->son[val >= t->value] = new node(val, t);
        }
    }

    注意,这个插入函数实现的是非重集合。

    与之对应的就是删除操作,相对的复杂一些。删除一个元素,需要先在树中找到这个结点,然后把这个结点Splay到根节点位置,开始分类讨论。如果这个结点没有左儿子(左子树),直接把右儿子放在根的位置上即可;否则的话就需要想方设法合并左右子树:在左子树种找到最靠右(最大)的结点,把它旋转到根节点的儿子上,此时它一定没有右儿子,因为根节点的左子树中不存在任何一个元素比它更大,那么把根节点的右子树接在这个结点的右儿子上即可。

    inline void erase(int val) {
        node *t = root;
        for ( ; t; ) {
            if (t->value == val)
                break;
            t = t->son[val > t->value];
        }
        if (t != NULL) {
            spaly(t, NULL);
            if (t->son[0] == NULL) {
                root = t->son[1];
                if (root != NULL)
                    root->father = NULL;
            }
            else {
                node *p = t->son[0];
                while (p->son[1] != NULL)
                    p = p->son[1];
                spaly(p, t); root = p;
                root->father = NULL;
                p->son[1] = t->son[1];
                if (p->son[1] != NULL)
                    p->son[1]->father = p;
            }
        }
    }

    相较于insert()确实复杂了不少。

    以上就是Splay的框架了,是Splay必不可少的部分,在此基础上可以加入许多新的功能。

    例如,手动实现垃圾回收,这样新建结点的常数会小很多,毕竟C++的new是很慢的。

    node tree[siz], *stk[siz]; int top;
    
    inline node *newnode(int v, node *f) {
        node *ret = stk[--top];
        ret->size = 1;
        ret->value = v;
        ret->father = f;
        ret->son[0] = NULL;
        ret->son[1] = NULL;
        ret->reverse = false;
        return ret;
    }
    
    inline void freenode(node *t) {
        stk[top++] = t;
    }

    有的时候需要我们维护子树大小。

    inline int size(node *t) {
        return t == NULL ? 0 : t->size;
    }
    
    inline void update(node *t) {
        t->size = 1;
        t->size += size(t->son[0]);
        t->size += size(t->son[1]);
    }

    简单,安全。

    维护区间翻转的时候需要用到打标记的方式。

    inline bool tag(node *t) {
        return t == NULL ? false : t->reverse;
    }
    
    inline void reverse(node *t) {
        if (t != NULL)
            t->reverse ^= true;
    }
    
    inline void pushdown(node *t) {
        if (tag(t)) {
            std::swap(t->son[0], t->son[1]);
            reverse(t->son[0]);
            reverse(t->son[1]);
            t->reverse ^= true;
        }
    }

    还有更为简洁的rotate函数。

    inline void connect(node *f, node *s, bool k) {
        if (f == NULL)
            root = s;
        else
            f->son[k] = s;
        if (s != NULL)
            s->father = f;
    }
    
    inline void rotate(node *t) {
        node *f = t->father;
        node *g = f->father;
        bool a = son(f, t), b = !a;
        connect(f, t->son[b], a);
        connect(g, t, son(g, f));
        connect(t, f, b);
        update(f);
        update(t);
    }

    @Author: YouSiki

  • 相关阅读:
    基于element-ui图片封装组件
    计算时间间隔具体每一天
    C语言学习笔记 —— 函数作为参数
    AtCoder Beginner Contest 049 题解
    AtCoder Beginner Contest 048 题解
    AtCoder Beginner Contest 047 题解
    AtCoder Beginner Contest 046 题解
    AtCoder Beginner Contest 045 题解
    AtCoder Beginner Contest 044 题解
    AtCoder Beginner Contest 043 题解
  • 原文地址:https://www.cnblogs.com/yousiki/p/6147455.html
Copyright © 2011-2022 走看看