Splay入門解析【保證讓你看不懂(滑稽)】

來自兩年後的提示
本篇文章只是娛樂向的介紹性文章,能夠進行初步理解。
\(\text{Splay}\)若是須要嚴格的證實均攤複雜度參考勢能分析。
另外\(\text{Splay}\)依靠\(rotate\)來維護\(size\)等節點維護的值。
若是代碼中沒有體現請不要忘記上面這句話。數組

另外本文中不少內容經不起推敲,然而我懶得改了。。。
QwQ......數據結構


BST真是神奇的東西。。。
並且種類好多呀。。。
我這個蒟蒻只學會了splay
orzCJ老爺,各類樹都會
好好好,不說了,直接說splay。spa


不知道splay是啥,,你也要知道平衡樹是啥。。。
平衡樹是一個神奇的數據結構,
對於任意一個節點,左兒子的值比它小,右兒子的值比它大
而且任意一棵子樹單獨拎出來也是一棵平衡樹
就像這樣。。。。
這裏寫圖片描述code

各位大佬請原諒我醜陋無比的圖blog

上面這個醜陋的東西就是一棵平衡樹,他如今很平衡,是一棵滿二叉樹,高度正好是logn。。。
可是。。
若是這個醜陋的東西極端一點,他就會變成這樣。。。
這裏寫圖片描述遞歸

這張圖依然很醜圖片

如今看起來,這個東西一點都不平衡。。。
二叉樹退化成了一條鏈
若是要查詢的話,,,最壞狀況下就變成了O(n)
這就很尷尬了。。。get


各位大佬們爲了解決平衡樹這個尷尬的問題,想出了各類方法。。
也就是弄出了各類樹。。。。(然而cj大佬都會)
而後有一個註明的大佬叫作Tarjan,弄出了splay這個玩意。。。ast


這個玩意怎麼解決上面的問題呢???
你是一個平衡樹是吧。。。
我把你的節點的順序修改一下,讓你仍是一棵平衡樹,在這個過程當中你的結構就變化了,就可能再也不是一條鏈了。
誒,這個看起來很厲害的感受。。。class

可是,,我怎麼說也說不清呀。。
弄張醜陋的圖過來
這裏寫圖片描述

這是一個醜陋的平衡樹的一部分
其中XYZ三個是節點,ABC三個是三棵子樹
如今這個玩意,我若是想把X弄到Y那個地方去要怎麼辦,這樣的話我就通過了旋轉,重構了這棵樹的結構,就可能讓他變得更加平衡

恩,咱們來看看怎麼辦。。。
X是Y的左兒子,因此X < Y
Y是Z的左兒子,因此Y < Z
因此X < Z,因此若是要把X弄到Y的上面去的話,X就應該放到Y的那個位置

繼續看,如今Y > X那麼Y必定是X的右兒子
可是X已經有了右兒子B,
根據平衡樹咱們能夠知道X < B < Y
因此咱們能夠把X的右兒子B丟給Y當作左兒子
而X的左兒子A有A < X < Y < Z顯然仍是X的左兒子

綜上,咱們一頓亂搞,原來的平衡樹被咱們搞成了這個樣子
這裏寫圖片描述

在檢查一下
原來的大小關係是
A < X < B < Y < C < Z

把X旋轉一下以後大小關係
A < X < B < Y < C < Z
誒,大小關係也沒有變
因此以前那棵平衡樹就能夠經過旋轉變成這個樣子
而且這個時候仍是一棵平衡樹
好神奇誒。。。

可是,XYZ的關係顯然不只僅只有這一種
有Y是Z的左兒子 X是Y的左兒子
有Y是Z的左兒子 X是Y的右兒子
有Y是Z的右兒子 X是Y的左兒子
有Y是Z的右兒子 X是Y的右兒子
一共4種狀況,你們能夠本身畫畫圖,轉一轉。


若是把上面的圖畫完了,咱們就能夠正式的來玩一玩splay了

轉完了上面四種狀況,咱們來找找規律

最明顯的一點,咱們把X轉到了原來Y的位置
也就是說,原來Y是Z的哪一個兒子,旋轉以後X就是Z的哪一個兒子

繼續看一看
咱們發現,X是Y的哪一個兒子,那麼旋轉完以後,X的那個兒子就不會變
什麼意思?
看一看我上面畫的圖
X是Y的左兒子,A是X的左兒子,旋轉完以後,A仍是X的左兒子
這個應該不難證實
若是X是Y的左兒子,A是X的左兒子
那麼A < X < Y旋轉完以後A仍是X的左兒子
若是X是Y的右兒子,A是X的右兒子
那麼A > X > Y 只是把不等式反過來了而已

再看一下,找找規律
若是原來X是Y的哪個兒子,那麼旋轉完以後Y就是X的另一個兒子
再看看圖
若是原來X是Y的左兒子,旋轉以後Y是X的右兒子
若是原來X是Y的右兒子,旋轉以後Y是X的左兒子
這個應該也很好證實吧。。。
若是X是右兒子 X > Y,因此旋轉後Y是X的左兒子
若是X是左兒子 Y > X,因此旋轉後Y是X的右兒子

因此總結一下:
1.X變到原來Y的位置
2.Y變成了 X原來在Y的 相對的那個兒子
3.Y的非X的兒子不變 X的 X原來在Y的 那個兒子不變
4.X的 X原來在Y的 相對的 那個兒子 變成了 Y原來是X的那個兒子

啊,,,寫出來真麻煩,用語言來寫一下
其中t是樹上節點的結構體,ch數組表示左右兒子,ch[0]是左兒子,ch[1]是右兒子,ff是父節點

void rotate(int x)//X是要旋轉的節點
{
    int y=t[x].ff;//X的父親
    int z=t[y].ff;//X的祖父
    int k=t[y].ch[1]==x;//X是Y的哪個兒子 0是左兒子 1是右兒子
    t[z].ch[t[z].ch[1]==y]=x;//Z的原來的Y的位置變爲X
    t[x].ff=z;//X的父親變成Z
    t[y].ch[k]=t[x].ch[k^1];//X的與X原來在Y的相對的那個兒子變成Y的兒子
    t[t[x].ch[k^1]].ff=y;//更新父節點
    t[x].ch[k^1]=y;//X的 與X原來相對位置的兒子變成 Y
    t[y].ff=x;//更新父節點
}

上面的代碼用了不少小小小技巧
好比t[y].ch[1]==x
t[y].ch[1]是y的右兒子,若是x是右兒子,那麼這個式子是1,不然是0,也正好對應着左右兒子
一樣的k^1,表示相對的兒子,左兒子0^1=1 右兒子1^1=0

好了,這就是一個基本的旋轉操做(別人講的


繼續看接下來的東西
如今考慮一個問題
若是要把一個節點旋轉到根節點(好比上面的Z節點呢)
咱們是否是能夠作兩步,先把X轉到Y再把X轉到Z呢?
咱們來看一看
這裏寫圖片描述

一個這樣的Splay

把X旋轉到Y以後

這裏寫圖片描述

再接着把X旋轉到Z以後

這裏寫圖片描述

好了,這就是對X連着旋轉兩次以後的Splay,看起來彷佛沒有什麼問題。
可是,咱們如今再來看一看
這裏寫圖片描述
原圖中的Splay有一條神奇鏈: Z->Y->X->B
而後再來看一看旋轉完以後的Splay
這裏寫圖片描述
也有一條鏈X->Z->Y->B

也就是說
若是你只對X進行旋轉的話,
有一條鏈依舊存在,
若是是這樣的話,splay極可能會被卡。

好了,
顯然對於XYZ的不一樣狀況,能夠本身畫圖考慮一下,
若是要把X旋轉到Z的位置應該如何旋轉

歸類一下,其實仍是隻有兩種:
第一種,X和Y分別是Y和Z的同一個兒子
第二種,X和Y分別是Y和Z不一樣的兒子

對於狀況一,也就是相似上面給出的圖的狀況,就要考慮先旋轉Y再旋轉X
對於狀況二,本身畫一下圖,發現就是對X旋轉兩次,先旋轉到Y再旋轉到X

這樣一想,對於splay旋轉6種狀況中的四種就很簡單的分了類
其實另外兩種狀況很容易考慮,就是不存在Z節點,也就是Y節點就是Splay的根了
此時不管怎麼樣都是對於X向上進行一次旋轉

那麼splay的旋轉也能夠很容易的簡化的寫出來

void splay(int x,int goal)//將x旋轉爲goal的兒子,若是goal是0則旋轉到根
{
    while(t[x].ff!=goal)//一直旋轉到x成爲goal的兒子
    {
        int y=t[x].ff,z=t[y].ff;//父節點祖父節點
        if(z!=goal)//若是Y不是根節點,則分爲上面兩類來旋轉
            (t[z].ch[0]==y)^(t[y].ch[0]==x)?rotate(x):rotate(y);
            //這就是以前對於x和y是哪一個兒子的討論
        rotate(x);//不管怎麼樣最後的一個操做都是旋轉x
    }
    if(goal==0)root=x;//若是goal是0,則將根節點更新爲x
}

這樣寫多簡單,比另一些人寫得分6種狀況討論要簡單不少。


應SYC大佬要求,繼續補充內容。


先是查找find操做
從根節點開始,左側都比他小,右側都比他大,
因此只須要相應的往左/右遞歸
若是當前位置的val已是要查找的數
那麼直接把他Splay到根節點,方便接下來的操做
相似於二分查找,
因此時間複雜度O(logn)

inline void find(int x)//查找x的位置,並將其旋轉到根節點
{
    int u=root;
    if(!u)return;//樹空
    while(t[u].ch[x>t[u].val]&&x!=t[u].val)//當存在兒子而且當前位置的值不等於x
        u=t[u].ch[x>t[u].val];//跳轉到兒子,查找x的父節點
    splay(u,0);//把當前位置旋轉到根節點
}

下一個Insert操做
往Splay中插入一個數
相似於Find操做,只是若是是已經存在的數,就能夠直接在查找到的節點的進行計數
若是不存在,在遞歸的查找過程當中,會找到他的父節點的位置,
而後就會發現底下沒有啦。。。
因此這個時候新建一個節點就能夠了

inline void insert(int x)//插入x
{
    int u=root,ff=0;//當前位置u,u的父節點ff
    while(u&&t[u].val!=x)//當u存在而且沒有移動到當前的值
    {
        ff=u;//向下u的兒子,父節點變爲u
        u=t[u].ch[x>t[u].val];//大於當前位置則向右找,不然向左找
    }
    if(u)//存在這個值的位置
        t[u].cnt++;//增長一個數
    else//不存在這個數字,要新建一個節點來存放
    {
        u=++tot;//新節點的位置
        if(ff)//若是父節點非根
            t[ff].ch[x>t[ff].val]=u;
        t[u].ch[0]=t[u].ch[1]=0;//不存在兒子
        t[tot].ff=ff;//父節點
        t[tot].val=x;//值
        t[tot].cnt=1;//數量
        t[tot].size=1;//大小
    }
    splay(u,0);//把當前位置移到根,保證結構的平衡。注意前面由於更改了子樹大小,因此這裏必須Splay上去進行pushup保證size的正確。
}

繼續,,,
前驅/後繼操做Next
首先就要執行Find操做
把要查找的數弄到根節點
而後,之前驅爲例
先肯定前驅比他小,因此在左子樹上
而後他的前驅是左子樹中最大的值
因此一直跳右結點,直到沒有爲止
找後繼反過來就好了

inline int Next(int x,int f)//查找x的前驅(0)或者後繼(1)
{
    find(x);
    int u=root;//根節點,此時x的父節點(存在的話)就是根節點
    if(t[u].val>x&&f)return u;//若是當前節點的值大於x而且要查找的是後繼
    if(t[u].val<x&&!f)return u;//若是當前節點的值小於x而且要查找的是前驅
    u=t[u].ch[f];//查找後繼的話在右兒子上找,前驅在左兒子上找
    while(t[u].ch[f^1])u=t[u].ch[f^1];//要反着跳轉,不然會愈來愈大(愈來愈小)
    return u;//返回位置
}

還有操做呀/。。。
刪除操做
如今就很簡單啦
首先找到這個數的前驅,把他Splay到根節點
而後找到這個數後繼,把他旋轉到前驅的底下
比前驅大的數是後繼,在右子樹
比後繼小的且比前驅大的有且僅有當前數
在後繼的左子樹上面,
所以直接把當前根節點的右兒子的左兒子刪掉就能夠啦

inline void Delete(int x)//刪除x
{
    int last=Next(x,0);//查找x的前驅
    int next=Next(x,1);//查找x的後繼
    splay(last,0);splay(next,last);
    //將前驅旋轉到根節點,後繼旋轉到根節點下面
    //很明顯,此時後繼是前驅的右兒子,x是後繼的左兒子,而且x是葉子節點
    int del=t[next].ch[0];//後繼的左兒子
    if(t[del].cnt>1)//若是超過一個
    {
        t[del].cnt--;//直接減小一個
        splay(del,0);//旋轉
    }
    else
        t[next].ch[0]=0;//這個節點直接丟掉(不存在了)
}

突然發現我連第K大都沒有寫,隨口口胡一下
從當前根節點開始,檢查左子樹大小
由於全部比當前位置小的數都在左側
若是左側的數的個數多餘K,則證實第K大在左子樹中
不然,向右子樹找,找K-左子樹大小-當前位置的數的個數
記住特判K剛好在當前位置

inline int kth(int x)//查找排名爲x的數
{
    int u=root;//當前根節點
    if(t[u].size<x)//若是當前樹上沒有這麼多數
        return 0;//不存在
    while(1)
    {
        int y=t[u].ch[0];//左兒子
        if(x>t[y].size+t[u].cnt)
        //若是排名比左兒子的大小和當前節點的數量要大
        {
            x-=t[y].size+t[u].cnt;//數量減小
            u=t[u].ch[1];//那麼當前排名的數必定在右兒子上找
        }
        else//不然的話在當前節點或者左兒子上查找
            if(t[y].size>=x)//左兒子的節點數足夠
                u=y;//在左兒子上繼續找
            else//不然就是在當前根節點上
                return t[u].val;
    }
}

還剩下一些splay的基本操做 先留個坑,之後再慢慢補。。。

相關文章
相關標籤/搜索