來自兩年后的提示
本篇文章只是娛樂向的介紹性文章,可以進行初步理解。
\(\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的基本操作
先留個坑,以后再慢慢補。。。