1. 程式人生 > >資料結構之伸展樹(二)

資料結構之伸展樹(二)

之前寫了一篇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特性