1. Linux 紅黑樹簡介
紅黑樹是一種自平衡二進位制搜尋樹,用於儲存可排序的鍵/值資料對。這不同於基數樹(基數樹用於有效儲存稀疏陣列,因此使用長整數索引插入/訪問/刪除節點)和雜湊表(不保留排序到容易按順序遍歷,並且必須針對特定大小進行調整,rbtrees適當擴充套件儲存任意鍵的雜湊函式)。紅黑樹類似於AVL樹,但提供更快的實時邊界插入和刪除的最壞情況效能(最多兩次旋轉和分別旋轉三圈以平衡樹),雖然為了平衡速度稍慢(但仍為O(log n))
1.1 Linux 紅黑樹實現
Linux rbtree實現針對速度進行了優化,因此有一個比傳統方式更少的間接樹實現。而不是使用指標來分隔rb_node和資料。在結構中,rb_node的每個例項都嵌入在其組織的資料結構中。而不是使用比較回撥函式指標,使用者應該編寫自己的樹搜尋和插入功能,呼叫提供的rbtree函式。
2.《資料結構與演算法分析》紅黑樹
AVL樹流行的另一變種是紅黑樹。對紅黑樹的操作最壞情形下花費O(logN)時間。(對於插入操作)一種慎重的非遞迴實現可以相對容易的完成(與AVL樹相比)。紅黑樹是具有下列著色性質的二叉查詢樹:
- 每一個節點或者是著成紅色,或者是著成黑色
- 根是黑色的
- 如果一個節點是紅色的,那麼它的子節點必須是黑色的
- 從一個節點到一個NULL指標的每一條路徑必須包含相同數目的黑色節點
著色法則的一個推論是,紅黑樹的高度最多是2log(N+1).因此查詢保證是一種對數的操作。困難在於將一個新項插入到樹中。通常把新項作為樹葉放到樹中,如果我們把它塗成黑色,就違反了條件4.因為會建立一條更長的黑節點的路徑。所以這一項必須塗成紅色,如果它的父節點是黑的,我們插入完成。如果它的父節點已經是紅色的,那麼就違反了條件3.這時我們必須調整該數以確保條件3滿足(又不引起條件4被破壞)。完成這項任務的基本操作是顏色的改變和樹的旋轉。
2.1 自底向上插入
如果向如下圖所示,插入25,則非常簡單,因為父節點是黑色節點
如果父節點是紅色的,那麼有幾種情形(每種都有一個映象對稱)需要考慮。首先,假設這個父節點的兄弟是黑的(約定:NULL節點都是黑色的)。這對於插入3或8是適用的,但對插入99不適用。令X是新加的樹葉,P是它的父節點,S是該節點的兄弟(若存在),G是祖父節點。這種情形只有X和P是紅的,G是黑的,因為否則就會在插入前有兩個相連的紅色節點,違反了紅黑樹的法則。X,P,G可以形成一個一字形鏈或之字形鏈(兩個方向中的任一個方向)
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的父節點的兄弟不可能也是紅的。
我們假設要將45插入到圖12-9中的樹上,在沿樹向下的過程中,我們看到50有兩個紅兒子。因此,我們執行一次顏色翻轉,使50為紅的,40和55是黑的。現在50和60都是紅的,我們在60和70之間執行單旋轉,使得60是30的右子樹的黑根,而70和50都是紅的,如果我們看到在含有兩個紅兒子的路徑上有另外一些節點,那麼我們繼續,執行同樣的操作。當我們到達樹葉時,把45作為紅節點插入,由於父節點是黑的,因此插入完成。
紅黑樹常常平衡的很好,平均紅黑樹大約和平均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和他的祖父將是黑的,此時可以回到第一種主情況。
參考文獻
- Mark Allen Weiss.資料結構與演算法分析[M].America, 2007
- Linux紅黑樹說明文件-linux/Documentation/rbtree.txt
- 紅黑樹解析與移植-https://blog.csdn.net/npy_lp/article/details/7420689
- 紅黑樹與AVL樹優劣-https://www.zhihu.com/question/19856999
- 紅黑樹平衡原理解析-https://my.oschina.net/u/4543837/blog/4406384
本文作者: CrazyCatJack
本文連結: https://www.cnblogs.com/CrazyCatJack/p/14408192.html
版權宣告:本部落格所有文章除特別宣告外,均採用 BY-NC-SA 許可協議。轉載請註明出處!
關注博主:如果您覺得該文章對您有幫助,可以點選文章右下角推薦一下,您的支援將成為我最大的動力!