1. 程式人生 > >紅黑樹 從入門到精通

紅黑樹 從入門到精通

紅黑樹

簡介

紅黑樹是一種非常優秀的二叉搜尋樹,誕生於1972年,並被優化於1978年,因其效率高而被廣泛應用於STL等需要高效率的實現。

基本性質

紅黑樹是一種基於旋轉的平衡樹,雖然沒有AVL的完全平衡,但相比於Splay,Treap又有嚴格的效率保證。
一顆紅黑樹應當滿足以下五個性質:
性質1.節點是紅色或黑色。
性質2.根節點是黑色。
性質3.每個葉節點是黑色的。
性質4.每個紅色節點的兩個子節點都是黑色。
性質5.從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。
注意上述的性質3,原樹上的任意節點,若其沒有左兒子或右兒子,我們給它加上一個虛節點NULL,這使得新樹中所有葉子節點為NULL,當然,這一步只是幫助思考,在程式碼實現中不會體現。
對於性質4,其等同於樹上沒有兩個相鄰的紅色節點。
另外,因為紅黑樹滿足性質5,這使得最深的葉子節點的深度不會超過最淺的葉子節點深度的兩倍,從根節點出發,沿途訪問到某個葉子節點,這條路徑的長度不會超過2

logn(n為節點數目)。這保證了紅黑樹任意操作的複雜度都是O(logn)

基本操作

查詢

查詢樹上的一個點,通常要從根節點開始往下走,時間複雜度就是樹的深度。

旋轉

紅黑樹的旋轉與Splay完全一致,這裡不作贅述。

插入

在樹上插入一個節點,先找到該點應插入的位置,這個位置應當是原先的某個葉子節點。插入時,為了不破壞性質5,我們將這個新加的節點標為紅色,但這有可能會破壞性質4。如果破壞了性質4,我們就要進行修復。

設N為當前待修復節點(初始為新加節點),P為N的父節點,S為P的另一個子節點,也就是N的兄弟節點,G為P的父節點,U為P的兄弟節點,也就是N的叔節點。

void Insert_Fixup(int N)
{
    int P,G,S,U;
    P=fa[N];
    G=fa[P];
    if(N==left[P]) S=right[P];
    else S=left[P];
    if(P==left[G]) U=right[G];
    else U=left[G];

情況1:當前節點是根節點,根據性質2,直接將該點染成黑色,不需要修復,退出。

if(N==root)
{
    color[N]=black;
    return;
}

情況2:P節點是黑色的,性質4沒有被破壞,修復完成,退出。

if(color
[P]==black) { return; }

若不滿足上述兩種情況,意味著性質4被破壞,但一定滿足:N和P是紅色的且P不是根節點,S和G是黑色的。

情況3:N是P的左兒子且P是G的右兒子或N是P的右兒子且P是G的左兒子,為了後續處理方便,將N進行一次旋轉操作,把P作為當前節點,更新兄弟節點S。

if((N==left[P])+(P==left[G])==1)
{
    Rotate(N);
    swap(N,P);
    if(N==left[P]) S=right[P];
    else S=left[P];
}

接下來,我們要討論叔節點U的顏色。

情況4:U節點是黑色的,我們要旋轉P,將P染成黑色,將G染成紅色,修復完成,退出。

if(color[U]==black)
{
    Rotate(P);
    color[P]=black;
    color[G]=red;
    return;
}

情況5:若U節點是紅色的,因為P和U同層又同為紅色,我們將紅色向上傳,即將P和U染成黑色,將G染成紅色,這樣原本N和P是相鄰的紅色節點,被破壞的性質4得到了修復,但G原本是黑色,現在變成了紅色,若G的父節點也是紅色,性質4被再次破壞,所以我們將G作為新的當前節點並進行遞迴操作,重新進入修復函式。

else
{
    color[P]=color[U]=black;
    color[G]=red;
    Insert_Fixup(G);
    return;
}

以上就是插入修復的所有情況,除了情況5需要遞迴,理論最多遞迴到根,也就是最對進行2logn次操作,但均攤只用2次。

刪除

從樹中刪除一個點,第一個步驟仿照Splay,那就是如果刪除點沒有兒子是空節點,我們要先找到一個能替代它的,也就是前驅後繼。我們將找到的前去後繼的資訊賦給待刪除點,然後待刪除點變成了這個前驅後繼。將待刪除節點刪去,再把下面的點接上來。
因為刪了點,所以性質2,性質3,性質5都可能被破壞。

情況1:被刪除點是紅色的,沒有性質被破壞,不做任何事情。

if(color[V]==red)
{
    return;
}

情況2:刪除後接上的點是紅色,性質3可能被破壞,性質5被破壞,把這個點染成黑色,修復完成。

if(color[N]==red)
{
    color[N]=black;
    return;
}

情況3:不滿足上述兩種情況,性質2,性質3未被破壞,性質5一定被破壞,且要呼叫修復函式修復。

else
{
    Delete_Fixup(N);
    return;
}

設N為當前待修復節點,P是N的父節點,S是P的兄弟節點,SL,SR是S的兩個兒子。

void Delete_Fixup(int N)
{
    int P,S,SL,SR;
    P=fa[N];
    if(N==left[P]) S=right[P];
    else S=left[P];
    SL=left[S];
    SR=right[S];

情況4:當前點是根節點,直接退出。

if(N==root)
{
    return;
}

情況5:P,S,SL,SR的顏色全為黑色,將S染成紅色,將P設為當前節點,進行遞迴操作,重新進入修復函式。

if(color[P]==black&&color[S]==black&&color[SL]==black&&color[SR]==black)
{
    color[S]=red;
    Delete_Fixup(F);
    return;
}

情況6:SL,SR都是黑色,P是紅色,將P染成黑色,將S染成紅色,修復完成,退出。
這裡寫圖片描述

if(color[P]==red&&color[SL]==black&&solor[SR]==black)
{
    color[P]=black;
    color[S]=red;
    return;
}

情況7:S節點是紅色的,因此P,SL,SR均為黑色,旋轉S,將S染成黑色,將P染成紅色,修復完成,退出。

if(color[S]==red)
{
    Rotate(S);
    color[S]=black;
    color[P]=red;
    return;
}

接下來的情況都是SL,SR有紅的情況
情況8:SL,SR中,較遠點是黑色,我們就通過旋轉將較遠點調整為紅色,繼續修復。

if(N==left[P]&&color[SR]==black)
{
    Rotate(SL);
    color[S]=red;
    color[SL]=black;
    SR=S;
    S=SL;
    SL=left[S];
}
if(N==right[P]&&color[SL]==black)
{
    Rotate(SR);
    color[S]=red;
    color[SR]=black;
    SL=S;
    S=SR:
    SR=right[S]

情況9:此時較遠點一定為紅色,我們旋轉S,將較遠點染成黑色,將S染成P的顏色,將P染成黑色,修復完成,退出。


    Rotate(S);
    color[S]=color[P];
    color[P]=black;
    if(N==left[P]) color[SR]=black;
    else color[SL]=black;
    return;
}

以上就是刪除修復的全部內容,要遞迴的只有一種情況,且概率小得可以忽略(P,F,B,LB,RB全為黑色),所以刪除修復時間複雜度O(1)

其它操作

紅黑樹的其它操作都基於上述的基本操作,由於查詢操作不可避免,所以所以操作都是O(logn)的,但看似常數較大的插入刪除都是O(1)的,所花的時間可以直接忽略,所以執行紅黑樹的大多數時間用於查詢一個節點。

簡要總結

紅黑樹的確是一個十分優秀的演算法,處理一類問題是有著極其卓越的高效率,但在插入和刪除的修復是程式碼略顯偏長(if語句有點多)。話說回來,插入就是在一個節點下發加一個紅色節點,插入修復就是處理“紅-紅”情況;而刪除,就是在一條鏈中間去掉一個點,再把剩餘的接上,刪除修復就是處理“黑-黑”情況(被刪除點和被刪除點的子節點都是黑色)。或許你會覺得程式碼複雜,理解困難,但只要理解一兩種情況,餘下的可以自己推出來,對於每種情況的處理方式不唯一,如果你能自己推出每種情況的處理方式,那程式碼實現也就不難了。