caioj1130:伸展樹(模版)
目錄
題目描述
【題意描述】
寫一種資料結構,來維護一些數,其中需要提供以下操作:
1. 插入x數
2. 刪除x數(若有多個相同的數,應只刪除一個)
3. 查詢x數的排名(若有多個相同的數,應輸出最小的排名)
4. 查詢排名為x的數
5. 求x的前驅(前驅定義為小於x,且最大的數)
6. 求x的後繼(後繼定義為大於x,且最小的數)
【輸入格式】
第一行為n,表示操作的個數,下面n行每行有兩個數opt和x,opt表示操作的序號(1<=opt<=6)
(n < = 100000, 所有數字均在-10^7到10^7內 )
【輸出格式】
對於操作3,4,5,6每行輸出一個數,表示對應答案
Sample Input
8
1 10
1 20
1 30
3 20
4 2
2 10
5 25
6 -1
Sample Output
2
20
20
20
伸展樹的基本概念
伸展樹(Splay Tree),也叫分裂樹,是一種二叉排序樹,它能在O(log n)內完成插入、查詢和刪除操作。它由丹尼爾·斯立特Daniel Sleator 和 羅伯特·恩卓·塔揚Robert Endre Tarjan 在1985年發明的。
在伸展樹上的一般操作都基於伸展操作:假設想要對一個二叉查詢樹執行一系列的查詢操作,為了使整個查詢時間更小,被查頻率高的那些條目就應當經常處於靠近樹根的位置。於是想到設計一個簡單方法, 在每次查詢之後對樹進行重構,把被查詢的條目搬移到離樹根近一些的地方。伸展樹應運而生。伸展樹是一種自調整形式的二叉查詢樹,它會沿著從某個節點到樹根之間的路徑,通過一系列的旋轉把這個節點搬移到樹根去。
它的優勢在於不需要記錄用於平衡樹的冗餘資訊。
伸展操作
伸展操作Splay(x,S)是在保持伸展樹有序性的前提下,通過一系列旋轉將伸展樹S中的元素x調整至樹的根部。在調整的過程中,要分以下三種情況分別處理:
情況一:節點x的父節點y是根節點。這時,如果x是y的左孩子,我們進行一次Zig(右旋)操作;如果x是y的右孩子,則我們進行一次Zag(左旋)操作。經過旋轉,x成為二叉查詢樹S的根節點,調整結束。即:如果當前結點父結點即為根結點,那麼我們只需要進行一次簡單旋轉即可完成任務,我們稱這種旋轉為單旋轉。
情況二:節點x的父節點y不是根節點,y的父節點為z,且x與y同時是各自父節點的左孩子或者同時是各自父節點的右孩子。這時,我們進行一次Zig-Zig操作或者Zag-Zag操作。即:設當前結點為X,X的父結點為Y,Y的父結點為Z,如果Y和X同為其父親的左孩子或右孩子,那麼我們先旋轉Y,再旋轉X。我們稱這種旋轉為一字形旋轉。
情況三:節點x的父節點y不是根節點,y的父節點為z,x與y中一個是其父節點的左孩子而另一個是其父節點的右孩子。這時,我們進行一次Zig-Zag操作或者Zag-Zig操作。即:這時我們連續旋轉兩次X。我們稱這種旋轉為之字形旋轉。
如圖4所示,執行Splay(1,S),我們將元素1調整到了伸展樹S的根部。再執行Splay(2,S),如圖5所示,我們從直觀上可以看出在經過調整後,伸展樹比原來“平衡”了許多。而伸展操作的過程並不複雜,只需要根據情況進行旋轉就可以了,而三種旋轉都是由基本得左旋和右旋組成的,實現較為簡單。
Find(x,S):判斷元素x是否在伸展樹S表示的有序集中。
首先,與在二叉查詢樹中的查詢操作一樣,在伸展樹中查詢元素x。如果x在樹中,則再執行Splay(x,S)調整伸展樹。
- 加入操作
Insert(x,S):將元素x插入伸展樹S表示的有序集中。
首先,也與處理普通的二叉查詢樹一樣,將x插入到伸展樹S中的相應位置上,再執行Splay(x,S)。
- 刪除操作
Delete(x,S):將元素x從伸展樹S所表示的有序集中刪除。
首先,用在二叉查詢樹中查詢元素的方法找到x的位置。如果x沒有孩子或只有一個孩子,那麼直接將x刪去,並通過Splay操作,將x節點的父節點調整
到伸展樹的根節點處。否則,則向下查詢x的後繼y,用y替代x的位置,最後執行Splay(y,S),將y調整為伸展樹的根。
- 合併操作
join(S1,S2):將兩個伸展樹S1與S2合併成為一個伸展樹。其中S1的所有元素都小於S2的所有元素。首先,我們找到伸展樹S1中最大的一個元素x,再通過Splay(x,S1)將x調整到伸展樹S1的根。然後再將S2作為x節點的右子樹。這樣,就得到了新的伸展樹S。
- 啟發式合併
當S1和S2元素大小任意時,將規模小的伸展樹上的節點一一插入規模大的伸展樹,總時間複雜度O(Nlg^2N)。
- 劃分操作
Split(x,S):以x為界,將伸展樹S分離為兩棵伸展樹S1和S2,其中S1中所有元素都小於x,S2中的所有元素都大於x。首先執行Find(x,S),將元素x調整為伸展樹的根節點,則x的左子樹就是S1,而右子樹為S2。
- 其他操作
除了上面介紹的五種基本操作,伸展樹還支援求最大值、求最小值、求前驅、求後繼等多種操作,這些基本操作也都是建立在伸展操作的基礎上的。
通常來說,每進行一種操作後都會進行一次Splay操作,這樣可以保證每次操作的平攤時間複雜度是O(logn)。
優勢
可靠的效能——它的平均效率不輸於其他平衡樹。
儲存所需的記憶體少——伸展樹無需記錄額外的什麼值來維護樹的資訊,相對於其他平衡樹,記憶體佔用要小。
由於Splay Tree僅僅是不斷調整,並沒有引入額外的標記,因而樹結構與標準紅黑樹沒有任何不同,從空間角度來看,它比Treap、SBT、AVL要高效得多。因為結構不變,因此只要是通過左旋和右旋進行的操作對Splay Tree性質都沒有絲毫影響,因而它也提供了BST中最豐富的功能,包括快速的拆分和合並,並且實現極為便捷。這一點是其它結構較難實現的。其時間效率也相當穩定,和Treap基本相當,常數較高。
缺點
伸展樹最顯著的缺點是它有可能會變成一條鏈。這種情況可能發生在以非降順序訪問n個元素之後。然而均攤的最壞情況是對數級的——O(logn)。
【來源:百度百科】
定義結構體
struct trnode
{
int d,n,c,f,son[2];
/*
d為值,f為父親的編號,
c為控制的節點個數,以他為根節點的那棵樹的所有的節點數
n為同值的節點個數,把多個同樣的值濃縮成一個結構體
(這一步是為了省空間,比如說3個為100的數,可以說這個d為100,n為3)
son[0]為左孩子,son[1]為右孩子
*/
}tr[110000]; int len;//len表示用到了第幾個節點
結構體的每一步定義都要搞清楚,這個關乎到了程式碼的所有意義。這裡有一個很重要的東西,就是結構體中的n,他把相同的數字都儲存在了一起,大大減少了記憶體,不佔用空間。
更新控制的節點數的函式
void update(int x)//更新編號為x的節點所控制的節點數
{
int lc=tr[x].son[0];//左孩子的編號
int rc=tr[x].son[1];//右孩子的編號
tr[x].c=tr[lc].c+tr[rc].c+tr[x].n;
//x總共的節點數=左孩子的節點數+右孩子的節點數+同值的數
}
更新節點數的這個函式在整個程式碼當中起到了一個很重要的作用,因為幾乎每一步的操作都離不開這個update函式,所以這個函式必須要記住,其實也很簡單的,就是 左孩子+右孩子+重複的 就是更新後的節點數
增加一個點的函式
void add(int d,int f)//新增值為d的點,認f為父親,同時,f也認他為孩子
{
len++;//增加一個節點數
tr[len].d=d; tr[len].n=1; tr[len].c=1;
/*
這一步是關於加入的值的
加入的這個值的值就是定義的d——值,
然後只有他自己一個,所以n=1
同時他控制的節點數也只有他自己一個
*/
tr[len].f=f; if(d<tr[f].d) tr[f].son[0]=len; else tr[f].son[1]=len;
/*
這一步是關於他認的父親的操作
這個節點的父親就是定義的f-父親
我們預設比父親節點的值小的為左孩子,比父親節點的值大的為右孩子
所以說如果加入的這個節點的值比父親節點的值大,就為左孩子,否則右孩子
狀態:左小右大
這裡可能會考慮到如果這個節點原本就有孩子怎麼辦?
這個的話我也解釋不清楚,
因為我們是增加進去的
所以我們只要找到合適的位置插入就好了
比如說
8
/ \
3 25
/ \ / \
2 4 20 30
假如我們要插入10的話,10離4最近,所以應該插入到4的下面,
但是這一整棵子樹的每一個節點都要比根節點小
所以這樣的話,這個10就只能認20為父親,也是最接近的答案了
*/
tr[len].son[0]=tr[len].son[1]=0;
/*
定義最開始的加入的這個點,是一個葉子節點
既沒有左孩子,也沒有右孩子
*/
}
這個函式主要實在插入的時候用的,用這個函式使得插入的時候少了一大串東西,而且也可以直接統計好更新之後的節點數。
rotate旋轉的函式(重要)
void rotate(int x,int w)
/*
這是整個程式碼當中的一個關鍵點
首先我們定義了x是我們要選擇旋轉的節點
w有兩個形式,一個是0,一個是1
0表示左轉,1表示右轉
(x,0)表示x這個點左轉
(x,1)表示x這個點右轉
注意,我們要轉的可能是x,但是變化的不止x,和x有關係的也有變動
*/
{
int y=tr[x].f; int z=tr[y].f;//x在旋轉之前,要確定x的父親y和爺爺z
//下來建立關係
int r,R;//r表示兒輩,R表示父輩 (ren,Ren)
//有四個角色:我x,我的兒子,我的父親,我的爺爺
/*
接下來的就是在旋轉的時候發生的關係
x為左孩子才可以右轉,為右孩子才可以左轉
在這裡可能要有圖才講得清楚
y x
/ \ / \
c x y b
/ \ / \
a b c a
左轉前 左轉後
y x
/ \ / \
x c a y
/ \ / \
a b b c
右轉前 右轉後
稍微解釋一下:
我們之前定義過左孩子的值比父親節點的值要小,
右孩子比父親節點的值要大,是吧?
那麼這個時候我們就可以看到,
x左轉之後一定跟y換了位置,這個是必然的,
然後,y是比x小的(x是y的右孩子),
所以x替代了y的位置之後,y就成為了x的左孩子(比x小),
然後c是y的右孩子,比y小,旋轉之後跟著y成為y的右孩子即可。
然後我們知道b是右孩子,比x要大,所以依舊成為x的右孩子即可,
那麼剩下a,首先我們知道a是比x小的,但是總體來看是比y要大的,
因為x比y大,所以a也比y大,然而x的左右孩子都有了,
y還有右孩子的空位,那麼a又比y大,所以a在y的右孩子的位置剛剛好。
*/
//更換過程是從下到上的,而且是兒子先認父親,父親再認兒子
r=tr[x].son[w]; R=y;//x的兒子->準備當新兒子
/*
左邊旋轉的話,x的左孩子就變成別人的孩子;
右邊旋轉的話,x的右孩子就變成別人的孩子。
然後這個孩子的新父親就是x的父親y
*/
tr[R].son[1-w]=r;
/*
左邊旋轉的話,x的左孩子就變成y的右孩子;
x的右孩子仍然是x的右孩子
右邊旋轉的話,x的右孩子就變成y的左孩子。
x的左孩子仍然是x的左孩子
*/
if(r!=0) tr[r].f=R;
//如果這個x的孩子節點不是0的話,這個孩子節點的父親就是前面認過的y節點
r=x; R=z;//x->準備當新兒子
if(tr[R].son[0]==y) tr[R].son[0]=r;
/*
首先我們知道,x左轉之後就變成了z的孩子節點,
因為y原來是z的孩子,現在x代替了y的位置
所以z就是x的父親節點
*/
//如果y所在的是z的左孩子,那麼x的位置就是z的左孩子
else tr[R].son[1]=r;
//否則就為z的右孩子,其實就是頂替的y的位置,其他不變
tr[r].f=R;
//x的父親節點變為z
r=y; R=x;//x的父親y->準備當新兒子
//y這個時候變成了孩子節點,他的父親節點是x
tr[R].son[w]=r;
/*
左轉的話,y就是x的左孩子
y的左孩子仍然是y的左孩子
右轉的話,y就是x的右孩子
y的右孩子仍然是y的右孩子
*/
tr[r].f=R;
//x就是y的父親節點
update(y);//先更新處於下層的點y,因為我們是先換下面的
update(x);//再更新上層的x,後換上面的
}
void splay(int x,int rt)
//該函式的功能是:經過旋轉之後,使x成為rt的孩子(左右都可以)
//最關鍵的操作
{
while(tr[x].f!=rt)//如果rt是x的父親,則什麼都不用做,否則x就要不斷向上旋轉
{
int f=tr[x].f; int ff=tr[f].f;//準備x的父親和爺爺
if(ff==rt)//如果x的爺爺是rt,那麼x只需要旋轉一次(相當於跳一層)
{
if(tr[f].son[0]==x) rotate(x,1); else rotate(x,0);
//如果x是f的左孩子的話,就右旋,也只能右旋
//如果x是f的右孩子的話,就左旋,也只能左旋
}
else//rt在ff的上面
{
if(tr[ff].son[0]==f && tr[f].son[0]==x) {rotate(f,1); rotate(x,1);}
/*
ff 第一次右轉 f 第二次右轉 x
/ f變成爺爺 / \ x變成爺爺 \
f x ff f
/ \
x ff
*/
else if(tr[ff].son[1]==f && tr[f].son[1]==x) {rotate(f,0); rotate(x,0);}
/*
ff 第一次左轉 f 第二次右轉 x
\ f變成爺爺 / \ x變成爺爺 /
f ff x f
\ /
x ff
*/
else if(tr[ff].son[0]==f && tr[f].son[1]==x) {rotate(x,0); rotate(x,1);}
/*
ff 第一次左轉 ff 第二次右轉 x
/ x變成父親 / x變成爺爺 / \
f x f ff
\ /
x f
這一次的旋轉比較特殊,如果f右轉的話就會出現這樣的情況
f
\
ff
/
x
轉了跟沒轉一樣,所以只能轉x,不能動y
*/
else if(tr[ff].son[1]==f && tr[f].son[0]==x) {rotate(x,1); rotate(x,0);}
/*
ff 第一次右轉 ff 第二次右轉 x
\ x變成父親 \ x變成爺爺 / \
f x ff f
/ \
x f
跟上面一樣也是隻能轉x,不能轉y
*/
}
}
if(rt==0) root=x;
/*
每一棵樹都要有一個最終極的根節點,如果x不能成為rt的孩子節點的話
說明x就是最終級的根節點
*/
}
這一步是非常重要的,因為伸展樹最大的作用就是把訪問過的移到根節點的位置,那麼這樣來說,就可以通過rotate這個函式來實現,所以程式碼要記住,建議不要死背,按照每一種方法的圖來背是最有效的。注意一下,調整的只是我們選中以x為中心的父親和爺爺,以及孩子,出來這四方,在x孩子的以下是不會受到改變的,他們的孩子節點跟著父親走就可以了。
rotate的幾種情況
也就是旋轉的情況,這個是挺重要的一個函式
當p為根節點時,進行zip step操作。
當x是p的左孩子時,對x右旋;
當x是p的右孩子時,對x左旋。
當p不是根節點,且x和p同為左孩子或右孩子時進行Zig-Zig操作。
當x和p同為左孩子時,依次將p和x右旋;
當x和p同為右孩子時,依次將p和x左旋。
當p不是根節點,且x和p不同為左孩子或右孩子時,進行Zig-Zag操作。
當p為左孩子,x為右孩子時,將x左旋後再右旋。
當p為右孩子,x為左孩子時,將x右旋後再左旋。
這裡只有一個圖,但是規律就是這樣的。
找某個值的編號的函式
int findip(int d)
//找值為d的節點的地址,補充:如果不存在d,就找到有可能是接近d的(或大或小)
{
int x=root;//root表示的是根節點,從根節點出發,x是我們找到的合適的值
//接下來就要判斷往左邊走還是往右邊走
while(tr[x].d!=d)//如果根節點的值等於要找的d的值,就不用找了
{
if(d<tr[x].d)//如果d小於根節點值
{
if(tr[x].son[0]==0) break;
/*
那就往左邊找,因為左孩子小於根節點
如果沒有左孩子,就退出,因為找不到合適的,只能去較大的右邊找
*/
else x=tr[x].son[0];
/*
如果有左孩子,那麼x就為根節點的左孩子,因為最開始的是最接近的
再往下的也不及根節點自己的孩子和根節點最近
*/
}
else//if(tr[x].d<d) //如果d大於根節點的值
{
if(tr[x].son[1]==0) break;
/*
那就往右邊找,因為右孩子大於根節點
如果沒有右孩子,就退出,因為找不到合適的,只能去較小的地方找
*/
else x=tr[x].son[1];//如果有右孩子,那麼x就為根節點的右孩子
}
}
return x;
/*
返回x的編號
伸展樹就是儲存了我們訪問過的所有資料,使得最快的找到我們要找的
而這一步其實就解決了我們題目中的第5步和第6步,找前驅和找後繼
*/
}
顯然這一步是為了後面的尋找的,這在尋找中會起到很重要的作用,知道一個值是不夠的,要知道這個值的編號才能進行整一棵伸展樹的調整。
插入的函式
void ins(int d)
//插入數值為d的一個節點
//這可以說是伸展樹的一大亮點,插入和刪除,是之前所有樹形結構所做不到的
{
if(root==0) {add(d,0); root=len; return ;}
/*
root為0,說明沒有根節點,表示這是一棵空樹
既然沒有,那就增加一個點,父親為0,也就是當前的root
root不能等於1,因為len是全域性變數
但是如果原來有一棵樹但是被全部刪掉之後,len是沒有清除資料的
所以這個時候我們就要接著len往下建樹
*/
int x=findip(d);//先看看能不能找到d
if(tr[x].d==d)//如果在這棵樹中找到了d,那就很簡單了
//比如說,要找7,但是編號為3的節點的值就為7,
//那就直接增加編號3的n(相同值的個數)就可以了
{
tr[x].n++;//直接把x相同的再增加一個,就算插入了
update(x);//更新x控制的人數,就是增加一個人
splay(x,0);
/*
把x提高到根節點
因為增加了一個,但是這個資料要彙報給根節點
所以就是要讓x為根節點
提高的過程中不斷旋轉,不斷更新孩子與父親的關係
所以我們找到的這個7的節點在跳的過程中
會不斷告訴別人7控制了多少個節點
這樣就不會混亂,也不會影響後面
*/
}
else//如果找不到
{
add(d,x);//增加一個值為d的點
update(x);//更新x
splay(len,0);
/*
新的這個點要拉上去,作為根節點
成為根節點就是伸展樹最神奇的地方
因為伸展樹把訪問過的點都提拔到了根節點
因為他覺得之後還會訪問,而且也確實如此,所以才能夠更快的實現尋找
*/
}
}
插入要判斷,判斷是否要真正意義上的插入,還是隻是增加一個相同的值。每一次的插入,都要更新節點數,所以update是一個眾觀全域性的函式。
刪除的函式
void del(int d)//刪除數值為d的一個節點
{
int x=findip(d); splay(x,0);
/*
找人,並且讓找到的這個人旋轉到根節點
這就是我們伸展樹的優點,把訪問過的旋轉到根節點
*/
if(tr[x].n>1) {tr[x].n--; update(x); return ;}
//如果重複度大於一,減少一個然後再更新一下就好了
if(tr[x].son[0]==0 && tr[x].son[1]==0) {root=0; len=0;}
/*
我們已經把這個點提到了根節點的話
而如果我們要刪的這個點既沒有左孩子也沒有右孩子
那就說明全世界只有他一個點,那刪掉之後就什麼都為0
根節點為0,節點數也為0
*/
else if(tr[x].son[0]==0 && tr[x].son[1]!=0) {root=tr[x].son[1]; tr[root].f=0;}
/*
如果這個點沒有左孩子但是有右孩子的話
右孩子成為根節點,並且這個右孩子沒有父親節點
*/
else if(tr[x].son[0]!=0 && tr[x].son[1]==0) {root=tr[x].son[0]; tr[root].f=0;}
/*
如果這個點沒有右孩子但是有左孩子的話
左孩子成為根節點,並且這個左孩子沒有父親節點
*/
else//if(tr[x].son[0]!= 0 && tr[x].son[1]!=0) //既有左孩子,也有右孩子
{
int p=tr[x].son[0];//定義p為x的左孩子
while(tr[p].son[1]!=0)//如果p有右孩子的話
{
p=tr[p].son[1];//那麼p就更新為自己的右孩子
splay(p,x);//把右孩子旋轉到x的孩子節點,也就是轉到p的位置
/*
一直往右邊跳,因為右邊是比根節點的值要大的,所以往右邊
*/
}//迴圈到沒有有孩子的時候,這個值就是最大的
//又因為沒有這個p點沒有了右孩子,所以就可以收x的右孩子成為自己的右孩子
int r=tr[x].son[1];//小人為x節點的右孩子
int R=p;//大人為p,也就是x節點的右孩子成為p節點的右孩子
/*
4 經過第一次 4
/ \ 轉動了3 /
2 6 而且4的右孩子也 3
/ \ / \ 成為了3的右孩子 / \
1 3 5 7 2 6
/ / \
1 5 7
這個時候就成為了我們要的,只有一個孩子節點
*/
tr[R].son[1]=r;
tr[r].f=R;//定下結論,我們現在只有一個子樹了
root=R; tr[root].f=0;
/*
這個時候新的root就等於我們找到的最大的值
目的就是把每一個訪問過的都記錄下來
*/
update(R);//更新這一整棵樹就好了
}
}
刪除在意義上和插入有幾分相似,大概也是判斷直接刪除重複的值還是刪除單個的值,但是這裡要比插入複雜一點,因為我們刪掉的那個值之後,可能會導致整棵伸展樹的倒塌,所以在背程式碼的時候要考慮清楚這些細節的東西。
找排名的函式
int findpaiming(int d)//找排名
{
int x=findip(d); splay(x,0);
//先找到這個值,然後讓他成為根節點
return tr[tr[x].son[0]].c+1;
//左孩子的控制人數再+1就是自己的排名
/*
100 第一次 100 第二次 23
/ \ 移動23 / \ 旋轉23 \
55 120 23 120 100
/ \ \ \ / \
23 144 55 144 55 144
\ / /
34 34 34
/ / /
30 30 30
比如說我們要找23的排名
排名為1
這樣就對了因為我們要找的是從小到大的排名
所以23最小就為1
*/
}
找排名是極其簡單的一個函式,排名是值從小到大排序,只要搞清楚為什麼是左孩子控制的人數+1就是自己的排名就可以了。
找某個排名對應的值的函式
int findzhi(int k)//找排名為k的值
{
int x=root;//定義x為根節點,從根節點開始找
while(1)//
{
int lc=tr[x].son[0]; int rc=tr[x].son[1];//左邊和右邊
if(k<=tr[lc].c) x=lc;
/*
如果k的這個排名比左邊控制的人數還要少
就去左邊找
這個時候就把左邊設定為要繼續往下找的一個終點位置
其實就是伸展樹的好處,記錄訪問過的
*/
else if(k>tr[lc].c+tr[x].n)
/*
如果這個排名比(左邊控制的人數+根節點重複的節點數)都要大
就去右邊找
*/
{
k-=tr[lc].c+tr[x].n;
/*
注意:光繼續在右邊找還不夠
要減去(左邊的控制人數+根節點重複的節點數)
比如說:我們要找17
3
/ \
10 ?
這個時候右邊控制的人數+根節點重複的節點數=13
比17要小,說明我們要去右邊找
去右邊找的就是 17-13=4,找排名為4的節點的值
*/
x=rc;//去右邊繼續找
}
else break;//否則要找的排名就在根節點中間
}
splay(x,0);//把找到的合適的移到根節點
return tr[x].d;//把我們找到的這個節點的值返回給函式findzhi
}
這個要稍微複雜一點,因為你要判斷當前這個排名是在左孩子還是右孩子,其他的就很簡單了。
找前驅的函式
int findqianqu(int d)//找前驅
{
int x=findip(d); splay(x,0);//找到d的編號,使他成為根節點
if(d<=tr[x].d && tr[x].son[0]!=0)
//如果是if( d<tr[x].d && tr[x].son[0]!=0 )則找到的是:小於等於d的前驅
//如果這個值比根節點的值要小,並且有左孩子的話
{
x=tr[x].son[0];//把這個點的左孩子移到根節點
while(tr[x].son[1]!=0) x=tr[x].son[1];
/*
找完之後一直往右邊跳(也就是尋找),找右邊的最大值
前驅是比要找的值小的最大值
所以只要是左孩子的話就一定比d要小
那麼左孩子的右孩子就是比d小而且是比左孩子要大的
這樣就可以找到最大的值
*/
}
if(tr[x].d>=d) x=0;//如果是if(tr[x].d>d)則找到的是:小於等於d的前驅
/*
如果我們找到的這個值大於等於d的話
說明以d為根節點的這棵數沒有左孩子
那就說明沒有合適的前驅
就只能為0
*/
return x;//返回x的值
}
找前驅,一點都不難,唯一要注意的就是要搞清楚一個節點的左孩子的值比自己要小,右孩子的值比自己要大,還有一個就是要判斷沒有前驅的情況。大概就是這三種。
找後繼的函式
int finddouji(int d)//找後繼
{
int x=findip(d); splay(x,0);//找到d的編號,使他成為根節點
if(tr[x].d<=d && tr[x].son[1]!=0)
//如果這個值比根節點的值要大,並且有右孩子的話
{
x=tr[x].son[1];//把這個點的右孩子移到根節點
while(tr[x].son[0]!=0) x=tr[x].son[0];
/*
找完之後一直往左邊跳(也就是尋找),找左邊的最小值
後繼是比要找的值大的最小值
所以只要是右孩子的話就一定比d要大
那麼右孩子的左孩子就是比d大而且是比右孩子要小的
這樣就可以找到最小的值
*/
}
if(tr[x].d<=d) x=0;
/*
如果我們找到的這個值小於等於d的話
說明以d為根節點的這棵數沒有右孩子
那就說明沒有合適的後繼
就只能為0
*/
return x;//返回x的值
}
找後繼,跟找前驅一樣的道理。
智障的主函式
int main()
{
int n; n=read();
root=0; len=0;//初始化沒有根節點,也沒有節點
for(int i=1;i<=n;i++)
{
int cz,x; cz=read(); x=read();
if(cz==1) ins(x);//插入
else if(cz==2) del(x);//刪除
else if(cz==3) printf("%d\n",findpaiming(x));//找排名
else if(cz==4) printf("%d\n",findzhi(x));//找排名值
else if(cz==5) printf("%d\n",tr[findqianqu(x)].d);//找前驅
else if(cz==6) printf("%d\n",tr[finddouji(x)].d);//找後繼
}
return 0;
}
不解釋
最後,我的思路相對來講會沒有那麼完善,但是詳解都在程式碼裡面了,把函式的作用搞清楚就可以了。
完整程式碼
/*
要求:畫圖理解並且默打
記住:是理解性默打(不然背死你)
*/
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
inline int read()
{
char c=getchar();
int x=0,f=1;
while(c<48 || c>57)
{
if(c=='-') f=-1;
c=getchar();
}
while(c>=48 && c<=57)
{
x=x*10+c-48;
c=getchar();
}
return x*f;
}
int root;//儲存根節點
struct trnode
{
int d,n,c,f,son[2];
/*
d為值,f為父親的編號,
c為控制的節點個數,以他為根節點的那棵樹的所有的節點數
n為同值的節點個數,把多個同樣的值濃縮成一個結構體
(這一步是為了省空間,比如說3個為100的數,可以說這個d為100,n為3)
son[0]為左孩子,son[1]為右孩子
*/
}tr[110000]; int len;//len表示用到了第幾個節點
void update(int x)//更新編號為x的節點所控制的節點數
{
int lc=tr[x].son[0];//左孩子的編號
int rc=tr[x].son[1];//右孩子的編號
tr[x].c=tr[lc].c+tr[rc].c+tr[x].n;
//x總共的節點數=左孩子的節點數+右孩子的節點數+同值的數
}
void add(int d,int f)//新增值為d的點,認f為父親,同時,f也認他為孩子
{
len++;//增加一個節點數
tr[len].d=d; tr[len].n=1; tr[len].c=1;
/*
這一步是關於加入的值的
加入的這個值的值就是定義的d——值,
然後只有他自己一個,所以n=1
同時他控制的節點數也只有他自己一個
*/
tr[len].f=f; if(d<tr[f].d) tr[f].son[0]=len; else tr[f].son[1]=len;
/*
這一步是關於他認的父親的操作
這個節點的父親就是定義的f-父親
我們預設比父親節點的值小的為左孩子,比父親節點的值大的為右孩子
所以說如果加入的這個節點的值比父親節點的值大,就為左孩子,否則右孩子
狀態:左小右大
這裡可能會考慮到如果這個節點原本就有孩子怎麼辦?
這個的話我也解釋不清楚,
因為我們是增加進去的
所以我們只要找到合適的位置插入就好了
比如說
8
/ \
3 25
/ \ / \
2 4 20 30
假如我們要插入10的話,10離4最近,所以應該插入到4的下面,
但是這一整棵子樹的每一個節點都要比根節點小
所以這樣的話,這個10就只能認20為父親,也是最接近的答案了
*/
tr[len].son[0]=tr[len].son[1]=0;
/*
定義最開始的加入的這個點,是一個葉子節點
既沒有左孩子,也沒有右孩子
*/
}
void rotate(int x,int w)
/*
這是整個程式碼當中的一個關鍵點
首先我們定義了x是我們要選擇旋轉的節點
w有兩個形式,一個是0,一個是1
0表示左轉,1表示右轉
(x,0)表示x這個點左轉
(x,1)表示x這個點右轉
注意,我們要轉的可能是x,但是變化的不止x,和x有關係的也有變動
*/
{
int y=tr[x].f; int z=tr[y].f;//x在旋轉之前,要確定x的父親y和爺爺z
//下來建立關係
int r,R;//r表示兒輩,R表示父輩 (ren,Ren)
//有四個角色:我x,我的兒子,我的父親,我的爺爺
/*
接下來的就是在旋轉的時候發生的關係
x為左孩子才可以右轉,為右孩子才可以左轉
在這裡可能要有圖才講得清楚
y x
/ \ / \
c x y b
/ \ / \
a b c a
左轉前 左轉後
y x
/ \ / \
x c a y
/ \ / \
a b b c
右轉前 右轉後
稍微解釋一下:
我們之前定義過左孩子的值比父親節點的值要小,
右孩子比父親節點的值要大,是吧?
那麼這個時候我們就可以看到,
x左轉之後一定跟y換了位置,這個是必然的,
然後,y是比x小的(x是y的右孩子),
所以x替代了y的位置之後,y就成為了x的左孩子(比x小),
然後c是y的右孩子,比y小,旋轉之後跟著y成為y的右孩子即可。
然後我們知道b是右孩子,比x要大,所以依舊成為x的右孩子即可,
那麼剩下a,首先我們知道a是比x小的,但是總體來看是比y要大的,
因為x比y大,所以a也比y大,然而x的左右孩子都有了,
y還有右孩子的空位,那麼a又比y大,所以a在y的右孩子的位置剛剛好。
*/
//更換過程是從下到上的,而且是兒子先認父親,父親再認兒子
r=tr[x].son[w]; R=y;//x的兒子->準備當新兒子
/*
左邊旋轉的話,x的左孩子就變成別人的孩子;
右邊旋轉的話,x的右孩子就變成別人的孩子。
然後這個孩子的新父親就是x的父親y
*/
tr[R].son[1-w]=r;
/*
左邊旋轉的話,x的左孩子就變成y的右孩子;
x的右孩子仍然是x的右孩子
右邊旋轉的話,x的右孩子就變成y的左孩子。
x的左孩子仍然是x的左孩子
*/
if(r!=0) tr[r].f=R;
//如果這個x的孩子節點不是0的話,這個孩子節點的父親就是前面認過的y節點
r=x; R=z;//x->準備當新兒子
if(tr[R].son[0]==y) tr[R].son[0]=r;
/*
首先我們知道,x左轉之後就變成了z的孩子節點,
因為y原來是z的孩子,現在x代替了y的位置
所以z就是x的父親節點
*/
//如果y所在的是z的左孩子,那麼x的位置就是z的左孩子
else tr[R].son[1]=r;
//否則就為z的右孩子,其實就是頂替的y的位置,其他不變
tr[r].f=R;
//x的父親節點變為z
r=y; R=x;//x的父親y->準備當新兒子
//y這個時候變成了孩子節點,他的父親節點是x
tr[R].son[w]=r;
/*
左轉的話,y就是x的左孩子
y的左孩子仍然是y的左孩子
右轉的話,y就是x的右孩子
y的右孩子仍然是y的右孩子
*/
tr[r].f=R;
//x就是y的父親節點
update(y);//先更新處於下層的點y,因為我們是先換下面的
update(x);//再更新上層的x,後換上面的
}
void splay(int x,int rt)
//該函式的功能是:經過旋轉之後,使x成為rt的孩子(左右都可以)
//最關鍵的操作
{
while(tr[x].f!=rt)//如果rt是x的父親,則什麼都不用做,否則x就要不斷向上旋轉
{
int f=tr[x].f; int ff=tr[f].f;//準備x的父親和爺爺
if(ff==rt)//如果x的爺爺是rt,那麼x只需要旋轉一次(相當於跳一層)
{
if(tr[f].son[0]==x) rotate(x,1); else rotate(x,0);
//如果x是f的左孩子的話,就右旋,也只能右旋
//如果x是f的右孩子的話,就左旋,也只能左旋
}
else//rt在ff的上面
{
if(tr[ff].son[0]==f && tr[f].son[0]==x) {rotate(f,1); rotate(x,1);}
/*
ff 第一次右轉 f 第二次右轉 x
/ f變成爺爺 / \ x變成爺爺 \
f x ff f
/ \
x ff
*/
else if(tr[ff].son[1]==f && tr[f].son[1]==x) {rotate(f,0); rotate(x,0);}
/*
ff 第一次左轉 f 第二次右轉 x
\ f變成爺爺 / \ x變成爺爺 /
f ff x f
\ /
x ff
*/
else if(tr[ff].son[0]==f && tr[f].son[1]==x) {rotate(x,0); rotate(x,1);}
/*
ff 第一次左轉 ff 第二次右轉 x
/ x變成父親 / x變成爺爺 / \
f x f ff
\ /
x f
這一次的旋轉比較特殊,如果f右轉的話就會出現這樣的情況
f
\
ff
/
x
轉了跟沒轉一樣,所以只能轉x,不能動y
*/
else if(tr[ff].son[1]==f && tr[f].son[0]==x) {rotate(x,1); rotate(x,0);}
/*
ff 第一次右轉 ff 第二次右轉 x
\ x變成父親 \ x變成爺爺 / \
f x ff f
/ \
x f
跟上面一樣也是隻能轉x,不能轉y
*/
}
}
if(rt==0) root=x;
/*
每一棵樹都要有一個最終極的根節點,如果x不能成為rt的孩子節點的話
說明x就是最終級的根節點
*/
}
int findip(int d)
//找值為d的節點的地址,補充:如果不存在d,就找到有可能是接近d的(或大或小)
{
int x=root;//root表示的是根節點,從根節點出發,x是我們找到的合適的值
//接下來就要判斷往左邊走還是往右邊走
while(tr[x].d!=d)//如果根節點的值等於要找的d的值,就不用找了
{
if(d<tr[x].d)//如果d小於根節點值
{
if(tr[x].son[0]==0) break;
/*
那就往左邊找,因為左孩子小於根節點
如果沒有左孩子,就退出,因為找不到合適的,只能去較大的右邊找
*/
else x=tr[x].son[0];
/*
如果有左孩子,那麼x就為根節點的左孩子,因為最開始的是最接近的
再往下的也不及根節點自己的孩子和根節點最近
*/
}
else//if(tr[x].d<d) //如果d大於根節點的值
{
if(tr[x].son[1]==0) break;
/*
那就往右邊找,因為右孩子大於根節點
如果沒有右孩子,就退出,因為找不到合適的,只能去較小的地方找
*/
else x=tr[x].son[1];//如果有右孩子,那麼x就為根節點的右孩子
}
}
return x;
/*
返回x的編號
伸展樹就是儲存了我們訪問過的所有資料,使得最快的找到我們要找的
而這一步其實就解決了我們題目中的第5步和第6步,找前驅和找後繼
*/
}
void ins(int d)
//插入數值為d的一個節點
//這可以說是伸展樹的一大亮點,插入和刪除,是之前所有樹形結構所做不到的
{
if(root==0) {add(d,0); root=len; return ;}
/*
root為0,說明沒有根節點,表示這是一棵空樹
既然沒有,那就增加一個點,父親為0,也就是當前的root
root不能等於1,因為len是全域性變數
但是如果原來有一棵樹但是被全部刪掉之後,len是沒有清除資料的
所以這個時候我們就要接著len往下建樹
*/
int x=findip(d);//先看看能不能找到d
if(tr[x].d==d)//如果在這棵樹中找到了d,那就很簡單了
//比如說,要找7,但是編號為3的節點的值就為7,
//那就直接增加編號3的n(相同值的個數)就可以了
{
tr[x].n++;//直接把x相同的再增加一個,就算插入了
update(x);//更新x控制的人數,就是增加一個人
splay(x,0);
/*
把x提高到根節點
因為增加了一個,但是這個資料要彙報給根節點
所以就是要讓x為根節點
提高的過程中不斷旋轉,不斷更新孩子與父親的關係
所以我們找到的這個7的節點在跳的過程中
會不斷告訴別人7控制了多少個節點
這樣就不會混亂,也不會影響後面
*/
}
else//如果找不到
{
add(d,x);//增加一個值為d的點
update(x);//更新x
splay(len,0);
/*
新的這個點要拉上去,作為根節點
成為根節點就是伸展樹最神奇的地方
因為伸展樹把訪問過的點都提拔到了根節點
因為他覺得之後還會訪問,而且也確實如此,所以才能夠更快的實現尋找
*/
}
}
void del(int d)//刪除數值為d的一個節點
{
int x=findip(d); splay(x,0);
/*
找人,並且讓找到的這個人旋轉到根節點
這就是我們伸展樹的優點,把訪問過的旋轉到根節點
*/
if(tr[x].n>1) {tr[x].n--; update(x); return ;}
//如果重複度大於一,減少一個然後再更新一下就好了
if(tr[x].son[0]==0 && tr[x].son[1]==0) {root=0; len=0;}
/*
我們已經把這個點提到了根節點的話
而如果我們要刪的這個點既沒有左孩子也沒有右孩子
那就說明全世界只有他一個點,那刪掉之後就什麼都為0
根節點為0,節點數也為0
*/
else if(tr[x].son[0]==0 && tr[x].son[1]!=0) {root=tr[x].son[1]; tr[root].f=0;}
/*
如果這個點沒有左孩子但是有右孩子的話
右孩子成為根節點,並且這個右孩子沒有父親節點
*/
else if(tr[x].son[0]!=0 && tr[x].son[1]==0) {root=tr[x].son[0]; tr[root].f=0;}
/*
如果這個點沒有右孩子但是有左孩子的話
左孩子成為根節點,並且這個左孩子沒有父親節點
*/
else//if(tr[x].son[0]!= 0 && tr[x].son[1]!=0) //既有左孩子,也有右孩子
{
int p=tr[x].son[0];//定義p為x的左孩子
while(tr[p].son[1]!=0)//如果p有右孩子的話
{
p=tr[p].son[1];//那麼p就更新為自己的右孩子
splay(p,x);//把右孩子旋轉到x的孩子節點,也就是轉到p的位置
/*
一直往右邊跳,因為右邊是比根節點的值要大的,所以往右邊
*/
}//迴圈到沒有有孩子的時候,這個值就是最大的
//又因為沒有這個p點沒有了右孩子,所以就可以收x的右孩子成為自己的右孩子
int r=tr[x].son[1];//小人為x節點的右孩子
int R=p;//大人為p,也就是x節點的右孩子成為p節點的右孩子
/*
4 經過第一次 4
/ \ 轉動了3 /
2 6 而且4的右孩子也 3
/ \ / \ 成為了3的右孩子 / \
1 3 5 7 2 6
/ / \
1 5 7
這個時候就成為了我們要的,只有一個孩子節點
*/
tr[R].son[1]=r;
tr[r].f=R;//定下結論,我們現在只有一個子樹了
root=R; tr[root].f=0;
/*
這個時候新的root就等於我們找到的最大的值
目的就是把每一個訪問過的都記錄下來
*/
update(R);//更新這一整棵樹就好了
}
}
int findpaiming(int d)//找排名
{
int x=findip(d); splay(x,0);
//先找到這個值,然後讓他成為根節點
return tr[tr[x].son[0]].c+1;
//左孩子的控制人數再+1就是自己的排名
/*
100 第一次 100 第二次 23
/ \ 移動23 / \ 旋轉23 \
55 120 23 120 100
/ \ \ \ / \
23 144 55 144 55 144
\ / /
34 34 34
/ / /
30 30 30
比如說我們要找23的排名
排名為1
這樣就對了因為我們要找的是從小到大的排名
所以23最小就為1
*/
}
int findzhi(int k)//找排名為k的值
{
int x=root;//定義x為根節點,從根節點開始找
while(1)//
{
int lc=tr[x].son[0]; int rc=tr[x].son[1];//左邊和右邊
if(k<=tr[lc].c) x=lc;
/*
如果k的這個排名比左邊控制的人數還要少
就去左邊找
這個時候就把左邊設定為要繼續往下找的一個終點位置
其實就是伸展樹的好處,記錄訪問過的
*/
else if(k>tr[lc].c+tr[x].n)
/*
如果這個排名比(左邊控制的人數+根節點重複的節點數)都要大
就去右邊找
*/
{
k-=tr[lc].c+tr[x].n;
/*
注意:光繼續在右邊找還不夠
要減去(左邊的控制人數+根節點重複的節點數)
比如說:我們要找17
3
/ \
10 ?
這個時候右邊控制的人數+根節點重複的節點數=13
比17要小,說明我們要去右邊找
去右邊找的就是 17-13=4,找排名為4的節點的值
*/
x=rc;//去右邊繼續找
}
else break;//否則要找的排名就在根節點中間
}
splay(x,0);//把找到的合適的移到根節點
return tr[x].d;//把我們找到的這個節點的值返回給函式findzhi
}
int findqianqu(int d)//找前驅
{
int x=findip(d); splay(x,0);//找到d的編號,使他成為根節點
if(d<=tr[x].d && tr[x].son[0]!=0)
//如果是if( d<tr[x].d && tr[x].son[0]!=0 )則找到的是:小於等於d的前驅
//如果這個值比根節點的值要小,並且有左孩子的話
{
x=tr[x].son[0];//把這個點的左孩子移到根節點
while(tr[x].son[1]!=0) x=tr[x].son[1];
/*
找完之後一直往右邊跳(也就是尋找),找右邊的最大值
前驅是比要找的值小的最大值
所以只要是左孩子的話就一定比d要小
那麼左孩子的右孩子就是比d小而且是比左孩子要大的
這樣就可以找到最大的值
*/
}
if(tr[x].d>=d) x=0;//如果是if(tr[x].d>d)則找到的是:小於等於d的前驅
/*
如果我們找到的這個值大於等於d的話
說明以d為根節點的這棵數沒有左孩子
那就說明沒有合適的前驅
就只能為0
*/
return x;//返回x的值
}
int finddouji(int d)//找後繼
{
int x=findip(d); splay(x,0);//找到d的編號,使他成為根節點
if(tr[x].d<=d && tr[x].son[1]!=0)
//如果這個值比根節點的值要大,並且有右孩子的話
{
x=tr[x].son[1];//把這個點的右孩子移到根節點
while(tr[x].son[0]!=0) x=tr[x].son[0];
/*
找完之後一直往左邊跳(也就是尋找),找左邊的最小值
後繼是比要找的值大的最小值
所以只要是右孩子的話就一定比d要大
那麼右孩子的左孩子就是比d大而且是比右孩子要小的
這樣就可以找到最小的值
*/
}
if(tr[x].d<=d) x=0;
/*
如果我們找到的這個值小於等於d的話
說明以d為根節點的這棵數沒有右孩子
那就說明沒有合適的後繼
就只能為0
*/
return x;//返回x的值
}
int main()
{
int n; n=read();
root=0; len=0;//初始化沒有根節點,也沒有節點
for(int i=1;i<=n;i++)
{
int cz,x; cz=read(); x=read();
if(cz==1) ins(x);//插入
else if(cz==2) del(x);//刪除
else if(cz==3) printf("%d\n",findpaiming(x));//找排名
else if(cz==4) printf("%d\n",findzhi(x));//找排名值
else if(cz==5) printf("%d\n",tr[findqianqu(x)].d);//找前驅
else if(cz==6) printf("%d\n",tr[finddouji(x)].d);//找後繼
}
return 0;
}