紅黑樹之插入節點
紅黑樹的性質
紅黑樹是每個節點都帶有顏色屬性的二叉查找樹,顏色或紅色或黑色。在二叉查找樹強制一般要求以外,對於任何有效的紅黑樹我們增加了如下的額外要求:
- 節點是紅色或黑色。
- 根節點是黑色。
- 每個葉節點(這里的葉節點是指NULL節點,在《算法導論》中這個節點叫哨兵節點,除了顏色屬性外,其他屬性值都為任意。為了和以前的葉子節點做區分,原來的葉子節點還叫葉子節點,這個節點就叫他NULL節點吧)是黑色的。
- 每個紅色節點的兩個子節點都是黑色。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點,或者理解為紅節點不能有紅孩子)
- 從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點(黑節點的數目稱為黑高black-height)。
正是紅黑樹的這5條性質,使一棵n個結點的紅黑樹始終保持了logn的高度,從而也就解釋了上面所說的“紅黑樹的查找、插入、刪除的時間復雜度最壞為O(log n)”這一結論成立的原因
通過對任何一條從根到葉子的路徑上各個結點着色方式的限制,紅黑樹確保沒有一條路徑會比其他路徑長出倆倍,因而是接近平衡的。
紅黑樹雖然本質上是一棵二叉查找樹,但它在二叉查找樹的基礎上增加了着色和相關的性質使得紅黑樹相對平衡,從而保證了紅黑樹的查找、插入、刪除的時間復雜度最壞為O(log n)。
紅黑樹的插入操作
紅黑樹首先是一棵二叉排序樹,所以它的插入操作要遵循二叉排序樹的插入原則。
接下來我們先考慮為待插入的節點塗上顏色
如果將待插入的節點塗成黑色,則新的樹必然違反了性質5 ,而這個時候我們讓樹重新成為紅黑樹的方法,似乎只有將剛剛插入的節點的顏色改成紅色(之所以用似乎,是因為我只想到了這種方法,也許有其他方法,或者說,這種方法是效率最高的,畢竟之所以要建立紅黑樹,而不是普通的二叉排序樹,看中的就是紅黑樹的效率),這樣不就多此一舉了嗎,我們直接把待插入的節點塗成紅色不就得了。
新插入節點后會使紅黑樹的哪些性質遭到破壞?
上面我們已經確定了,待插入的節點要被塗成紅色。那么插入一個紅色節點會破壞哪些性質呢?首先1和3一定不會被破壞(很顯然了),此外5也不會被破壞,因為紅節點代替了原來的NULL節點(黑色),但是紅節點自身也有兩個NULL節點,所以插入一個紅色節點,路徑上的黑節點數目並不會發生改變。所以,插入一個節點影響的只能是性質2和性質4,而且是可能影響:當插入的節點是根節點的時候,性質2就遭到破壞;當插入的節點的父親節點為紅色的時候,性質4就遭到破壞。明確了哪種性質會被破壞,再牢記紅黑樹的5個性質,我們就有了調整的依據。
如何調整插入節點后帶來的影響?
首先,性質2遭到破壞的情況調整起來比較簡單,就是將節點的顏色改成黑色就好啦!
對於性質4遭到破壞的情況,比較復雜,是我們要重點關注的。
現在我們關注的情況是:插入的節點為紅色,其父親節點也為紅色的情況。這種情況要根據其叔叔節點的顏色,再細分成兩種情況:
叔叔節點的顏色為紅色:(這個時候,它的祖父節點必為黑色)

如上圖,現在我們插入21
這個時候我們采取的辦法是,將25節點(也就是祖父節點)塗成紅色,然后將父親節點和叔叔節點塗成黑色。如下圖:
這個時候又出現了新的問題,就是17和25節點又違法了性質4,這個時候我們看到25的叔叔節點,也就是8節點也為紅色,那么情況和上一次調整是一樣的,我們就再做一次和上次一樣的調整即可。
再次調整后,我們發現,現在違反的只有性質2了,這個時候我們就把根節點改成黑色就行啦!
所以,總結一下,就是,當叔叔節點為紅色的時候調整的過程是:將新插入的節點作為起始點,然后將父親節點和叔叔節點塗成黑色,將祖父節點塗成紅色,然后以祖父節點為新的起始點再次進行同樣的操作(如果新的起點的叔叔節點也為紅色)。這樣直到新的起始點為根節點為止,最后把根節點塗成黑色即可。
但是問題來了,如果叔叔節點是黑色要怎么辦,或者在上面的過程中,某個新的起始點的叔叔節點是黑色的怎么辦。這就是下面我們要討論的第二種情況。
叔叔節點為黑色:(這個時候,祖父節點也必定為黑色,因為父親節點時紅色的)
如圖,我們插入的是7節點
這個時候我們采取的調整方法為:我們將以祖父節點(1節點)為根的子樹看成是RR型,然后進行RR型的調整(就是AVL樹種的RR型),最后將新的根節點塗成黑色,將其兩個孩子中不是紅色的節點的顏色改成紅色。
如下圖:
這個時候,我們發現整棵樹已經是紅黑樹了,不需要向情況1一樣一直調整到根部了。
有RR型,自然也有LL型,LR型和RL型。如下圖:
我們可以發現,新的樹根總是黑色的,它的兩個孩子總是紅色的。
對於AVL樹的四種類型:LL、LR、RR、RL的類型判斷和調整,請看我之前的文章:http://www.cnblogs.com/qingergege/p/7294892.html
代碼如何寫:
有了上面的理論知識,我們現在要看代碼怎么寫
首先先給出節點的數據結構:
//定義節點的顏色 enum color{ BLACK, RED }; //節點的數據結構 typedef struct b_node{ int value;//節點的值 enum color color;//樹的深度 struct b_node *l_tree;//左子樹 struct b_node *r_tree;//右子樹 struct b_node *parent;//父親節點 } BNode,*PBNode; /** * 分配一個節點 * */ PBNode allocate_node() { PBNode node = NULL; node = (PBNode)malloc(sizeof(struct b_node)); if(node == NULL) return NULL; memset(node,0,sizeof(struct b_node)); return node; } /** * 設置一個節點的值 * */ void set_value(PBNode node,int value) { if(node == NULL) return; node->value = value; node->color = RED; node->l_tree = NULL; node->r_tree = NULL; node->parent = NULL; }
紅黑樹的插入,首先要遵循二叉排序樹的插入,所以我們先給出二叉排序樹的插入代碼(這里因為要涉及父親節點的指向,所以用非遞歸的方法創建),由於根節點的調整情況比較簡單,只需將顏色改成黑色即可,所以我們捎帶加上一句即可。
/** * 向二叉查找樹中添加一個節點,使得新的二叉樹依然時二叉查找樹 * 非遞歸方法實現 * */ void insert_node(PBNode *root,int value) { if(*root == NULL) { *root = allocate_node(); set_value(*root,value); (*root)->color = BLACK; } else { PBNode p = *root; PBNode pp = NULL;//保存父親節點 bool is_left = false; while(p != NULL) { pp = p; is_left = false; if(value < p->value) { is_left = true; p = p->l_tree; } else if(value > p->value) { p = p->r_tree; } } PBNode node = allocate_node(); set_value(node,value); node->parent = pp;//填父親節點 if(is_left) { pp->l_tree = node; } else { pp->r_tree = node; } } }
接下來就該討論調整部分的代碼了。
首先,我們從小到大,先從小的模塊開始。上面提到,當叔叔節點為黑色的時候,除了顏色的改變之外,還需要進行RR、RL、LL、LR四種類型的變換操作,所以我們可以先定義兩個功能函數,一個用於判斷子樹為哪種類型,另一個函數對子樹,根據類型進行調整,捎帶把顏色也變了。(四種類型的確定和調整,不懂的可以看我之前的文章,http://www.cnblogs.com/qingergege/p/7294892.html)
/** * 根據子樹類型進行調整,順便把顏色也調整了 * ch_root為待調整的子樹的樹根,也就是插入新插入節點的祖父節點 * type為子樹的類型 * root為整棵樹的樹根,因為這個過程中,整棵樹的樹根可能都在隨時變換 * */ void case2_adjust(PBNode *root,PBNode ch_root,enum unbalance_type type) { int t = type; PBNode small; PBNode middle; PBNode big; switch (t) { case TYPE_LL: { //確定small、middle、big三個節點 big = ch_root; middle = ch_root->l_tree; small = ch_root->l_tree->l_tree; //分配middle節點的孩子,給small和big big->l_tree = middle->r_tree; //別忘了該父親節點!!!!!!!!! if(middle->r_tree != NULL) middle->r_tree->parent = big; //將small和big作為midlle的左子和右子 middle->r_tree = big; break; } case TYPE_LR: { //確定small、middle、big三個節點 big = ch_root; small = ch_root->l_tree; middle = ch_root->l_tree->r_tree; //分配middle節點的孩子,給small和big small->r_tree = middle->l_tree; big->l_tree = middle->r_tree; //別忘了該父親節點!!!!!!!!! if(middle->l_tree != NULL) middle->l_tree->parent = small; if(middle->r_tree != NULL) middle->r_tree->parent = big; //將small和big作為midlle的左子和右子 middle->l_tree = small; middle->r_tree = big; break; } case TYPE_RL: { //確定small、middle、big三個節點 small = ch_root; big = ch_root->r_tree; middle = ch_root->r_tree->l_tree; //分配middle節點的孩子,給small和big small->r_tree = middle->l_tree; big->l_tree = middle->r_tree; //別忘了該父親節點!!!!!!!!! if(middle->l_tree != NULL) middle->l_tree->parent = small; if(middle->r_tree != NULL) middle->r_tree->parent = big; //將small和big作為midlle的左子和右子 middle->l_tree = small; middle->r_tree = big; break; } case TYPE_RR: { //確定small、middle、big三個節點 small =ch_root; middle = ch_root->r_tree; big = ch_root->r_tree->r_tree; //分配middle節點的孩子,給small和big small->r_tree = middle->l_tree; //別忘了該父親節點!!!!!!!!! if(middle->l_tree != NULL) middle->l_tree->parent = small; //將small和big作為midlle的左子和右子 middle->l_tree = small; break; } } //將子樹的父親節點的子節點指向middle(也就是將middle,調整后的子樹的根結點) if(ch_root->parent == NULL) //說明子樹的根節點就是整棵樹的根結點 { *root = middle; } else if(ch_root->parent->l_tree == ch_root)//根是父親的左孩子 { ch_root->parent->l_tree = middle; } else if(ch_root->parent->r_tree == ch_root)//根是父親的右孩子 { ch_root->parent->r_tree = middle; } if(ch_root->parent != NULL) //更改small、middle、big的父親節點 middle->parent = ch_root->parent; big->parent = middle; small->parent = middle; if(ch_root->parent != NULL) //更改節點的顏色 middle->color = BLACK;//根節點為黑色 big->color = RED;//孩子為紅色 small->color = RED;//孩子為紅色 }
對於情況1,也就是叔叔節點為紅色的情況,只涉及顏色的調整,比較簡單,但是我們也可以將其操作封裝在函數中,這樣更加模塊化,代碼也更加清晰。
/** * 對情況1,也就是叔叔節點為紅色的情況的調整(這種情況,只涉及顏色的調整) * 其中begin為調整的起始點,對於第一次調整,這個點就是剛剛插入的節點 * 返回值是下一輪調整(如果需要)的起始節點 * */ PBNode case1_adjust(PBNode begin) { PBNode parent = begin->parent;//父親節點 PBNode grand = parent->parent;//祖父節點 PBNode uncle = grand->l_tree == parent ? grand->r_tree : grand->l_tree;//叔叔節點 //顏色調整 grand->color = RED; parent->color = BLACK; uncle->color = BLACK; //返回下一次迭代的起始點 return grand; }
接下來就是將上面的模塊進行串聯了。由於情況1,也就是叔叔為紅色的情況可能要迭代,所以,串聯要在循環中進行。
首先我們看循環結束的條件,如果一直迭代下去,最后要到根節點,如果迭代的圖中,起始點的父親節點的顏色為黑色則調整結束,如果迭代過程中遇到情況2,也就是叔叔為黑色的情況,那么進行情況2的調整后,也達到了紅黑樹的要求,這個時候循環也要結束。所以我們可以這樣:while循環中條件為非根節點,如果遇到起始點顏色為黑色或者處理了情況2,那么就用break結束循環。代碼如下:
/** * 串連函數,負責將兩種情況的調整函數串連起來 * 參數root為整棵樹的根節點,node為剛剛插入的節點 **/ void insert_adjust(PBNode *root,PBNode node) { PBNode begin = node; while(begin != *root) { if(begin->parent->color == BLACK)//如果父親節點為黑色,則不用調整 { break; } //反之,如果父親節點為紅色則需要調整 PBNode parent = begin->parent;//獲得父親節點 PBNode grand = parent->parent;//獲得祖父節點 PBNode uncle = grand->l_tree == parent ? grand->r_tree: grand->l_tree;//叔叔節點 //注意節點為空的時候,C語言邏輯與和邏輯或都為短路操作,也就是一點能判斷整個表達式的值,就不再進行后面的判斷 if(uncle == NULL || uncle->color == BLACK )//如果叔叔節點為黑色,則進行情況2的調整,調整后,整棵樹就滿足紅黑樹了,則退出循環 { enum unbalance_type type = get_type(grand,begin->value);//獲得子樹的類型 case2_adjust(root,grand,type); break;//退出循環 } //反之,如果叔叔節點為紅色,則進行情況1的調整,返回值為下一次迭代的起始節點 begin = case1_adjust(begin); } if(begin == *root) { (*root)->color = BLACK; } }
最后附上word文件和源代碼文件
鏈接:http://pan.baidu.com/s/1nvQI2iX 密碼:16nd
參考資料:
http://blog.csdn.net/v_JULY_v/article/details/6105630
http://blog.csdn.net/dreamclr/article/details/50962566
http://blog.csdn.net/goodluckwhh/article/details/12718233
http://www.cnblogs.com/sandy2013/p/3270999.html