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


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

另外本文中很多內容經不起推敲,然而我懶得改了。。。
QwQ......


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


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

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

上面這個丑陋的東西就是一棵平衡樹,他現在很平衡,是一棵滿二叉樹,高度正好是logn。。。
但是。。
如果這個丑陋的東西極端一點,他就會變成這樣。。。
這里寫圖片描述

這張圖依然很丑

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


各位大佬們為了解決平衡樹這個尷尬的問題,想出了各種方法。。
也就是弄出了各種樹。。。。(然而cj大佬都會)
然后有一個注明的大佬叫做Tarjan,弄出了splay這個玩意。。。


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

但是,,我怎么說也說不清呀。。
弄張丑陋的圖過來
這里寫圖片描述

這是一個丑陋的平衡樹的一部分
其中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的基本操作
先留個坑,以后再慢慢補。。。


免責聲明!

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



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