1. 程式人生 > >平衡樹解析

平衡樹解析

就會 二叉樹 insert 小技巧 二分 畫圖 相對 沒有 元素

轉自:yyb巨佬的平衡樹

不知道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是父節點

oid 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);//把當前位置移到根,保證結構的平衡
}

繼續,,,
前驅/後繼操作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(rg int x)//找第x小的數
{
    rg int now=root;//從根開始找
    if(ljl[now].size<x)return 0;//如果排名都超過總數了…………
    while(1)//嘿嘿,一直找
    {
        rg int ls=ljl[now].ch[0];//左孩子
        if(ljl[ls].size+ljl[now].cnt<x)//如果排名比左孩子總元素數和與我相等的數總和還大
        {
            x-=ljl[ls].size+ljl[now].cnt;//就減去前面的元素數
            now=ljl[now].ch[1];//去右孩子上找這個排名
        }
        else
            if(ljl[ls].size>=x)now=ls;//如果左孩子裏包括它
            else return ljl[now].v;//那就在這個點上了,返回
    }
}

最後鏈接一下一些題目及題解

luoguP3369[模板]普通平衡樹(Treap/SBT) 題解

luoguP3391[模板]文藝平衡樹(Splay) 題解

平衡樹解析