一步一圖一代碼,一定要讓你真正徹底明白紅黑樹

一步一圖一代碼,一定要讓你真正徹底明白黑樹

作者:July二零一一年一月

<!--EndFragment-->

-----------------------------

本文參考:
I、 The Art of Computer Programming Volume I
II、 Introduction to Algorithms, Second Edition
III、The Annotated STL Sources
IV、 Wikipedia
V、 Algorithms In C Third Edition

VI、 本人寫的關於紅黑樹的前三篇文章:

第一篇:教你透徹瞭解紅黑樹:
http://blog.csdn.net/v_JULY_v/archive/2010/12/29/6105630.aspx
第二篇:紅黑樹算法的層層剖析與逐步實現
http://blog.csdn.net/v_JULY_v/archive/2010/12/31/6109153.aspx
第三篇:教你徹底實現紅黑樹:紅黑樹的c源碼實現與剖析
http://blog.csdn.net/v_JULY_v/archive/2011/01/03/6114226.aspx

---------------------------------------------
前言:
1、有讀者反應,說看了我的前幾篇文章,對紅黑樹的瞭解還是不夠透徹。
2、我個人覺得,如果我一步一步,用圖+代碼來闡述各種插入、刪除情況,可能會更直觀易懂。
3、既然寫了紅黑樹,那麼我就一定要把它真正寫好,讓讀者真正徹底明白紅黑樹。

本文相對我前面紅黑樹相關的3篇文章,主要有以下幾點改進:
1.圖、文字敘述、代碼編寫,彼此對應,明朗而清晰。
2.宏觀總結,紅黑樹的性質與插入、刪除情況的認識。
3.代碼來的更直接,結合圖,給你最直觀的感受,徹底明白紅黑樹。

ok,首先,以下幾點,你現在應該是要清楚明白了的:
I、紅黑樹的五個性質:
1)每個結點要麼是紅的,要麼是黑的。
2)根結點是黑的。
3)每個葉結點,即空結點(NIL)是黑的。
4)如果一個結點是紅的,那麼它的倆個兒子都是黑的。
5)對每個結點,從該結點到其子孫結點的所有路徑上包含相同數目的黑結點。

II、紅黑樹插入的幾種情況:
情況1,z的叔叔y是紅色的。
情況2:z的叔叔y是黑色的,且z是右孩子
情況3:z的叔叔y是黑色的,且z是左孩子

III、紅黑樹刪除的幾種情況。
情況1:x的兄弟w是紅色的。
情況2:x的兄弟w是黑色的,且w的倆個孩子都是黑色的。
情況3:x的兄弟w是黑色的,且w的左孩子是紅色,w的右孩子是黑色。
情況4:x的兄弟w是黑色的,且w的右孩子是紅色的。

除此之外,還得明確一點:
IV、我們知道,紅黑樹插入、或刪除結點後,
可能會違背、或破壞紅黑樹的原有的性質,
所以爲了使插入、或刪除結點後的樹依然維持爲一棵新的紅黑樹,
那就要做倆方面的工作:
1、部分結點顏色,重新着色
2、調整部分指針的指向,即左旋、右旋。

V、並區別以下倆種操作:
1)紅黑樹插入、刪除結點的操作,RB-INSERT(T, z),RB-DELETE(T, z)
2).紅黑樹已經插入、刪除結點之後,
爲了保持紅黑樹原有的紅黑性質而做的恢復與保持紅黑性質的操作。
如RB-INSERT-FIXUP(T, z),RB-DELETE-FIXUP(T, x)

以上這5點,我已經在我前面的2篇文章,都已闡述過不少次了,希望,你現在已經透徹明瞭。

---------------------------------------------------------------------

本文,着重圖解分析紅黑樹插入、刪除結點後爲了維持紅黑性質而做修復工作的各種情況。
[文各種插入、刪除的情況,與我的第二篇文章,紅黑樹算法的實現與剖析相對應]

ok,開始。
一、在下面的分析中,我們約定:
要插入的節點爲,N
父親節點,P
祖父節點,G
叔叔節點,U
兄弟節點,S

如下圖所示,找一個節點的祖父和叔叔節點:
node grandparent(node n) //祖父

{
return n->parent->parent;
}

node uncle(node n) //叔叔

{
if (n->parent == grandparent(n)->left)
return grandparent(n)->right;
else
return grandparent(n)->left;
}

二、紅黑樹插入的幾種情況
情形1: 新節點N位於樹的根上,沒有父節點
void insert_case1(node n) {
if (n->parent == NULL)
n->color = BLACK;
else
insert_case2(n);
}

情形2: 新節點的父節點P是黑色
void insert_case2(node n) {
if (n->parent->color == BLACK)
return; /* 樹仍舊有效 */
else
insert_case3(n);
}


情形3:父節點P、叔叔節點U,都爲紅色,
[對應第二篇文章中,的情況1:z的叔叔是紅色的。]
void insert_case3(node n) {
if (uncle(n) != NULL && uncle(n)->color == RED) {
n->parent->color = BLACK;
uncle(n)->color = BLACK;
grandparent(n)->color = RED;
insert_case1(grandparent(n)); //因爲祖父節點可能是紅色的,違反性質4,遞歸情形1.
}
else
insert_case4(n); //否則,叔叔是黑色的,轉到下述情形4處理。

此時新插入節點N做爲P的左子節點或右子節點都屬於上述情形3,上圖僅顯示N做爲P左子的情形。

情形4: 父節點P是紅色,叔叔節點U是黑色或NIL;
插入節點N是其父節點P的右孩子,而父節點P又是其父節點的左孩子。
[對應我第二篇文章中,的情況2:z的叔叔是黑色的,且z是右孩子]
void insert_case4(node n) {
if (n == n->parent->right && n->parent == grandparent(n)->left) {
rotate_left(n->parent);
n = n->left;
} else if (n == n->parent->left && n->parent == grandparent(n)->right) {
rotate_right(n->parent);
n = n->right;
}
insert_case5(n); //轉到下述情形5處理。

情形5: 父節點P是紅色,而叔父節點U 是黑色或NIL,
要插入的節點N 是其父節點的左孩子,而父節點P又是其父G的左孩子。
[對應我第二篇文章中,情況3:z的叔叔是黑色的,且z是左孩子。]
void insert_case5(node n) {
n->parent->color = BLACK;
grandparent(n)->color = RED;
if (n == n->parent->left && n->parent == grandparent(n)->left) {
rotate_right(grandparent(n));
} else {
/* 反情況,N 是其父節點的右孩子,而父節點P又是其父G的右孩子 */
rotate_left(grandparent(n));
}
}

三、紅黑樹刪除的幾種情況
上文我們約定,兄弟節點設爲S,我們使用下述函數找到兄弟節點:
struct node * sibling(struct node *n) //找兄弟節點
{
if (n == n->parent->left)
return n->parent->right;
else
return n->parent->left;
}

情況1: N 是新的根。
void
delete_case1(struct node *n)
{
if (n->parent != NULL)
delete_case2(n);
}


情形2:兄弟節點S是紅色
[對應我第二篇文章中,情況1:x的兄弟w是紅色的。]
void delete_case2(struct node *n)
{
struct node *s = sibling(n);

if (s->color == RED) {
n->parent->color = RED;
s->color = BLACK;
if (n == n->parent->left)
rotate_left(n->parent); //左旋
else
rotate_right(n->parent);
}
delete_case3(n);
}

情況 3: 兄弟節點S是黑色的,且S的倆個兒子都是黑色的。但N的父節點P,是黑色。
[對應我第二篇文章中,情況2:x的兄弟w是黑色的,且兄弟w的倆個兒子都是黑色的
(這裏,父節點P爲黑)]
void delete_case3(struct node *n)
{
struct node *s = sibling(n);

if ((n->parent->color == BLACK) &&
(s->color == BLACK) &&
(s->left->color == BLACK) &&
(s->right->color == BLACK)) {
s->color = RED;
delete_case1(n->parent);
} else
delete_case4(n);
}

情況4: 兄弟節點S 是黑色的、S 的兒子也都是黑色的,但是 N 的父親P,是紅色。
[還是對應我第二篇文章中,情況2:x的兄弟w是黑色的,且w的倆個孩子都是黑色的
(這裏,父節點P爲紅)]
void delete_case4(struct node *n)
{
struct node *s = sibling(n);

if ((n->parent->color == RED) &&
(s->color == BLACK) &&
(s->left->color == BLACK) &&
(s->right->color == BLACK)) {
s->color = RED;
n->parent->color = BLACK;
} else
delete_case5(n);
}

情況5: 兄弟S爲黑色,S 的左兒子是紅色,S 的右兒子是黑色,而N是它父親的左兒子。
//此種情況,最後轉化到下面的情況6。
[對應我第二篇文章中,情況3:x的兄弟w是黑色的,w的左孩子是紅色,w的右孩子是黑色。]
void delete_case5(struct node *n)
{
struct node *s = sibling(n);

if (s->color == BLACK)
if ((n == n->parent->left) &&
(s->right->color == BLACK) &&
(s->left->color == RED)) {
// this last test is trivial too due to cases 2-4.
s->color = RED;
s->left->color = BLACK;
rotate_right(s);
} else if ((n == n->parent->right) &&
(s->left->color == BLACK) &&
(s->right->color == RED)) {
// this last test is trivial too due to cases 2-4.
s->color = RED;
s->right->color = BLACK;
rotate_left(s);
}
}
delete_case6(n); //轉到情況6。

情況6: 兄弟節點S是黑色,S的右兒子是紅色,而 N 是它父親的左兒子。
[對應我第二篇文章中,情況4:x的兄弟w是黑色的,且w的右孩子時紅色的。]
void delete_case6(struct node *n)
{
struct node *s = sibling(n);

s->color = n->parent->color;
n->parent->color = BLACK;

if (n == n->parent->left) {
s->right->color = BLACK;
rotate_left(n->parent);
} else {
s->left->color = BLACK;
rotate_right(n->parent);
}
}

//呵呵,畫這12張圖,直接從中午畫到了晚上。希望,此文能讓你明白。

四、紅黑樹的插入、刪除情況時間複雜度的分析
因爲每一個紅黑樹也是一個特化的二叉查找樹,
因此紅黑樹上的只讀操作與普通二叉查找樹上的只讀操作相同。
然而,在紅黑樹上進行插入操作和刪除操作會導致不再符合紅黑樹的性質。

恢復紅黑樹的屬性需要少量(O(log n))的顏色變更(實際是非常快速的)和
不超過三次樹旋轉(對於插入操作是兩次)。
雖然插入和刪除很複雜,但操作時間仍可以保持爲 O(log n) 次。


ok,完。

後記:
此紅黑樹系列,前前後後,已經寫了4篇文章,如果讀者讀完了這4篇文章,
對紅黑樹有一個相對之前來說,比較透徹的理解,
那麼,也不枉費,我花這麼多篇幅、花好幾個鐘頭去畫紅黑樹了。

真正理解一個數據結構、算法,最緊要的還是真正待用、實踐的時候體會。
歡迎,各位,將現在、或以後學習、工作中運用此紅黑樹結構、算法的經驗與我分享。
謝謝。:D。
----------------------------------------

作者聲明: 本人July對本博客所有文章和資料享有版權,轉載、或引用任何內容請註明出處。 向您的厚道致敬。謝謝。二零一一年一月九日。