紅黑樹之刪除節點
上一篇文章中講了如何向紅黑樹中添加節點,也順便創建了一棵紅黑樹。今天寫寫怎樣從紅黑樹中刪除節點。
相比於添加節點,刪除節點要復雜的多。不過我們慢慢梳理,還是能夠弄明白的。
回顧一下紅黑樹的性質
紅黑樹是每個節點都帶有顏色屬性的二叉查找樹,顏色或紅色或黑色。在二叉查找樹強制一般要求以外,對於任何有效的紅黑樹我們增加了如下的額外要求:
- 節點是紅色或黑色。
- 根節點是黑色。
- 每個葉節點(這里的葉節點是指NULL節點,在《算法導論》中這個節點叫哨兵節點,除了顏色屬性外,其他屬性值都為任意。為了和以前的葉子節點做區分,原來的葉子節點還叫葉子節點,這個節點就叫他NULL節點吧)是黑色的。
- 每個紅色節點的兩個子節點都是黑色。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點,或者理解為紅節點不能有紅孩子)
- 從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點(黑節點的數目稱為黑高black-height)。
首先先說一下我們要刪除節點的類型
我們要刪除的節點類型從大的方面來說,只有兩種:
1、 單個的葉子節點(不是指NULL節點,就是二叉排序樹中的葉子節點的概念)
2、 只有右子樹(或只有左子樹)的節點
為什么這樣呢?
我們知道,對於一棵普通的二叉排序樹來說,刪除的節點情況可以分為3種:
1、 葉子節點
2、 只有左子樹或只有右子樹的節點
3、 既有左子樹又有右子樹的節點。
我們知道對於一棵普通二叉樹的情況3來說,要刪除既有左子樹又有右子樹的節點,我們首先要找到該節點的直接后繼節點,然后用后繼節點替換該節點,最后按1或2中的方法刪除后繼節點即可。
所以情況3可以轉換成情況1或2。
同樣,對於紅黑樹來講,我們實際上刪除的節點情況只有兩種。
對於情況2,也就是待刪除的節點只有左子樹或這有右子樹的情況,有很多組合在紅黑樹中是不可能出現的,因為他們違背了紅黑樹的性質。
情況2中不存在的情況有(其中D表示要刪除的節點,DL和DR分別表示左子和右子):
1、
2、
3、
4、
上面這四種明顯都違背了性質5
5、
6、
5和6兩種情況明顯都違背了性質4
此外對於刪除紅色節點的情況比較簡單,我們可以先來看看。
我們上面把待刪除的節點分成為了兩種,那這里,對於刪除的紅色節點,我們也分兩種:
1、 刪除紅色的葉子節點(D表示待刪除的節點,P表示其父親節點)
上面這兩種情況其實處理方式都一樣,直接刪除D就好!
2、 刪除的紅色節點只有左子樹或只有右子樹
上面已經分析了,紅黑樹中根本不存在這種情況!!
接下來我們要討論刪除最復雜的情況了,也就是刪除的節點為黑色的情況
同樣,我們也將其分成兩部分來考慮:
1、 刪除黑色的葉子節點
對於這種情況,相對復雜,后面我們再細分
2、 刪除的黑色節點僅有左子樹或者僅有右子樹
去掉前面已經分析的不存在的情況。這種情況下節點的結構只肯能是
(豎直的西線代替了左右分支的情況)
這兩種情況的處理方式是一樣的,即用D的孩子(左或右)替換D,並將D孩子的顏色改成黑色即可(因為路徑上少了一個黑節點,所已將紅節點變成黑節點以保持紅黑樹的性質)。
所以,這些情況處理起來都很簡單。。。除了,刪除黑色葉子節點的情況。
下面重點討論刪除黑色葉子節點的情況
情況1:待刪除節點D的兄弟節點S為紅色
D是左節點的情況
調整做法是將父親節點和兄弟節點的顏色互換,也就是p變成紅色,S變成黑色,然后將P樹進行AVL樹種的RR型操作,結果如下圖
這個時候我們會發現,D的兄弟節點變成了黑色,這樣就成后面要討論的情況。
D是右節點的情況
將P和S的顏色互換,也就是將P變成紅色,將S變成黑色,然后對P進行類似AVL樹的LL操作。結果如下圖:
此時D的兄弟節點變成了黑色,這樣就成了我們后面要討論的情況
情況2:兄弟節點為黑色,且遠侄子節點為紅色。
D為左孩子對的情況,這時D的遠侄子節點為S的右孩子
沒有上色的節點表示黑色紅色均可,注意如果SL為黑色,則SL必為NULL節點。
這個時候,如果我們刪除D,這樣經過D的子節點(NULL節點)的路徑的黑色節點個數就會減1,但是我們看到S的孩子中有紅色的節點,如果我們能把這棵紅色的節點移動到左側,並把它改成黑色,那么就滿足要求了,這也是為什么P的顏色無關,因為調整過程只在P整棵子樹的內部進行。
調整過程為,將P和S的顏色對調,然后對P樹進行類似AVL樹RR型的操作,最后把SR節點變成黑色,並刪除D即可。
D為右孩子的情況,此時D的遠侄子為S的左孩子
同樣,將P和S的顏色對調,然后再對P樹進行類似AVL樹RL型的操作,最后將SR變成黑色,並刪掉D即可。結果如下圖:
情況3:兄弟節點S為黑色,遠侄子節點為黑色,近侄子節點為紅色
D為左孩子的情況,此時近侄子節點為S的左孩子
做法是,將SL右旋,並將S和SL的顏色互換,這個時候就變成了情況4。
D為右孩子的情況,此時近侄子節點為S的右孩子
做法是將S和SR顏色對調,然后對SR進行左旋操作,這樣就變成了情況4,結果如下圖:
情況4:父親節p為紅色,兄弟節點和兄弟節點的兩個孩子(只能是NULL節點)都為黑色的情況。
如果刪除D,那經過P到D的子節點NULL的路徑上黑色就少了一個,這個時候我們可以把P變成黑色,這樣刪除D后經過D子節點(NULL節點)路徑上的黑色節點就和原來一樣了。但是這樣會導致經過S的子節點(NULL節點)的路徑上的黑色節點數增加一個,所以這個時候可以再將S節點變成紅色,這樣路徑上的黑色節點數就和原來一樣啦!
所以做法是,將父親節點P改成黑色,將兄弟節點S改成紅色,然后刪除D即可。如下圖:
情況5:父親節點p,兄弟節點s和兄弟節點的兩個孩子(只能為NULL節點)都為黑色的情況
方法是將兄弟節點S的顏色改成紅色,這樣刪除D后P的左右兩支的黑節點數就相等了,但是經過P的路徑上的黑色節點數會少1,這個時候,我們再以P為起始點,繼續根據情況進行平衡操作(這句話的意思就是把P當成D(只是不要再刪除P了),再看是那種情況,再進行對應的調整,這樣一直向上,直到新的起始點為根節點)。結果如下圖:
至此,所有的情況都討論完了。我們稍稍總結一下,然后開始時寫代碼
我這里總結的是如何判斷是那種類型,至於特定類型的處理方法,就找前面的內容就好。
記住一句話:判斷類型的時候,先看待刪除的節點的顏色,再看兄弟節點的顏色,再看侄子節點的顏色(侄子節點先看遠侄子再看近侄子),最后看父親節點的顏色。把握好這一點,寫代碼思路就清晰了。
流程圖如下(忽略了處理過程)
開始寫代碼啦
節點的數據結構
//定義節點的顏色 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 free_node(PBNode *node) { if(*node == NULL) return; free(*node); *node = NULL; }
后面是與刪除有關的函數,我們由易到難,先小后大進行處理。
首先,我們先寫一個刪除節點的函數:
/** * 刪除一個節點 * 其中root為整棵樹的根結點 * d為待刪除的節點,或者新的起始點 * */ void delete_node(PBNode *root,PBNode d) { PBNode p = d->parent;//父親節點 if(p == NULL)//說明d就是樹根 { free_node(root); return; } if(p->l_tree == d) p->l_tree = NULL; else if(p->r_tree == d) p->r_tree = NULL; free_node(&d); }
刪除紅色節點的情況非常簡單,只需要刪除節點就行,所以直接調用刪除函數即可。
/** * 刪除紅色節點 * */ void delete_d_red(PBNode *root,PBNode d) { delete_node(root,d); } 刪除黑色節點的情況比較復雜,我們先處理小的模塊: 黑色節點非葉子節點 /** * 黑色節點不是葉子節點,這時候它只有一個孩子,且孩子的顏色為紅色 * * */ void delete_d_black_not_leaf(PBNode *root,PBNode d) { PBNode dl_r; if(d->l_tree != NULL) { dl_r = d->l_tree; } else if(d->r_tree != NULL) { dl_r = d->r_tree; } else { printf("節點有問題!\n"); return; } dl_r->color = BLACK; PBNode p = d->parent;//父親節點 if(p == NULL)//說明是整棵樹的樹根 { *root = dl_r; } else { if(p->l_tree == d) { p->l_tree = dl_r; } else if(p->r_tree == d) { p->r_tree = dl_r; } } //別忘了修改父親節點 dl_r->parent = p; free_node(&d); }
刪除黑色葉子節點是最復雜的一種情況,這種情況總體要再循環中進行,循環結束條件為:新的起始節點為根節點。當然,如果循環中某種類型變換完成后,可以確定整棵樹都滿足紅黑樹,循環也就結束了。
D為葉子節點且兄弟節點為紅色的情況(也就是情況1):
這種情況涉及RR型變換和RL型變換,所以我們先寫一個函數用來處理RR型和RL型變換。
/** * RR類型和LL類型的變換 * */ void avl_trans(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_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; } //更改small、middle、big的父親節點 middle->parent = ch_root->parent; big->parent = middle; small->parent = middle; }
有了這兩個變換的函數后,對於兄弟節點為紅色的這種情況,處理起來就很簡單了。
/** * D為黑色,S為紅色的情況 * 也就是情況1 * 將其類型變換成D為黑色,S也為黑色的情況 * */ void delete_black_case1(PBNode *root,PBNode d) { PBNode p = d->parent;//父親節點 if(p->l_tree == d)//d為左子的情況 { PBNode s = p->r_tree; p->color = RED;//父親節點變成紅色 s->color = BLACK;//兄弟節點變成黑色 avl_trans(root,p,TYPE_RR); } else if(p->r_tree == d)//d為右子的情況 { PBNode s = p->l_tree; p->color = RED;//父親節點變成紅色 s->color = BLACK;//兄弟節點變成黑色 avl_trans(root,p,TYPE_LL); } }
S為黑色,遠侄子節點為紅色的情況(也就是情況2):
/** * D為黑色,S為黑色,遠侄子節點為紅色 * 也就是情況2 * */ void delete_black_case2(PBNode *root,PBNode d) { PBNode p = d->parent;//父親節點 if(p->l_tree == d)//d為左孩子的情況 { PBNode s = p->r_tree;//兄弟節點 //交換父親姐弟和兄弟節點的顏色 enum color temp = p->color; p->color = s->color; s->color = temp; PBNode far_nephew = s->r_tree;//遠侄子節點 far_nephew->color = BLACK;//將遠侄子節點的顏色變成黑色 avl_trans(root,p,TYPE_RR);//進行類似AVL樹RR類型的轉換 } else if(p->r_tree == d)//d為右孩子的情況o { PBNode s = p->l_tree;//兄弟節點 //交換父親姐弟和兄弟節點的顏色 enum color temp = p->color; p->color = s->color; s->color = temp; PBNode far_nephew = s->l_tree;//遠侄子節點 far_nephew->color = BLACK;//將遠侄子節點的顏色變成黑色 avl_trans(root,p,TYPE_LL);//進行類似AVL樹LL類型的轉換 } }
D為黑色,S為黑色,遠侄子為黑色,近侄子為紅色的情況(也就是情況3)
這種情況涉及節點的左旋和右旋操作,所以寫一個函數處理節點的旋轉
/** * 處理左旋和右旋操作 * */ void node_rotate(PBNode to_rotate,enum rotate_type type) { PBNode p = to_rotate->parent;//父親節點 PBNode g = p->parent;//祖父節點 int t = type; switch(t) { case TURN_RIGHT: { g->r_tree = to_rotate; p->l_tree = to_rotate->r_tree; to_rotate->r_tree = p; break; } case TURN_LEFT: { g->l_tree = to_rotate; p->r_tree = to_rotate->l_tree; to_rotate->l_tree = p; break; } } //別忘了更改父親節點 to_rotate->parent = g; p->parent = to_rotate; }
有了旋轉操作,剩下的就只有顏色變換了。
/** * D為黑色,S為黑色,遠侄子為黑色,近侄子為紅色 * 也就是情況3 * 通過旋轉近侄子節點,和相關顏色變換,使情況3變成情況2 * */ void delete_black_case3(PBNode d) { PBNode p = d->parent;//父親節點 if(p->l_tree == d)//d為左孩子的情況 { PBNode s = p->r_tree; PBNode near_nephew = s->l_tree; s->color = RED; near_nephew->color = BLACK; node_rotate(near_nephew,TURN_RIGHT); } else if(p->r_tree == d) { PBNode s = p->l_tree; PBNode near_nephew = s->r_tree; s->color = RED; near_nephew->color = BLACK; node_rotate(near_nephew,TURN_LEFT); } }
父親節p為紅色,兄弟節點和兄弟節點的兩個孩子(只能是NULL節點)都為黑色的情況,也就是情況4
這種情況比較簡單,只涉及顏色的改變
/** * D為黑色,S為黑色,遠侄子為黑色,近侄子為黑色,父親為紅色 * 也就是情況4 * */ void delete_black_case4(PBNode d) { PBNode p = d->parent;//父親節點 if(p->l_tree == d)//d為左孩子 { PBNode s = p->r_tree; s->color = RED; } else if(p->r_tree == d)//d為左孩子 { PBNode s = p->l_tree; s->color = RED; } p->color = BLACK; }
父親節點p,兄弟節點s和兄弟節點的兩個孩子(只能為NULL節點)都為黑色的情況,也就是情況5
這種情況也比較簡單,就是將S的顏色變成紅色,將起始點有d變成p即可
/** * D,S,P,SL,SR都為黑色的情況 * 也就是情況5 * */ PBNode delete_black_case5(PBNode d) { PBNode p = d->parent;//父親節點 if(p->l_tree == d)//d為左孩子 { PBNode s = p->r_tree; s->color = RED; } if(p->r_tree == d)//d為左孩子 { PBNode s = p->l_tree; s->color = RED; } return p; }
最后要寫一個串聯函數,將刪除黑色葉子節點的各個函數串聯起來,這個串聯函數中有循環,循環結束條件是新的起始點為根節點,但是由於情況1-4,處理結束后,整棵樹就是紅黑樹了,此時可以用break退出循環。
/** * 刪除黑色葉子節點的函數,會將上面的多個函數串連起來 * */ void delete_d_black_leaf(PBNode *root,PBNode d) { PBNode begin = d;//起始節點 while(begin != *root) { PBNode p = begin->parent;//父親節點 if(p->l_tree == begin)//d為左孩子 { PBNode s = p->r_tree;//兄弟節點 if(s->color == RED)//情況1 { delete_black_case1(root,begin); continue; } PBNode sl = s->l_tree;//近侄子 PBNode sr = s->r_tree;//遠侄子 if(sr != NULL && sr->color == RED)//情況2 { delete_black_case2(root,begin); break; } if(sl != NULL && sl->color == RED)//情況3 { delete_black_case3(begin); continue; } if(p->color == RED)//情況4 { delete_black_case4(begin); break; } //情況5 begin = delete_black_case5(begin);//起始點要變換 continue; } else if(p->r_tree == begin)//d為左孩子 { PBNode s = p->l_tree;//兄弟節點 if(s->color == RED)//情況1 { delete_black_case1(root,begin); continue; } PBNode sl = s->l_tree;//遠侄子 PBNode sr = s->r_tree;//近侄子 //一定要先看遠侄子,再看近侄子 if(sl != NULL && sl->color == RED)//情況2 { delete_black_case2(root,begin); break; } if(sr != NULL && sr->color == RED)//情況3 { delete_black_case3(begin); continue; } if(p->color == RED)//情況4 { delete_black_case4(begin); break; } //情況5 begin = delete_black_case5(begin);//起始點要變換 continue; } } //循環退出后,刪除d delete_node(root,d); }
最后寫一個函數將刪除紅色節點、黑色非葉子節點和黑色葉子節點的函數合並到一起,此外還要根據二叉排序樹的要求處理有兩個孩子的節點
/** * 刪除節點函數,並進行調整,保證調整后任是一棵紅黑樹 * */ void delete_brt_node(PBNode *root,int value) { //找到該節點 PBNode node = get_node(*root,value); if(node == NULL) return; int tag = 0; while(tag != 2) { if(node->l_tree == NULL) { PBNode r = node->r_tree; if(r == NULL)//為葉子節點的情況 { if(node->color == RED) { delete_d_red(root,node); } else { delete_d_black_leaf(root,node); } break; } else//只有右子樹的情況 { delete_d_black_not_leaf(root,node); break; } } else if(node->r_tree == NULL)//只有左子樹的情況 { delete_d_black_not_leaf(root,node); break; } else//既有左孩子又有右孩子 { //找到后繼節點 PBNode y = node->r_tree; while(y->l_tree != NULL) { y = y->l_tree; } //用后繼節點和該節點進行值交換 int temp = node->value; node->value = y->value; y->value = temp; node = y;//待刪除的節點轉換成后繼節點 tag ++; } } }
最后附上word文檔和源代碼文件
鏈接:http://pan.baidu.com/s/1nvQI2iX 密碼:16nd
那個brt2.c是包含添加和刪除節點的
如果你覺得對你有用,請點個贊吧~~~光圖都畫了好長時間~~