預備知識
二叉樹:二叉樹是每個結點最多有兩個子樹的樹結構;通常子樹被稱作“左子樹”(left subtree)和“右子樹”(right subtree);二叉樹常被用於實現二叉查找樹和二叉堆;
平衡二叉樹:又被稱為AVL樹(有別於AVL算法),且具有以下性質:它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹。平衡二叉樹的常用實現方法有紅黑樹、AVL、替罪羊樹、Treap、伸展樹等;
排序二叉樹:任何節點的鍵值一定大於其左子樹中的每一個節點的鍵值,並小於其右子樹中的每一個節點的鍵值;
紅黑樹:
紅黑樹在原有的排序二叉樹增加了如下幾個要求:
性質 1:每個節點要么是紅色,要么是黑色。
性質 2:根節點永遠是黑色的。
性質 3:所有的葉節點都是空節點(即 null,實際上是不存在的節點),並且是黑色的。
性質 4:每個紅色節點的兩個子節點都是黑色。(從每個葉子到根的路徑上不會有兩個連續的紅色節點)
性質 5:從任一節點到其子樹中每個葉子節點的路徑都包含相同數量的黑色節點。
紅黑樹除了具有排序二叉樹特性也屬於平衡二叉樹,但不是嚴格的平衡二叉樹,說它不嚴格是因為它不是嚴格控制左、右子樹高度或節點數之差小於等於1,但紅黑樹高度依然是平均log(n),且最壞情況高度不會超過2log(n),這有數學證明。所以它算平衡樹。
紅黑樹的自我調整
在樹的結構發生改變時(插入或者刪除操作),往往會破壞上述條件4或條件5,需要通過調整使得查找樹重新滿足紅黑樹的條件;需要通過調整使得查找樹重新滿足紅黑樹的條件。
調整可以分為兩類:一類是顏色調整,即改變某個節點的顏色;另一類是結構調整,集改變檢索樹的結構關系。結構調整過程包含兩個基本操作:左旋(RotateLeft),右旋(RotateRight)。
左旋
左旋的過程是將x
的右子樹繞x
逆時針旋轉,使得x
的右子樹成為x
的父親(x成為其右子樹的左節點),同時修改相關節點的引用。旋轉之后,二叉查找樹的屬性仍然滿足。
如下,3--->9--->10這個鏈便是以3節點為當前節點,3節點的右子樹9--->10逆時針左旋的效果,最后3成了9的左節點,3、9、10達成了平衡
右旋
右旋的過程是將x
的左子樹繞x
順時針旋轉,使得x
的左子樹成為x
的父親(x成為其左子樹的右節點),同時修改相關節點的引用。旋轉之后,二叉查找樹的屬性仍然滿足。
如下,3--->2--->1這個鏈便是以3節點為當前節點,3節點的左子樹2--->1順時針時針右旋的效果,最后3成了2的右節點,3、2、1達成了平衡;
增加節點場景分析
增加節點后有哪些場景是需要調整的?
在一個已有的紅黑樹中增加一個新節點,假設3為祖父節點,2或4為父(叔)節點,新加入的節點為當前節點,當前新增節點作為2或4的子節點;可以用排除法來一個個排除,場景如下:
- 如果3是紅節點,那么只可能出現一種情況,2、4同時存在且都是黑色節點(3不可能有紅色子節點,黑色子節點不能只出現一個),2和4都為黑色節點的情況新加入的節點無論是2還是4的子節點都不影響原有紅黑樹的性質,不需要調整,排除
- 再看,如果3節點為黑色,2或者4節點為也為黑色,那么2、4節點必須同時存在,不可能存在3只有一個黑色子節點的情況(違背了性質5);而3、2、4都為黑色節點的情況新加入的節點無論是2還是4的子節點都不影響原有紅黑樹的性質,不需要調整,排除
- 其實現在只剩下3是黑色節點,2或4均為紅色節點,這種有分為只有2節點、只有4節點、2和4同時存在,其實反過來思考,正是因為有這三種情況的存在,每當新加入一個紅色節點后會引起紅黑樹的自我調整,調整結束后就不會有能引起調整的條件了。
綜合分析,一顆存在紅黑樹本身處於相對穩定狀態(沒有外力能觸發調整),穩定的紅黑樹中只會存在上述無影響和待調整的結構圖,不會出現不存在的結構圖;而無影響的結構圖新增節點並不破壞紅黑樹特性,所以待處理的就剩下待調整的三種了,接下來分析這三種。
注意,看圖時注意結構,不要盯着純數字,因為不同場景,數字代表的節點含義不一樣
場景一:祖父節點黑色、父節點為祖父節點的左節點、無叔叔節點
這種情況,如果是孫節點作為父節點的右節點加入,對應1.1開始;如果孫節點作為父節點的左節點加入,對應1.2開始;
- 當前節點:2
- 當前節點的父節點是紅色,叔叔節點是黑色,且當前節點是其父節點的右孩子
- 將“父節點”作為“新的當前節點”
- 以“新的當前節點”為支點進行左旋
- ----------------------------------分割線,如果從圖1.2開始,只有以下步驟------------------------------------
- 當前節點:1
- 當前節點的父節點是紅色,叔叔節點是黑色(不存在就是黑色,紅黑色性質三),且當前節點是其父節點的左孩子
- 將“父節點”設為“黑色”
- 將“祖父節點”設為“紅色”
- 以“祖父節點”為支點進行右旋
場景二:祖父節點黑色、父節點為祖父節點的右節點、無叔叔節點
這種情況,如果是孫節點作為父節點的左節點加入,對應2.1開始;如果孫節點作為父節點的右節點加入,對應2.2開始;
- 當前節點:4
- 當前節點的父節點是紅色,叔叔節點是黑色,且當前節點是其父節點的左孩子
- 將“父節點”作為“新的當前節點”
- 以“新的當前節點”為支點進行右旋
- -------------------------------------分割線,如果從圖2.2開始,只有以下步驟-------------------------------------------
- 當前節點:5
- 當前節點的父節點是紅色,叔叔節點是黑色,且當前節點是其父節點的右孩子
- 將“父節點”設為“黑色”
- 將“祖父節點”設為“紅色”
- 以“祖父節點”為支點進行左旋
場景三:祖父節點黑色、父節點紅色、叔叔節點紅色
這種情況,如果無論孫節點是作為父節點的左節點還是右節點,或者無論父節點是哪一個紅色節點,處理方式都是統一的
- 當前節點:1
- 當前節點的父節點是紅色,且當前節點的祖父節點的另一個子節點(叔叔節點)也是紅色
- 將“父節點”設為黑色
- 將“叔叔節點”設為黑色
- 將“祖父節點”設為“紅色
- 將“祖父節點”設為“當前節點”(紅色節點);即,之后繼續對“當前節點”進行操作
這種場景下,最后一步,祖父節點變成了紅色,而祖父節點的父節點可能之前也是紅色的,所以可能違背了紅黑樹性質,所以才會有最后一步將祖父節點設為“當前節點”,然后就成為了場景一和二的情況,這是一個遞歸的過程直到當前節點的父節點顏色是黑色。
參考java中TreeMap的實現,來看是否和上述分析一致
fixAfterInsertion是每次向TreeMap中新增節點后都會調用的修正方法,正式這個方法保證和紅黑樹的性質,與之對應的有fixAfterDeletion(刪除節點后調用)
看這個方法的邏輯,完全與上述分析一致。
1 private void fixAfterInsertion(Entry<K,V> x) { 2 // 新增節點都是紅色 3 x.color = RED; 4 5 // 遞歸處理,當前節點的父節點是紅色就要一直處理 6 while (x != null && x != root && x.parent.color == RED) { 7 // 父節點為祖父節點的左節點 8 if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { 9 Entry<K,V> y = rightOf(parentOf(parentOf(x))); 10 if (colorOf(y) == RED) { 11 // 叔叔節點為紅色,父、叔節點置黑,祖父節點置紅,以祖父節點為當前節點遞歸 12 setColor(parentOf(x), BLACK); 13 setColor(y, BLACK); 14 setColor(parentOf(parentOf(x)), RED); 15 x = parentOf(parentOf(x)); 16 } else { 17 if (x == rightOf(parentOf(x))) { 18 // 父節點左旋為祖父節點的右旋騰出位置(孫父祖三代處於同一斜率,依次為左子節點) 19 x = parentOf(x); 20 rotateLeft(x); 21 } 22 // 父節點置黑、祖父節點置紅、祖父節點右旋 23 setColor(parentOf(x), BLACK); 24 setColor(parentOf(parentOf(x)), RED); 25 rotateRight(parentOf(parentOf(x))); 26 } 27 } 28 // 父節點為祖父節點的右節點 29 else { 30 Entry<K,V> y = leftOf(parentOf(parentOf(x))); 31 if (colorOf(y) == RED) { 32 // 叔叔節點為紅色,父、叔節點置黑,祖父節點置紅,以祖父節點為當前節點遞歸 33 setColor(parentOf(x), BLACK); 34 setColor(y, BLACK); 35 setColor(parentOf(parentOf(x)), RED); 36 x = parentOf(parentOf(x)); 37 } else { 38 if (x == leftOf(parentOf(x))) { 39 // 父節點右旋為祖父節點的左旋騰出位置(孫父祖三代處於同一斜率,依次為右子節點) 40 x = parentOf(x); 41 rotateRight(x); 42 } 43 // 父節點置黑、祖父節點置紅、祖父節點左旋 44 setColor(parentOf(x), BLACK); 45 setColor(parentOf(parentOf(x)), RED); 46 rotateLeft(parentOf(parentOf(x))); 47 } 48 } 49 } 50 51 // 根節點置黑 52 root.color = BLACK; 53 }
思考
為甚麽紅黑樹中新增加的節點一定是紅色?
紅黑樹的5個性質中,性質4和性質5是比較容易違背的,為了盡量避免因為破壞紅黑樹的特性而做調整,每次新插入的節點都是紅色。因為插入之前所有根至外部節點的路徑上黑色節點數目都相同,如果插入的節點是黑色肯定錯誤(黑色節點數目不相同),而相對的插入紅節點可能會也可能不會違反“沒有連續兩個節點是紅色”這一條件,所以插入的節點為紅色代價相對小,如果違反條件再調整。
為什么基於“子節點-->父節點-->祖父節點”來調整紅黑樹的平衡?
在進行顏色變化或旋轉的時候,往往要涉及祖孫三代節點(X表示操作的基准節點,P代表X的父節點,G代表X的父節點的父節點);這是因為基於至少三個節點來旋轉調色可以盡量保持局部滿足紅黑樹的5個特性,這樣就能盡量不破壞整體特性;如果只有兩個節點子和父,就算知道破壞了紅黑樹的性質也沒法通過自我調整來達到效果,只有兩個節點旋轉來旋轉去也不平衡。
為什么場景一和場景二中,孫節點作為父節點的左節點和右節點處理場景不一樣?
拿場景一來說,如果不將下圖中的1和2節點進行一次左旋,那么3節點在右旋的時候2會成為3的左節點,不能同時保證性質4和5;假設紅黑樹只有三個節點,右旋之后1成為根節點,3是1的右節點,2是3的左節點;由於1是黑色且3只能為紅色,那么2節點為黑色破壞了性質5,2節點為紅色破壞了性質4;
有人問為什么不把3往左旋,其實仔細想想紅黑樹的默認排序規則,一個節點的值一定大於其左子樹小於其右子樹。因此,保證孫--->父--->祖三代節點都在同一斜率(依次為左子節點或依次為右子節點)可以同時滿足紅黑樹的五種性質並且盡可能保證紅黑樹的平衡。
刪除節點場景分析
待補充
PS:在線生成紅黑樹連接:https://sandbox.runjs.cn/show/2nngvn8w
參考: