通過2-3樹理解紅黑樹
一、簡介
前面的文章我們循序漸進的講解了《二叉樹》《二分搜尋樹》《AVL-平衡二叉樹》,從左至右互為基礎。尤其是二分搜尋樹給了我們如何將資料組織成為搜尋樹的思想,當然二分搜尋樹存在的天然問題--在極端情況下回退化為連結串列。所以引出了AVL-平衡二叉樹,通過再平衡即LL,LR,RR,RL四個旋轉操作維護了一棵平衡的二分搜尋樹。本章節我們繼續梳理一個高階的樹結構即:紅黑樹。想必大家都知道,紅黑樹如何維持平衡,如何進行顏色反轉讓人很難理解,雖然很多博文很多書對紅黑樹都有講解,但是想要掌握或者精通紅黑樹依然讓大家望而生畏。本文,我們借鑑《演算法-4》對紅黑樹的分析,從2-3樹入手來理解紅黑樹。至於為什麼從2-3樹入手去理解紅黑樹是有原因的,就像達爾文的進化論,任何一個物種都不會是從石頭中蹦出來的一樣。資料結構的發展同樣遵循著生物進化的理論。紅黑樹正是從2-3樹進化來的一種樹結構。
二、2-3樹
2.1、2-3樹的性質
2-3樹類似於一棵完美二叉樹(滿二叉樹),不過就是2-3樹允許一個節點有三個孩子,正如其名字一樣。2-3樹為了維持這種完美的平衡性的願景。具備如下要求:
1 2節點有且只能有兩個孩子節點,並只能包含一個數據項。 2 3節點必有三個孩子,並只能包含兩個資料項,從左至右依次遞增 3 插入節點時不能將該節點插入到一個空節點上,新的節點只能通過分裂或者融合產生
4 當2-3樹只有2節點的時候,其只能是一棵滿二叉樹(完美二叉樹)
我們通過圖解詳細理解一下以上三個性質。
2節點必有兩個孩子節點,並只能包含一個數據項:如圖1所示,2節點只能有且只能有2個孩子節點,5節點和8節點。並且2直接只能包含一個數據項即6;
3節點必有三個孩子,並只能包含兩個資料項,從左至右依次遞增:如圖2所示,5 < 6< 7< 8< 9;
插入節點時不能將該節點插入到一個空節點上,新的節點只能通過分裂或者融合產生:關於這一條的解釋,我們下面通過2-3樹的插入操作好好體會。
2.2、2-3樹的插入
本章節從2-3樹的插入理解“插入節點時不能將該節點插入到一個空節點上,新的節點只能通過分裂或者融合產生”這句話。
在前文《二分搜尋樹》的分析中我們可知,當我們依次插入(1, 2, 3, 4, 5, 6, 7, 8 ...)連續節點時,二分搜尋樹將退化為連結串列。本章節我們看看2-3樹是如何保持平衡的。
如上圖3所示,我們一次插入(1,2,3,4,5)五個元素。
插入元素1,建立一個2節點(元素為1)。
插入元素2,1,2元素融合暫時形成一個3節點。為什麼2元素不能生成一個節點作為1元素所在節點的右孩子?因為“插入節點時不能將該節點插入到一個空節點上,新的節點只能通過分裂或者融合產生”
插入元素3,1,2,3元素暫時融合形成一個4節點。
分裂,因為這是一棵2-3樹,不能存在4節點,所以暫時形成的4節點要進行分裂,將中間的元素作為根節點,左右兩個元素各為其左右孩子節點。這時可見形成了一棵滿二叉樹。
插入元素4,根據元素的大小關係其將會找到元素3所在的節點。因為新插入的節點不能插入到一個空節點上,所以4元素將根據搜尋樹的性質找到最後一個節點與其融合。即3,4元素將融合為一個三節點。並且4元素要位於3元素的右側。
插入元素5,同插入元素4,元素5一路查詢到3,4元素所在的三節點,與其融合,暫時形成一個4節點。
分裂,3,4,5元素所在的4節點同上面1,2,3元素形成的4節點一樣,進行分裂操作。根據大小關係,4元素將會作為根節點,3,5元素各為其左右孩子節點。
融合,前面的分裂操作已經導致該2-3樹不滿足其第四條性質“當2-3樹只有2節點的時候,其只能是一棵滿二叉樹(完美二叉樹)”,所以該2-3樹將要向上融合以滿足2-3樹的性質。我們只需要將4元素所在節點與其父節點即元素2所在的節點進行融合即可。這時,2,4元素就形成了一個3節點。
繼續插入,6,7元素,最終形成的2-3樹如上圖4所示。可見,2-3樹維護平衡的機制是如此的神奇。整個過程我們是一次插入了(1,2,3,4,5,6,7)7個元素,如果是二分搜尋樹,該二分搜尋樹將會退化為一個連結串列,但是2-3樹卻神奇的生成了一個滿二叉樹。
三,紅黑樹
前面惡補了2-3樹的知識,接下來我們就進入正題。
紅黑樹的定義:
1 每個節點或者是紅色的,或者是黑色的; 2 根節點是黑色的; 3 每個葉子結點(紅黑樹中葉子節點為最後的空節點)是黑色的; 4 如果一個節點是紅色的,那麼他的孩子都是黑色的; 5 從任意一個節點到葉子節點經過的黑色節點是一樣的。
上面的5點定義是建立在紅黑樹是一個二分搜尋樹的基礎上的。所以紅黑樹具備二分搜尋樹的所有性質。
看著定義是不是感覺無法理解紅黑樹?大部分的教材或者博文中對紅黑樹的講解都是生硬的根據這5條定義開始。真的是一頭霧水,不知所以然。接下來我們根據2-3樹的插入過程結合紅黑樹的性質,看看紅黑樹的旋轉和變色過程。
3.1、2-3樹到紅黑樹的轉換規則
前面詳細介紹了2-3樹的分裂和融合過程,本小節,我們來看看2-3樹向紅黑樹轉換的過程。2-3樹有兩類節點,1節點和2節點。還有一個臨時的節點3節點。下面看看2-3樹的這三種節點對應於紅黑樹的節點情況。
如上所示:
1節點:對應於紅黑樹的黑色節點。
2節點:對應於紅黑樹黑色的父節點和紅色的左孩子節點。
3節點:對應於紅色的父節點和黑色的左右孩子節點。這裡需要說一下,為什麼是紅色的父節點而不是黑色的呢?主要是因為2-3樹的3節點需要將分裂後的父節點進行向上融合,紅色的符合我們向紅黑樹中插入任何一個節點預設都是紅色的實現方式(後面會介紹)。如果該父節點是紅黑樹的根節點的話,那他肯定需要變色,這一點就不屬於2-3樹向紅黑樹的變換規則了,而屬於紅黑樹的性質。
3.2、紅黑樹的旋轉,變色和顏色反轉
本節是本文的重點,前面介紹了這麼多關於2-3樹的知識,都是為本節做鋪墊,如果螢幕前的你看到了這裡請繼續保持耐心。
下面我們向2-3樹和紅黑樹依次插入(1,2,3)三個元素來看看旋轉,變色和顏色反轉的過程。
在此之前我們需要清楚紅黑樹的一個性質:根節點必須為黑色的。一個實現紅黑樹的規則:新插入的節點永遠為紅色。
插入1:
如上所示,插入元素1。2-3樹就是一個1節點不需要做任何改變。根據紅黑樹新增的規則:新插入的節點為紅色,所以1元素的節點為紅色。根據紅黑樹的性質:根節點必須為黑色。1元素的節點需要進行變色。
插入2:
如上所示,插入元素2.2-3樹會形成一個2節點。根據2-3樹向紅黑樹變換的規則,需要變為2元素所在的節點為黑色的父節點,1元素所在的節點為紅色,併為2元素的左孩子節點。
左旋:但是根據二分搜尋樹的性質,插入的2元素會成為1元素的右孩子,這時我們需要對1元素進行左旋轉,然後得到如上左旋後的結果。
變色:這時再將2元素換成1元素的顏色,然後將1元素變為紅色。這樣的變色是有原因的,首先為了向上相容,該子樹的根節點需要始終保持原來的顏色,即將新的根節點2換成原來的根節點的顏色。其次根據2-3樹的2節點向紅黑樹轉換的規則,我們需要將1節點的顏色變為紅色。
關於左旋和變色程式碼實現如下
1 ////////////////////////////////////////// 2 // y x // 3 // / \ 左旋轉 / \ // 4 // T1 x ---------> y T3 // 5 // / \ / \ // 6 // T2 T3 T1 T2 // 7 ////////////////////////////////////////// 8 private Node leftRotate(Node y){ 9 Node x = y.right; 10 Node t2 = x.left; 11 12 y.right = t2; 13 x.left = y; 14 15 x.color = y.color;// 為了向上相容,將新的根節點變成老根節點的顏色 16 y.color = RED; // 將被旋轉的節點顏色置為紅色。 17 18 return x; 19 }
對於上面變色的過程大家可能會想為什麼變色過程是這樣的而不是根據紅黑樹的性質,將2元素變為黑色?如果是這樣,1,2都是黑色的。從根節點2觸發,到所有葉子節點鎖經過的黑色節點數就不對了。所以說,我們需要顏色交換而不是簡單地將父節點變黑。
插入3:
插入3:
如上所示,插入3元素,對於2-3樹會形成一個臨時的3節點,然後進行分裂。紅黑樹中新插入的節點都是紅色的,所以,這時會形成一個根節點為2。左右孩子都是紅色的節點。
顏色反轉:根據2-3樹向紅黑樹變化的規則,並不滿足。需要進行顏色反轉,即將1,3元素變為黑色,2元素變為紅色。至於為什麼這樣進行顏色反轉,原因很簡單,因為在2-3樹中,三節點分裂後需要向上融合。
變色:這時2元素為根節點,根據紅黑樹的性質,需要進行變色即可。假如2元素不是根節點,需要向上融合,如2.2章節描述。
顏色翻轉相關程式碼
1 /** 2 * 顏色翻轉 3 * @param node 4 */ 5 ///////////////////////////////////////// 6 // 黑 紅 // 7 // / \ ------> / \ // 8 // 紅 紅 黑 黑 // 9 //////////////////////////////////////// 10 private void flipColors(Node node){ 11 node.color = RED; // 置為紅色,為了向上融合,在2-3樹中,3節點分裂後的根節點要向上融合 12 node.left.color = BLACK; 13 node.right.color = BLACK; 14 }
上面我們詳細介紹了依次插入(1,2,3)三個元素的過程,並且這個過程詳細覆蓋了紅黑樹的旋左轉,變色,顏色反轉的過程。這時就差一種右旋的操作,是因為我們插入的資料的問題沒遇到右旋的情況,這時如果我們依次插入(1,2,3)就會遇到右旋的情況了。
先插入3,2元素,會形成上面的紅黑樹。
繼續插入1元素,這個變化的過程如上所示,詳細過程我們就不詳細介紹了。我們簡單說一下右旋的變色過程,旋轉之前,該子樹的根節點顏色為黑色,為了向上相容需要將新的根節點即2元素的顏色換成原來的根節點3元素的顏色,然後將3元素的顏色變成紅色。
右旋的過程如下程式碼所示
1 ////////////////////////////////////////// 2 // y x // 3 // / \ 右旋轉 / \ // 4 // x T2 -------> z y // 5 // / \ / \ // 6 // z T1 T1 T2 // 7 ////////////////////////////////////////// 8 private Node rightRotate(Node y){ 9 Node x = y.left; 10 Node t1 = x.right; 11 12 y.left = t1; 13 x.right = y; 14 15 x.color = y.color;// 為了向上相容,將新的根節點變成老根節點的顏色 16 y.color = RED;// 將被旋轉的節點顏色置為紅色。 17 18 return x; 19 }
總結一下上面依次插入(1,2,3)和(3,2,1)的過程,我們總結一個規律。
右旋:當一個節點的左孩子節點和左孩子的左孩子節點都是紅色的時候需要進行右旋。
左旋:當一個節點的右孩子是紅色節點並且左孩子不是紅色,進行左旋。
顏色反轉:當一個節點的左右孩子節點都是紅色的時候需要進行顏色反轉。
隨之延伸出來的一個性質即,在我們的2-3樹向紅黑樹變換的規則下,紅色的節點只能出現在一個黑色節點的左孩子處。
注意:以上我們總結的規律,只是建立在我們在2-3樹向紅黑樹變換的過程中,為什麼這麼說呢?因為當你查閱的資料多了你會發現,紅黑樹的實現是多種多樣的,只要能滿足紅黑樹的5點性質即可是一棵符合要求的紅黑樹。當然本文我們介紹的變換規則是主流的規則。也是維持紅黑樹的平衡性最好的一種變換規則。
如果你對左旋或者右旋具體是怎麼旋轉的請參閱我的另一篇博文《平衡二叉樹》中關於旋轉的過程,嚴格來說紅黑樹的旋轉較於平衡二叉樹只是多了一個顏色變化的過程,上面我們也有詳細的描述。
大家看程式碼一目瞭然。
感興趣的話,依次插入(1,2,3,4,5,6,7),手動畫一下如果能得到最終結果如下,說明你對紅黑樹的理解就沒什麼問題了。
四、紅黑樹的實現
關於程式碼實現,和平衡二叉樹的實現思想是一樣的,我們就不具體描述了。當然瞭如果你只是為了面試而閱讀本文,恭喜你你可以跳過本章節,基本沒有哪個公司會讓你手寫紅黑樹。但是手寫平衡二叉樹或者二分搜尋樹是有可能的。所以大家可以參閱筆者有關《二分搜尋樹》和《平衡二叉樹》的文章。
如果出於研究的目的你想具體實現,我相信你肯定是具備了平衡二叉樹的知識才來手撕紅黑樹的,所以當你具備了平衡二叉樹的知識,以下程式碼應該是沒啥難度的。
1 /** 2 * 描述:紅黑樹的實現 3 * 4 * @Author shf 5 * @Date 2019/8/1 9:42 6 * @Version V1.0 7 **/ 8 public class RBTree<K extends Comparable<K> , V>{ 9 private static final boolean RED = true; 10 private static final boolean BLACK = false; 11 private class Node{ 12 public K key; 13 public V value; 14 public Node left, right; 15 public boolean color; 16 public Node(K key, V value){ 17 this.key = key; 18 this.value = value; 19 left = null; 20 right = null; 21 color = RED; 22 } 23 @Override 24 public String toString(){ 25 return "key-->" + key + "== value-->" + value + "== color-->" + color; 26 } 27 } 28 private Node root; 29 private int size; 30 31 public RBTree(){ 32 root = null; 33 size = 0; 34 } 35 public int size(){ 36 return size; 37 } 38 public boolean isEmpty(){ 39 return size == 0; 40 } 41 private boolean isRed(Node node){ 42 if(node == null){ 43 return BLACK; 44 } 45 return node.color; 46 } 47 ////////////////////////////////////////// 48 // y x // 49 // / \ 左旋轉 / \ // 50 // T1 x ---------> y T3 // 51 // / \ / \ // 52 // T2 T3 T1 T2 // 53 ////////////////////////////////////////// 54 private Node leftRotate(Node y){ 55 Node x = y.right; 56 Node t2 = x.left; 57 58 y.right = t2; 59 x.left = y; 60 61 x.color = y.color; 62 y.color = RED; 63 64 return x; 65 } 66 ////////////////////////////////////////// 67 // y x // 68 // / \ 右旋轉 / \ // 69 // x T2 -------> z y // 70 // / \ / \ // 71 // z T1 T1 T2 // 72 ////////////////////////////////////////// 73 private Node rightRotate(Node y){ 74 Node x = y.left; 75 Node t1 = x.right; 76 77 y.left = t1; 78 x.right = y; 79 80 x.color = y.color; 81 y.color = RED; 82 83 return x; 84 } 85 private void flipColors(Node node){ 86 node.color = RED; 87 node.left.color = BLACK; 88 node.right.color = BLACK; 89 } 90 public void add(K key, V value){ 91 root = add(root, key, value); 92 root.color = BLACK; 93 } 94 private Node add(Node node, K key, V value){ 95 if(node == null){ 96 size ++; 97 return new Node(key, value); 98 } 99 if(key.compareTo(node.key) < 0){ 100 node.left = add(node.left, key, value); 101 } else if(key.compareTo(node.key) > 0){ 102 node.right = add(node.right, key, value); 103 } else { 104 node.value = value; 105 } 106 if(isRed(node.right) && !isRed(node.left)){ 107 node = leftRotate(node); 108 } 109 if(isRed(node.left) && isRed(node.left.left)){ 110 node = rightRotate(node); 111 } 112 if(isRed(node.left) && isRed(node.right)){ 113 flipColors(node); 114 } 115 return node; 116 } 117 public void levelOrder(){ 118 levelOrder(root); 119 } 120 private void levelOrder(Node node){ 121 Queue<Node> queue = new LinkedList<>(); 122 queue.add(node); 123 while(!queue.isEmpty()){ 124 Node cur = queue.remove(); 125 System.out.println(cur); 126 if(cur.left != null){ 127 queue.add(cur.left); 128 } 129 if(cur.right != null){ 130 queue.add(cur.right); 131 } 132 } 133 } 134 }
五、紅黑樹在java中的應用
在java的眾多集合類中,仔細研究大家可能會發現,TreeMap,TreeSet,都是紅黑樹的實現。本質上TreeMap是真正的紅黑樹的實現,TreeSet是對TreeMap的二次封裝。還有一個重要的集合,HashMap,對是他是他就是他,幾乎每次面試面試官都會死磕一遍HashMap,除此之外他也是我們日常開發工作中最常用的集合之一。感興趣的可以研究一下HashMap。在HashMap中當一個索引位置維護的連結串列長度超過8即轉換為紅黑樹,小於6從紅黑樹轉換為連結串列。為什麼是6,8而不是8,8主要是一個複雜度震盪的問題。
愛國,敬業
參考文獻:
《玩轉資料結構-從入門到進階-劉宇波》
《演算法4》
如有錯誤的地方還請留言指正。
原創不易,轉載請註明原文地址:https://www.cnblogs.com/hello-shf/p/11364565.html
&n