数据结构之伸展树(二)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Q1410136042/article/details/82735621

之前写了一篇Splay的博客【数据结构之伸展树(一)】,只是说了一下了它的原理及核心的伸展操作,后来发现具体在哪里应用splay我还是分不大清。

事实上,Splay常常用于实现可分裂与合并的序列,举个板栗,比如给你一个数组,将数组从某一个地方分成俩数组,或者给你俩数组,将他们直接连接成一个数组——这就是Splay的强项

就像上次写的伸展树博客里面说的,要处理一个区间[L,R]只需要将L-1伸展到根,R+1伸展到根的右儿子,然后就好操作了。先说分裂操作:将前k个结点从原来的splay里面分离出来,只需要将第k个结点伸展到根,然后让根与右子树断开连接,就分好了~然后再说合并,将合并后放左边的的Splay的最大的结点伸展到根,然后另外一棵Splay作为它的右子树~

上面操作里,"第k个结点","最大的结点",怎么求这俩玩意儿是关键~在实现中,我们用一个结构体来存结点,在结构体里加一个s,以保存以这个结点为根的子树有多少个结,这样如果一棵树的左子树的s是k-1,那么它自己就是第k个结点了,至于最大的结点,从根向右儿子递归下去,直到没有右儿子为止,就像普通BST一样

附上代码(顺便加上了splay翻转的代码)

// Splay Tree

#include<bits/stdc++.h>
struct Node // Splay Tree结点的定义
{
    Node* ch[2];    //左右子树
    int v;          //键值(1~n),表示这个结点是第v大的,键值成BST
    int s;          //以它为根的子树的总结点数
    int flip;       //延迟标记————是否需要翻转,如果不需要区间反转就不需要这个变量

    Node(int v=0):v(v)  {   ch[0] = ch[1] = NULL; s = 1;flip = 0;}
    int cmp(const int& x)  const // 第x大的元素在左子树还是右子树(或者是其本身?)
    {
        int t = ch[0] == NULL ? 0 : ch[0]->s;
        if(x == t+1)  return -1;
        return x <= t ? 0 : 1;
    }
    void maintain()
    {
        s = 1;
        if(ch[0] != NULL)   s += ch[0]->s;
        if(ch[1] != NULL)   s += ch[1]->s;
    }

    void pushdown() // 延迟标记的下沉函数,如果不需要区间反转就不需要这个函数
    {
        if(flip)
        {
            Node* p = ch[0];
            ch[0] = ch[1];
            ch[1] = p;
            flip = 0;
            if(ch[0] != NULL)   ch[0]->flip ^= 1;
            if(ch[1] != NULL)   ch[1]->flip ^= 1;
        }
    }

};


void rotate(Node* &o, int d)//d=0代表左旋,d=1代表右旋,最终o仍然指向根
{
    Node* k = o->ch[d^1];
    o->ch[d^1] = k->ch[d];
    k->ch[d] = o;
    o->maintain();
    k->maintain();
    o = k;
}

void insert(Node* &o, int x)//在以o为根的子树插入键值x,修改o继续为根节点(假设没有x)
{
    if(o == NULL)
        o = new Node(x);
    else
    {
        int d = (x < o->v ? 0 : 1);
        insert(o->ch[d], x);
    }
    o->maintain();
}

// 将int数组a[n]转化成伸展树,中序遍历出来仍然是a[](如果原来无序,建树后仍然无序)
void build(Node* &rt, int a[], int n)
{
    if(n <= 2)
    {
        for(register int i = 0; i < n; ++ i)
            insert(rt, a[i]);
        return;
    }
    insert(rt, a[n/2]);
    build(rt, a, n/2);
    build(rt, a+n/2+1, n-n/2-1);
}

void remove(Node* &o, int x)//在以o为根的子树中删去元素第x大的元素,修改o继续为根节点
{
    int d = o->cmp(x);//如果需要删除元素x(x存在SPT里的话),只需要修改这一句就好
    if(d == -1)
    {
        if(o->ch[0] == NULL)        o = o->ch[1];
        else if(o->ch[1] = NULL)    o = o->ch[0];
        else
        {
            int d2 = (o->ch[0]->s > o->ch[1]->s ? 1 : 0);
            rotate(o, 0);
            remove(o->ch[0], x);
        }
    }
    else
        remove(o->ch[d], x);
    if(o != NULL)
        o->maintain();
}

void splay(Node* &o, int k)//找到第k大的元素并伸展到根
{
    o->pushdown();//如果没有区间反转就不需要这一句
    int d = o->cmp(k);//看第k小的数是在左子树还是右子树
    int t = o->ch[0] == NULL ? 0 : o->ch[0]->s;
    if(d == 1)  k -= t + 1;//如果在右子树,那么o的第k小数就是是右子树的第 (k - (o->ch[0]->s + 1)) 小数(也就是减去左子树节点数以及o结点)
    if(d != -1)//只需要考虑d!=-1,因为当d==-1,第k个元素就在根上
    {
        Node* p = o->ch[d];//直接找那棵子树
        p->pushdown();//如果没有区间反转就不需要这一句
        int d2 = p->cmp(k);//同上面的d
        t = p->ch[0] == NULL ? 0 : p->ch[0]->s;
        int k2 = (d2 == 0 ? k : k - t - 1);//同上面的if(d == 1)
        if(d2 != -1)
        {
            splay(p->ch[d2], k2);
            if(d == d2) //加上最后的旋转构成一字双旋
                rotate(o, d^1);
            else        //加上最后的旋转构成之字双旋
                rotate(o->ch[d], d);
        }
        rotate(o, d^1);//配合if(d2!=-1)里面内容构成双旋,或者if条件不成立,即单旋
    }
}

Node* merge(Node* left, Node* right)//合并left和right。假定left的所有元素比right小。注意right可以是null,但left不可以
{
    splay(left, left->s);
    left->ch[1] = right;
    left->maintain();
    return left;
}

// 把o的前k小结点放在left里,其它的放在right里。1<=k<=o->s。当k=o->s时,right=null
void split(Node* o, int k, Node* &left, Node* &right)
{
    splay(o, k);
    left = o;
    right = o->ch[1];
    o->ch[1] = NULL;
    left->maintain();
}

void print(Node* o) // 中序遍历输出splayTree
{
    o->pushdown();//如果没有区间反转就不需要这一句
    if(o->ch[0] != NULL)
        print(o->ch[0]);
    printf("%d\n", o->v);
    if(o->ch[1] != NULL)
        print(o->ch[1]);
}

之前做一个Splay的题,给一个序列,然后中间截一段翻转一下,放到后面,输出新的序列。当时就在想,splay是一棵BST,为什么BST这样弄完还会保持BST的性质。不知道有没有人和我想的一样~

后来我想明白了,与其说Splay的键值构成BST,不如说Splay的索引值构成BST,也就是说a[1]恒在a[2]前面,a[m]恒在a[m+1]前面~~这样想上面的问题就好理解了,翻转放到后面,实际上是对索引值的修改,修改了索引值,然后通过伸展操作来维护其BST特性

猜你喜欢

转载自blog.csdn.net/Q1410136042/article/details/82735621