學習筆記:平衡樹-splay


嗯好的今天我們來談談cosplay

splay是一種操作,是一種調整二叉排序樹的操作,但是它並不會時時刻刻保持一個平衡,因為它會根據每一次操作把需要操作的點旋轉到根節點上

所謂二叉排序樹,就是滿足對樹中的任意一個節點,它左子樹上的任意一個值比它的值小右子樹上的任意一個值比它的值大的一棵二叉樹 ;至於平衡:是一棵空樹或任意節點的左右兩個子樹的深度差的絕對值不超過1(from:百度百科) 看圖:

不平衡:                                                                                平衡:

   

可以觀察到這兩棵樹都是滿足⑥<⑤<④<③<①<②的大小關系,只是改變每個點的相對位置不同

然后呢,splay的真正含義就出來了:通過很多很多次變換把要處理的點放到根節點上。與此同時,我們把這種變換叫做旋轉。旋轉的精髓就是在不破壞樹中的大小順序的同時改變節點的位置。旋轉是splay的核心(雖然大部分排序樹的核心操作都是旋轉),大概思路和treap是差不多的(沒學過treap然后爆發出狂笑),但對於splay相對來說懶一點的是它轉的時候是自動判斷左旋還是右旋。每一次旋轉,我們把被旋轉的這個點向上轉一層(也就是轉到它父節點的位置上)

比如說要旋轉⑤節點:                                             可以看看旋轉之后的樣子:

  

因為⑤<④<③,所以要把⑤旋轉后應該是③的左子樹(也就是④的位置),但是⑧始終是大於④的所以⑧的位置是不變的 (④的右子樹),然后因為⑤>④>⑦,所以④和⑦都應該是⑤的右子樹而且⑦應該是④的左子樹(因為是一棵二叉樹),然后可以發現與⑥,⑧沒有任何關系,真正改變的就只有圖中的紅線(表示各個節點間的父子關系)。這就是旋轉的步驟。

但如果⑤是④的右子樹呢?其實是差不多的,就是左右翻轉一下。再想想這個圖,自己推一遍:

 

好的,那么旋轉的普遍規律就可以看出來了:整個旋轉的操作中改變的只是被旋轉點、它的父節點、以及它的某一棵子樹,到底是哪一棵子樹是根據被旋轉點與它父親的關系來決定的:如果這個點是它父節點的左子樹,那么應該調整這個點的右子樹;如果是右子樹,則相反。

(有兩個很棒的動圖來表示旋轉)

       

 

 

 接下來打出代碼。在那之前,我們先定義數組:

son[i][0]是指點i的左節點編號,son[i][1]右節點編號

root表示當前根節點的編號

sz是指整個樹的值的種數,同時用於新節點插入時的編號(可以類比時間戳)

siz[i]表示以i為頂點的樹的值的個數,要計算重復出現的值(包括i節點自己)

key[i]表示節點i的值是多少

fa[i]表示節點i的父節點編號

cnt[i]表示節點i的值出現了多少次數量cnt,siz[i]和種類sz是兩個東西不要搞混了)

 因為旋轉的方式是由它是左子樹還是右子樹決定的,所以我們可以先寫一個函數來判斷:

int get(int x){
    return son[fa[x]][1]==x;//如果它父節點的右兒子編號等於它那么返回1(右節點),否則返回0(是左節點)
}

接下來就是旋轉了:

void rotate(int x){
    int f=fa[x],ff=fa[f],w=get(x);//父節點、祖父節點、是父節點的左子樹還是右子樹
    son[f][w]=son[x][w^1];//x節點的另一個子樹放給原本x節點的位置
    fa[son[f][w]]=f;//更新x節點的另一個子樹的父節點
    son[x][w^1]=f;//將父節點接到x節點的另一個子樹上
    fa[f]=x;
    fa[x]=ff;//f、x位置互換后更新祖父節點
    if(ff){//父節點不是根節點(根節點的父節點為0)
        son[ff][son[ff][1]==f]=x;
    }
    update(f);
    update(x);
}

 注意這里的^位運算,意思是兩位不同時返回1,相同時返回0,這里^1可以快速找出另一個兒子(0^1=1,1^1=0

 這里的update是指旋轉之后由於位置的改變而引起的種類總數的變化,因為每一個節點值的出現的次數是沒有改變的,其實很簡單就是左子樹種類加上右子樹種類:

void update(int x){
    if(x!=0){//如果是根節點,旋轉時種類數始終不變
        siz[x]=cnt[x];//自身值的出現次數
        if(son[x][0])//如果有左子樹
            siz[x]+=siz[son[x][0]];
        if(son[x][1])//如果有右子樹
            siz[x]+=siz[son[x][1]];
    }
}

這只是一次操作,我們前面說了我們是把要處理的點旋轉到根節點上,那么怎么做呢?循環就行了。但是還有一種特殊情況,由於這種情況都滿足被旋轉點和父節點都是左節點或者是右兒子,我們姑且稱它為三點一線,先看圖:

比如說我們這里要splay④,如果直接把④一直旋轉到根節點的話就會是這樣:

可以看見③還是①的左節點,相當於只是改變了④和①的關系,專業一點就是說形成了單旋使平衡樹失衡。而解決的方法就是在出現三點一線時先旋轉它的父節點避免單旋,正確的應該是這樣:

void splay(int x){
    for(int f;f=fa[x];rotate(x)){//注意旋轉的始終是x
        if(fa[f]){//可能存在三點一線
            rotate(get(x)==get(f)?f:x);//三點一線情況判斷
        }
    }
}

接下來是splay的插入:由於在旋轉的時候,我們是建立在這棵樹是有序的前提下的,而要保證這個前提,就需要從這棵樹的建立開始就讓它有序,所以,我們在插入的時候就必須按照排序樹的規定來插入,也就是說每次插入都是從根節點開始比較大小,直到找到這個值或者是找到樹的底部(這個值沒有出現過)

void insect(int v){
    if(sz==0){//如果這棵樹是空樹,插入點應該為根節點
        sz++;
        son[1][1]=son[1][0]=fa[1]=0;
        siz[1]=cnt[1]=1;
        root=1;
        key[1]=v;
        return;
    }
    int now=root,f=0;//now表示現在查找到節點編號,f表示當前節點的父節點
    while(1){
        if(key[now]==v){//如果這個值已經在樹中,那么它出現的次數增加
            cnt[now]++;
            update(now);//更新相關點
            update(f);
            splay(now);//將插入的點旋轉到根節點,便於下一次可能的操作
            break;
        }
        f=now;
        now=son[now][v>key[now]];//依照節點值查找位置,如果大於當前值v>key[now]=1,則在右子樹范圍內,反之亦然
        if(now==0){//樹中無這個值,查找到樹的底端,新建一個子節點
            sz++;//新節點編號
            son[sz][1]=son[sz][0]=0;//新節點初始化
            fa[sz]=f;
            siz[sz]=cnt[sz]=1;
            son[f][v>key[now]]=sz;//判斷新節點是左節點還是右節點並更新父節點
            key[sz]=v;
            update(f);//更新數量
            splay(sz);//旋轉到根節點
            break;
        }
    }
}

 接下來是查找一個值在樹中排從小到大第幾(是比它小的有多少個)(如果為了方便理解也可以說是從大到小求倒數第幾),原理很簡單,因為排序樹的性質,我們可以知道只要是在這個節點的左邊的都是比它小的,那么我們就可以利用之前記錄好的siz來快速求出:(注意是數量,要加上重復的,比如說1,2,2,3,這個數列中,3排第4)

int find(int v){
    int ans=0,now=root;//ans記錄已經有多少比它小的點,now表示正在尋找的節點的編號
    while(1){
        if(v<key[now]){//如果當前節點的值大於v,那么當前節點的左子樹不完全小於v,繼續向當前節點的左子樹尋找
            now=son[now][0];
        }
        else{//當前節點的左子樹上的值必然全部小於v
            ans+=(son[now][0]!=0?siz[son[now][0]]:0);//如果有左子樹則直接加上左子樹的數量
            if(v==key[now]){//如果當前節點的值等於v,則右子樹上不可能有比它小的數,所有比它小的數已經找完
                splay(now);//下一次可能的操作
                return ans+1;//有1個數比它小那么它應該是第2,以此類推,要+1
            }
            ans+=cnt[now];//key[now]<v的情況,除了它的左兒子還要加上它自身的數量
            now=son[now][1];//右子樹中可能存在比v小的值,所以在右子樹中繼續尋找
        }
    }
}

有了查找排名,如果需要知道第幾是多少,也是可以求的:

int findx(int x){
    int now=root;//當前節點
    while(1){
        if(son[now][0]!=0&&siz[son[now][0]]>=x){//如果左子樹的數量大於x,就是說第x個是在左子樹上(前提是有左子樹)
            now=son[now][0];//在左子樹上接着搜索
        }
        else{//第x個在以當前節點為頂點的樹中
            int less=(son[now][0]!=0?siz[son[now][0]]:0)+cnt[now];//左子樹的數量(可能沒有)+當前節點的值的數量
            if(x<=less)//由於之前判斷過是否在左子樹上,並且在之后的運算中排除了所有左子樹,x卻不在右子樹上,那么只可能是當前點的值
                return key[now];
            x-=less;//在右子樹中還有多少值比它小,排除左子樹
            now=son[now][1];//繼續搜索
        }
    }
}

接下來是前驅,就是指比它小的第一個點(就是比它自己的值小而且它的前驅與它自己的值的差的絕對值最小)。在樹中表現為它的左子樹中最右邊的那個點,這樣才滿足比它自身的值小並且最接近它自身的值

int query_pre(int x){
   splay(x);//首先旋轉到根節點方便查找
int now=son[root][0];//定位左子樹 while(son[now][1]!=0)now=son[now][1];//循環查找左子樹中最右邊的點 return now;//最底部跳出循環,找到答案 }

有前驅當然也有后繼,類似的,后繼是指比它大的第一個點比它自己的值大而且它的后繼與它自己的值的差的絕對值最小)。在樹中表現為它右子樹最左邊的那個點:(原理一模一樣)

int query_next(int x){
    splay(x);
    int now=son[root][1];
    while(son[now][0]!=0)now=son[now][0];
    return now;
}

Last but not the least:刪除。這里的刪除是刪除一次值,就是說受cnt的影響,不一定是刪一個節點。這個挺復雜的,比如說刪除一個值v,首先我們用之前的find(v)將值等於v的點旋轉到根上,之后要分5種情況:

  • 這個值出現了不止一次:直接減少次數,更新數量即可;
  • 整棵樹只剩下它一個值 孤苦伶仃 :將樹清空
  • 這個點沒有左子樹:直接將右子樹的頂點提出來,取代它的父節點;
  • 這個點沒有右子樹:直接將左子樹的頂點提出來,取代它的父節點;
  • 這個點左右子樹都有:先把前驅為作新樹的根(叫做新根)(后繼也完全沒有問題,只是之后的處理要想一想是左子樹還是右子樹),將它旋轉到根,這個時候在將原根的右兒子接到新根的左兒子上,更新即可。

這里先給出清空:(就是把所有關於這個點的東西歸零

void clear(int x){
    son[x][0]=son[x][1]=fa[x]=siz[x]=key[MX]=cnt[x]=0;
}

接下來就可以開始刪除了:

void del(int v){
    find(v);
    if(cnt[root]>1){//第一種情況
        cnt[root]--;
        update(root);
        return;
    }
    if(son[root][0]==0&&son[root][1]==0){//第二種情況
        clear(root);
        root=0;//將樹清空
        return;
    }
    if(son[root][0]==0){//第三種情況
        int old=root;
        root=son[root][1];
        fa[root]=0;//新根的父節點更新
        clear(old);
        return;
    }
    if(son[root][1]==0){//第四種情況
        int old=root;
        root=son[root][0];
        fa[root]=0;
        clear(old);
        return;
    }
    int newroot=query_per(root),oldroot=root;
    splay(newroot);//將新根轉上來
    fa[son[oldroot][1]]=newroot;
    son[root][1]=son[oldroot][1];//繼承右子樹
    clear(oldroot);//舊根歸零
    update(root);//更新新根
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM