1. 程式人生 > >紅黑樹詳解

紅黑樹詳解

[toc] *** ## 1. Linux 紅黑樹簡介 紅黑樹是一種自平衡二進位制搜尋樹,用於儲存可排序的鍵/值資料對。這不同於基數樹(基數樹用於有效儲存稀疏陣列,因此使用長整數索引插入/訪問/刪除節點)和雜湊表(不保留排序到容易按順序遍歷,並且必須針對特定大小進行調整,rbtrees適當擴充套件儲存任意鍵的雜湊函式)。紅黑樹類似於AVL樹,但提供更快的實時邊界插入和刪除的最壞情況效能(最多兩次旋轉和分別旋轉三圈以平衡樹),雖然為了平衡速度稍慢(但仍為O(log n)) ### 1.1 Linux 紅黑樹實現 Linux rbtree實現針對速度進行了優化,因此有一個比傳統方式更少的間接樹實現。而不是使用指標來分隔rb_node和資料。在結構中,rb_node的每個例項都嵌入在其組織的資料結構中。而不是使用比較回撥函式指標,使用者應該編寫自己的樹搜尋和插入功能,呼叫提供的rbtree函式。 ## 2.《資料結構與演算法分析》紅黑樹 AVL樹流行的另一變種是紅黑樹。對紅黑樹的操作最壞情形下花費O(logN)時間。(對於插入操作)一種慎重的非遞迴實現可以相對容易的完成(與AVL樹相比)。紅黑樹是具有下列著色性質的二叉查詢樹: 1. 每一個節點或者是著成紅色,或者是著成黑色 2. 根是黑色的 3. 如果一個節點是紅色的,那麼它的子節點必須是黑色的 4. 從一個節點到一個NULL指標的每一條路徑必須包含相同數目的黑色節點 著色法則的一個推論是,紅黑樹的高度最多是2log(N+1).因此查詢保證是一種對數的操作。困難在於將一個新項插入到樹中。通常把新項作為樹葉放到樹中,如果我們把它塗成黑色,就違反了條件4.因為會建立一條更長的黑節點的路徑。所以==這一項必須塗成紅色==,如果它的父節點是黑的,我們插入完成。如果它的父節點已經是紅色的,那麼就違反了條件3.這時我們必須調整該數以確保條件3滿足(又不引起條件4被破壞)。完成這項任務的基本操作是==顏色的改變和樹的旋轉==。 ### 2.1 自底向上插入 如果向如下圖所示,插入25,則非常簡單,因為父節點是黑色節點 [![sCjSKJ.png](https://s3.ax1x.com/2021/01/04/sCjSKJ.png)](https://imgchr.com/i/sCjSKJ) 如果父節點是紅色的,那麼有幾種情形(每種都有一個映象對稱)需要考慮。首先,假設這個父節點的兄弟是黑的(約定:NULL節點都是黑色的)。這對於插入3或8是適用的,但對插入99不適用。令X是新加的樹葉,P是它的父節點,S是該節點的兄弟(若存在),G是祖父節點。這種情形只有X和P是紅的,G是黑的,因為否則就會在插入前有兩個相連的紅色節點,違反了紅黑樹的法則。X,P,G可以形成一個一字形鏈或之字形鏈(兩個方向中的任一個方向) [![sP9cQK.png](https://s3.ax1x.com/2021/01/04/sP9cQK.png)](https://imgchr.com/i/sP9cQK) 12-10展示了當P是左兒子時(還有一個對稱情況),該如何旋轉和更改著色該樹。第一種情形對應P和G之間的單旋轉,第二種情形對應雙旋轉,雙旋轉首先在X和P之間進行,然後在X和G之間進行。當編寫程式的時候,我們必須記錄父節點、祖父節點,以及為了重新連線還要記錄曾祖父節點。兩種情形下,子樹的新根均被塗成黑色,因此即使原來的曾祖是紅色的,我們也排除了兩個相鄰紅節點的可能性。同樣重要的是,這些==旋轉的結果是通向A,B和C諸路徑上的黑節點個數保持不變==。如果我們企圖將79插入到圖12-9中,如果S是紅色的,初始時從子樹的根到C的路徑上有一個黑色節點,在旋轉之後,一定==仍然還是隻有一個黑色節點==。兩種情況下,在通向C的路徑上都有三個節點(新的根,G和S)。由於只有一個可能是黑的,且我們不能有連續的紅色節點,我們必須把S和子數的新根都塗成紅色,而把G(以及第四個節點)都塗成黑色。但是如果曾祖父也是紅色的,那麼我們可以將這個過程朝著根的方向上濾,就像對B樹和二叉堆所做的那樣,直到我們不再有兩個相連的紅色節點或者到達根(它將被重新塗成黑色)處為止 ### 2.2 自頂向下的紅黑樹 上濾的實現需要用一個棧或用一些父指標儲存路徑。如果使用一個自頂向下的過程,實際上是對紅黑樹應用從頂向下保證S不會是紅的過程,則伸展樹會更有效。在向下的過程中,當我們看到一個節點X有兩個紅兒子的時候,我們讓X成為紅的而讓它的兩個兒子成為黑的。只有當X的父節點P也是紅的時候這種翻轉將破壞紅黑的法則,此時我們可以進行12-10中那樣適當的旋轉。如果X的父節點的兄弟是紅的會如何,這種可能已經被從頂向下過程中的行動==所排除==。因此==X的父節點的兄弟不可能是紅的==。如果在沿樹向下的過程中我們看到一個節點==Y有兩個紅兒子,那我們知道Y的孫子必然是黑的==。由於Y的兒子也要變成黑的,甚至在可能發生的旋轉之後,因此我們將不會看到兩層上另外的紅節點。這樣,當我們看到X,若X的父節點是紅的,則X的父節點的兄弟不可能也是紅的。 [![sPJ2CR.png](https://s3.ax1x.com/2021/01/04/sPJ2CR.png)](https://imgchr.com/i/sPJ2CR) 我們假設要將45插入到圖12-9中的樹上,在沿樹向下的過程中,我們看到50有兩個紅兒子。因此,我們執行一次顏色翻轉,使50為紅的,40和55是黑的。現在50和60都是紅的,我們在60和70之間執行單旋轉,使得60是30的右子樹的黑根,而70和50都是紅的,如果我們看到在含有兩個紅兒子的路徑上有另外一些節點,那麼我們繼續,執行同樣的操作。當我們到達樹葉時,把45作為紅節點插入,由於父節點是黑的,因此插入完成。 [![sPULTS.png](https://s3.ax1x.com/2021/01/04/sPULTS.png)](https://imgchr.com/i/sPULTS) 紅黑樹常常平衡的很好,==平均紅黑樹大約和平均AVL樹一樣深==,從而==查詢時間一般接近最優==。紅黑樹的==優點是執行插入所需要的開銷相對較低==,實踐中==發生的旋轉相對較少==。紅黑樹的具體實現是複雜的,不僅因為有大量可能地旋轉,而且還因為一些子樹可能是空的,以及處理根的特殊的情況(尤其是根沒有父親)。因此我們使用兩個標記節點:一個是根,一個是NullNode。它的作用像在伸展樹中那樣是指示一個NULL指標。根標記將儲存關鍵字負無窮和一個指向真正的根的右指標。為此,查詢和列印過程需要調整,遞迴的歷程都很巧妙。我們使用一個隱藏的遞迴過程,而並不強迫使用者傳遞T->Right。因此使用者不必關心頭節點。 ``` 使用兩個標記對樹的中序遍歷 列印樹,關注NULLNode,跳過頭部 static void DoPrint(RedBlackTree T) { if (T != NullNode) { DoPrint(T->Left); Output(T->Element); DoPrint(T->Right); } } void PrintTree(RedBlackTree T) { DoPrint(T->Right); } ``` 我們還需要使使用者呼叫歷程Initialize來指定頭節點。如果構造的是第一棵樹,那麼Initialize應該再為NullNode分配記憶體(其後的樹可以分享NullNode) ``` typedef enum ColorType{Red, Blck} ColorType; struct RedBlackNode { ElementType Element; RedBlackTree Left; RedBlackTree Right; ColorType Color; }; Position NullNode = NULL; /* Needs initialization */ /* Initialization procedure */ RedBlackTree Initialize(void) { RedBlackTree T; if (NullNode == NULL) { NullNode = malloc (sizeof(struct RedBlackNode)); if (NullNode == NULL) FatalError("Out of space!"); NullNode->Left = NullNode->Right = NullNode; NullNode->Color = Black; NullNode->Element = Infinity; } /* Create the header node */ T = malloc(sizeof(struct RedBlackNode)); if (T == NULL) FatalError("Out of space!"); T->Element = NegInfinity; T->Left = T->Right = NullNode; T->Color = Balck; return T; } ``` 旋轉過程 ``` 在X節點處執行旋轉, static Position Rotate(ElementType Item, Position Parent) { if (Item < Parent->Element) return Parent->Left = Item < Parent->Left->Element? SingleRotateWithLeft(Parent->Left) : SingleRotateWithRight(Parent->Left); else return Parent->Right = Item < Parent->Right->Element? SingleRotateWithLeft(Parent->Right) : SingleRotateWithRight(Parent->Right); } ``` 插入過程 ``` static Position X, P, GP, GGP; static void HandleReorient(ElementType Item, RedBlackTree T) { X->Color = Red; /* Do the color flip */ X->Left->Color = Black; X->Right->Color = Black; if (P->Color == Red) /* 必須進行旋轉 */ { GP->Color = Red; if ((Item < GP->Element) != (Item < P->Element)) P = Rotate(Item, GP); /* 開始雙旋轉 */ X = Rotate(Item, GGP); X->Color = Black; } T->Right->Color = Black; /* Make root black */ } RedBlackTree Insert(ElementType Item, RedBlackTree T) { X = P = GP = T; NullNode->Element = Item; while (X->Element != Item) { GGP = GP; GP = P; P = X; if (Item < X->Element) X = X->Left; else X = X->Right; if (X->Left->Color == Red && X->Right->Color == Red) HandleReorient(Item, T); } if (X != NullNode) return NullNode; /* Duplicate */ X = malloc(sizeof(struct RedBlackNode)); if (X == NULL) FatalError("Out of space!"); X->Element = Item; X->Left = X->Right = NullNode; if (Item < P->Element) /* Attach to its parent */ P->Left = X; else P->Right = X; HandleReorient(Item, T); / * Color red: 可能需要旋轉 */ return T; } ``` ### 2.3 自頂向下的刪除 紅黑樹中的刪除也可以自頂向下進行,每一件工作都歸結於==能夠刪除一片樹葉==。因為要==刪除一個帶有兩個兒子的節點==,我們用==右子樹上的最小節點代替他==。該節點必然最多隻有一個兒子,然後將該節點刪除。只有一個右兒子的節點可以用相同的方式刪除,而只有一個左兒子的節點通過用其左子樹上最大節點替換,然後可將該節點刪除。對於紅黑樹,我們使用的方法繞過帶有一個兒子的節點的情形,因為這可能在樹的中部連線兩個紅色節點,為紅黑條件的實現增加困難。 紅色樹葉的刪除很簡單,如果一片樹葉是黑的,刪除會變複雜,因為黑色節點的刪除將破壞條件4.解決辦法是==保證從上到下刪除期間樹葉是紅的==。令X為當前節點,T是它的兄弟,而P是他們的父親。開始時我們把樹的根部塗成紅色。當沿樹向下遍歷時,我們設法保證X是紅色的,當我們到達一個新的節點時,我們要去確信P是紅的。並且X和T是黑的(因為我們不能有兩個相連的紅色節點)。存在兩種主要的情形: 首先,設X有兩個黑兒子,此時有三種子情況,如果==T也有兩個黑兒子,那麼我們可以翻轉X,T和P的顏色==來保持這種不變性,否則,T的兒子之一是紅的,根據這個兒子節點是哪一個,可以應用下圖第二和第三種情形表示的旋轉。==注意這種情形對於樹葉將是適用的,因為NullNode被認為是黑的==。設X的兒子之一是紅的,在這種情形下,我們落到下一層上,得到新的X,T和P。如果X落在紅兒子上,我們可以繼續向前進行。如果不是這樣,我們知道T將是紅的,而X和P將是黑的。我們可以旋轉T和P,使得X的新父親是紅的,X和他的祖父將是黑的,此時可以回到第一種主情況。 [![si9uBF.png](https://s3.ax1x.com/2021/01/04/si9uBF.png)](https://imgchr.com/i/si9uBF) ## 參考文獻 1. Mark Allen Weiss.資料結構與演算法分析[M].America, 2007 2. Linux紅黑樹說明文件-linux/Documentation/rbtree.txt 3. 紅黑樹解析與移植-https://blog.csdn.net/npy_lp/article/details/7420689 4. 紅黑樹與AVL樹優劣-https://www.zhihu.com/question/19856999 5. 紅黑樹平衡原理解析-https://my.oschina.net/u/4543837/blog/4406384 *** **本文作者:** CrazyCatJack **本文連結:** [https://www.cnblogs.com/CrazyCatJack/p/14408192.html](https://www.cnblogs.com/CrazyCatJack/p/14408192.html) **版權宣告:**本部落格所有文章除特別宣告外,均採用 [BY-NC-SA](https://creativecommons.org/licenses/by-nc-nd/4.0/) 許可協議。轉載請註明出處! **關注博主:**如果您覺得該文章對您有幫助,可以點選文章右下角**推薦**一下,您的支援將成為我最大的動力