【演算法導論】紅/黑樹狀結構詳解之一(插入),導論紅黑
本文地址:http://blog.csdn.net/cyp331203/article/details/42677833
作者:苦_咖啡
歡迎轉載,但轉載請註明出處,否則將追究相應責任,謝謝!。
紅/黑樹狀結構是建立在二叉尋找樹的基礎之上的,關於二叉尋找樹可以參看【演算法導論】二叉搜尋樹的插入和刪除和【演算法導論】二叉樹的前中後序非遞迴遍曆實現。對於高度為h的二叉尋找樹而言,它的SEARCH、INSERT、DELETE、MINIMUM、MAXIMUM等操作的時間複雜度均為O(h)。所以在二叉尋找樹的高度較高時,上述操作會比較費時,而紅/黑樹狀結構就可以解決這種問題。紅/黑樹狀結構是許多平衡搜尋樹中的一種,可以保證在最壞的情況下基本動態集合操作的時間複雜度為O(logn),n為節點個數。
一、什麼是紅/黑樹狀結構
紅/黑樹狀結構是一顆二叉尋找樹,所不同的是,它的每個節點上增加了一個儲存位來表示節點的顏色,可以是RED或BLACK。紅/黑樹狀結構的每個節點包含5個屬性:color、key、left、right、和p(父節點)。如果一個節點沒有子節點或父節點,在二叉尋找樹中,相應的指標就會指向NULL(空),而這裡就是紅/黑樹狀結構與二叉尋找樹的第二個不同之處:
紅/黑樹狀結構中並沒有任何一個節點的左子節點、右子節點或者父節點會指向NULL(空),取而代之的是使用一個nil的哨兵節點來放在原來為NULL的位置,類似於【演算法導論】10.2不帶哨兵節點和帶哨兵節點的雙向鏈表文中帶哨兵節點的雙向鏈表。在紅/黑樹狀結構中,哨兵節點nil是所有一個與樹種普通節點有相同屬性的對象。它的color屬性為BLACK,而它的其他屬性可以任意設定,一般時沒有什麼意義。nil用來替換二叉尋找樹中原本為NULL的指標的指向,同時root節點的父指標指向nil。我們將除nil節點的其他節點視為內部節點,在紅/黑樹狀結構中,我們主要關注內部節點。
紅/黑樹狀結構結構圖:
紅/黑樹狀結構的五條性質:
紅/黑樹狀結構,通過對任何一條從根到葉子的簡單路徑上各個節點的顏色進行約束,紅/黑樹狀結構可以保證沒有一條路徑會比其他路徑長出2倍以上,所以稱之為平衡。為了保持平衡,紅/黑樹狀結構要滿足以下五條性質:
1、每個節點不是紅色就是黑色的。
2、根結點是黑色的。
3、每個分葉節點(nil)是黑色的,實際上nil只有一個。
4、如果一個節點是紅色的,則它的兩個子節點都是黑色的。
5、對每個節點,從該節點到其所有後代葉結點的簡單路徑上,均包含相同數目的黑色節點。
對於上面的第5點,又有一個新的定義,黑高:從某個節點x出發(不含該節點)到達一個葉結點的任意一條簡單路徑上的黑色節點個數稱之為該節點的黑高,注意黑高是將nil計算在內的。
將的黑高標識出來:
紅/黑樹狀結構如何保證其效能O(lgn)?
證明:紅/黑樹狀結構的高度為O(lgn)
實際上,一顆有n個內部節點的紅/黑樹狀結構的高度至多為2lg(n+1)。我們設一個節點x的黑高為hb(x)。
先來證明以任一節點x為根的子樹中至少包含2^(hb(x)-1) -1個內部節點,使用歸納法:
①如果x的高度為0,那麼x是葉結點(nil),且以x為根結點的子樹至少包含2^(1-1) -1=0個內部節點,符合結論。
②假設x的高度為k時(k>=1),以x為根節點的子樹至少包含2^(hb(x)-1)-1個內部節點。
③對於x的高度為k+1的紅/黑樹狀結構,我們考慮它的兩個子樹,它的兩個子樹的高度為k,那麼滿足②號條件,兩個子樹至少包含了2^(hb(x)-1)-1個內部節點,所以以x為根的樹至少包含了(2^(hb(x)-1)-1)+(2^(hb(x)-1)-1)+1=2^(hb(x))-1個內部節點,因此得證。
對於高度為h的紅/黑樹狀結構,根據性質4我們不難發現在每一條路徑上,至少有一半以上的黑色節點,否則必定有兩個紅色節點會相鄰。所以樹的黑高至少為h/2。所以根據上面已證明的結論,有n>=2(h/2 - 1)-1,所以有:h<=2lg(n+1)。得證。
根據這個證明,我們由二叉搜尋樹的字典操作的時間複雜度為O(h),而紅/黑樹狀結構的高度為O(lgn)可知,紅/黑樹狀結構的字典操作的時間複雜度應為O(lgn)。所以紅/黑樹狀結構有比二叉搜尋樹更優的效能。
紅/黑樹狀結構的單分支情況
這裡需要注意的是,由於紅/黑樹狀結構的性質的限制,對於某些情況的單分支是不可能出現的。
1、可能出現的單分支:
因為只有在上面的兩種情況下才有可能在單分支的情況下,保持上面的第五條性質(注意nil節點)。而下面的幾種情況,都是不能保證第五條性質的單分支情況。
2、不可能出現的單分支情況:
因為上述四中但分支情況下,不能保證性質5。例如前兩種,必定會讓紅色節點->葉子節點(nil)線路的黑色節點數比紅色節點->黑色節點線路的黑色節點數少一。所以,我們容易發現,紅/黑樹狀結構種只可能有雙分支或黑上紅下的單分支情況。
二、左旋和右旋操作
旋轉操作是後序很多紅/黑樹狀結構操作必不可少的部分,旋轉操作可以保持二叉搜尋樹性質的搜尋樹局部操作。
左旋和右旋(都是針對上面的那個節點):
通過,我們很容易發現,旋轉之後,還是可以保持二叉搜尋樹的性質。但是卻不能保證紅/黑樹狀結構的性質,例如右圖,如果是21節點是單分支的情況(a節點=nil),就一定不會滿足第五條性質。
我們可以比較容易的給出左旋和右旋的代碼:
左旋:
/* * 以x為支點進行左旋 */void left_rotate(node* x) {if (x == nil_node) {return;}node* y = x->r_child;x->r_child = y->l_child; //讓y的左節點接到x的右節點位置,這樣能滿足二叉搜尋樹的性質if (y->l_child != nil_node) { //這一句判斷不能少y->l_child->parent = x; //讓左節點的父指標指向x}y->parent = x->parent;if (x->parent == nil_node) { //x原本為根結點的情況this->root = y;} else if (x == x->parent->r_child) {y->parent->r_child = y;} else {y->parent->l_child = y;}//處理左節點y->l_child = x;x->parent = y;}
右旋:
/* * 以x為支點進行右旋 */void right_rotate(node* x) {if (x == nil_node) {return;}node* y = x->l_child;x->l_child = y->r_child;x->l_child->parent = x;y->parent = x->parent;if (x->parent == nil_node) { //x原本為根結點的情況this->root = y;} else if (x == x->parent->l_child) {y->parent->l_child = y;} else {y->parent->r_child = y;}y->r_child = x;x->parent = y;}
我們容易發現,旋轉操作,不論是左旋還是右旋,時間複雜度都是O(1)。
三、紅/黑樹狀結構的插入操作
之前有提到過,二叉搜尋樹的字典操作的時間複雜度為O(h),而紅/黑樹狀結構的操作卻可以在O(lgn)_內完成。為了做到這一點,我們肯定需要在插入之後進行一些節點的調整,讓其滿足紅/黑樹狀結構的性質。所以在完成插入之後,還需要對樹進行調整,對節點重新著色,並旋轉。
紅/黑樹狀結構的節點插入可以分為插入過程和調整過程;其中插入過程與二叉搜尋樹差不太多。只是注意搜尋樹中為NULL的地方,現在被替換成nil哨兵節點,其次插入的節點我們總是將其著色為紅色。
插入的代碼:
void insert(int k) {node* n = new node(k);node* y = nil_node;node* x = root;while (x != nil_node) { //尋找合適的插入位置,這裡主要是滿足二叉搜尋樹的性質y = x; //y來記錄x的位置if (x->key > k) {x = x->l_child;} else {x = x->r_child;}}n->parent = y; //找到了合適的插入位置,y為這個位置的父節點if (y == nil_node) { //為空白樹的情況root = n;} else if (y->key > n->key) { //左節點y->l_child = n;} else {y->r_child = n; //右節點}//處理niln->l_child = nil_node;n->r_child = nil_node;//處理插入節點的顏色n->color = RED;//調整操作rb_insert_fixup(n);}
下面是重頭戲,我們來看一看,rb_insert_fixup這個調整函數如何?。
要調整,我們首先得明白要調整什麼,即我們之前的插入操作,會破壞紅/黑樹狀結構的哪些性質?我們再來看看這5條性質:
1、每個節點不是紅色就是黑色的。
2、根結點是黑色的。
3、每個分葉節點(nil)是黑色的,實際上nil只有一個。
4、如果一個節點是紅色的,則它的兩個子節點都是黑色的。
5、對每個節點,從該節點到其所有後代葉結點的簡單路徑上,均包含相同數目的黑色節點。
其中1、3性質肯定是滿足的,而對於5性質,由於我們插入的節點都染紅色了,所以不存在多出黑色節點的情況,所以5性質也可以滿足。可能出現問題的就是2、4性質,對於2號性質,如果插入的是一顆空樹(只有nil節點),那麼根結點就是紅的,就不滿足2性質。如果插入的節點的父節點是紅色,而我們插入的也是一個紅色的節點,那麼就不滿足4性質了。
處理性質2:
對於2號性質,其實很好解決,我們只需要在調整函數最後加上一句root->color=BLACK即可,將根結點染黑。
處理性質4:
而對於4號性質,出現問題的是父節點為紅色的情況,這時待調整節點和父節點都為紅,不滿足4性質,於是我們需要調整的判斷條件就是父節點如果為紅,就需要繼續調整。這裡實際上就要分三種情況,首先都已經預設z的父節點z->p的顏色為紅色,迴圈條件為while(z->color==RED):
情況一、z的叔節點y是紅色,其中z為要調整的節點(下同):
' 由於z的父節點z->p和叔節點都是紅色,所以z的祖父節點z->p->p的顏色一定是黑色,這是因為插入之前是一顆“完好”的紅/黑樹狀結構。
對於這種情況,處理辦法是:將z的父節點和叔節點都染黑,而將z的祖父節點染紅,這樣做了之後,我們仔細思考一下,發現其實這樣對當前樹的走父節點或走叔節點的路徑的黑節點數沒有影響。然後這時我們再將z點指向z的祖父節點位置,然後繼續進入迴圈;
情況二、z的叔節點y是黑色的且z是一個右孩子
這裡的做法就是將二情況轉換成三情況,當然就要用到旋轉,但是我們又不希望z的位置發現變化,所以這裡先讓z=z->p,然後再以z為支點進行左旋,因為左旋會讓z下降一級,所以實際上z還是指向的原來那一層的節點,z->p->p的位置還是沒有變。
情況三、z的叔節點y是黑色的且z是一個左孩子
這時實際上是z和z->p都是紅色,z->p->p和z.->p->right都是黑色,我們想做的就是讓z和z->p之間多個黑節點,這樣就能滿足性質4,但是同時我們又希望保持性質5。於是我們這樣處理:
z->p染黑,z->p->p染紅,然後以z->p->p為支點右旋,這樣就搞定了,
經過上面分別對性質2和性質4三種情況處理的分析,不難給出實現代碼:
/* * 修複顏色,因為插入的點,我們總是將其先染成紅色,所以如果父節點為黑色則不需要修複,如果父節點為紅色,則需要修複 * 修複顏色分三種情況: * ①當前插入點的父親為紅色,且祖父節點的另一個節點為也為紅色,且父節點為祖父節點的左子節點 * ②當前插入點的父節點為紅色,且祖父節點的另一個節點為黑色,且本節點為父結點的右子節點 * ③當前插入點的父節點為紅色,且祖父節點的另一個節點為黑色,且本節點為父結點的左子節點 */void rb_insert_fixup(node* z) {while (z->parent->color == RED) {if (z->parent == z->parent->parent->l_child) { //如果z的父節點是祖父節點的左子節點node* y = z->parent->parent->r_child;if (y->color == RED) {//情況1處理性質4z->parent->color = BLACK;//情況1z->parent->parent->r_child->color = BLACK;//情況1z->parent->parent->color = RED;//情況1z = z->parent->parent;//情況1} else if (z == z->parent->r_child) {//情況2z = z->parent;//情況2left_rotate(z);//情況2} else {//情況3z->parent->color = BLACK;//情況3z->parent->parent->color = RED;//情況3right_rotate(z->parent->parent);//情況3}} else { //當父節點是祖父節點的右孩子的情況,與上面差別在於將所有的left操作和節點改成對應的right的node* y = z->parent->parent->l_child;if (y->color == RED) {z->parent->color = BLACK;z->parent->parent->l_child->color = BLACK;z->parent->parent->color = RED;z = z->parent->parent;} else if (z->parent == z->parent->parent->l_child) {z = z->parent;right_rotate(z);} else {z->parent->color = BLACK;z->parent->parent->color = RED;left_rotate(z->parent->parent);}}}root->color = BLACK;//處理性質2}
四、插入操作的演算法分析
通過觀察,不難發現,由於有n個節點的紅/黑樹狀結構的高度為O(lgn),因此插入過程的時間複雜度為O(lgn);而對於調整過程rb_insert_fixup而言,只有情況1讓z上升兩層,有可能讓while一直發生,但是由於樹高的限制,時間複雜度也只可能在O(lgn),而對於情況2和情況3,程式所做的旋轉不會超過兩次。因為只要執行到情況2或者3,while迴圈就將要結束了。所以整個節點插入的時間複雜度為O(lgn)。
下一篇將是紅/黑樹狀結構的刪除節點,敬請關注。