1. 程式人生 > >caioj1130:伸展樹(模版)

caioj1130:伸展樹(模版)

目錄

題目描述

伸展樹的基本概念

定義結構體

更新控制的節點數的函式

增加一個點的函式

rotate旋轉的函式(重要)

 找某個值的編號的函式

插入的函式

刪除的函式

找排名的函式

 找某個排名對應的值的函式

找前驅的函式

找後繼的函式

智障的主函式

完整程式碼


題目描述

【題意描述】
寫一種資料結構,來維護一些數,其中需要提供以下操作: 
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僅僅是不斷調整,並沒有引入額外的標記,因而樹結構與標準紅黑樹沒有任何不同,從空間角度來看,它比TreapSBT、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的幾種情況

也就是旋轉的情況,這個是挺重要的一個函式

Zig Step

 

當p為根節點時,進行zip step操作。

當x是p的左孩子時,對x右旋;

當x是p的右孩子時,對x左旋。

 

Zig-Zig Step

當p不是根節點,且x和p同為左孩子或右孩子時進行Zig-Zig操作。

當x和p同為左孩子時,依次將p和x右旋;

當x和p同為右孩子時,依次將p和x左旋。

 

Zig-Zag Step

當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;
}