前面的文章咱們按部就班的講解了《二叉樹》《二分搜索樹》《AVL-平衡二叉樹》,從左至右互爲基礎。尤爲是二分搜索樹給了咱們如何將數據組織成爲搜索樹的思想,固然二分搜索樹存在的自然問題--在極端狀況下回退化爲鏈表。因此引出了AVL-平衡二叉樹,經過再平衡即LL,LR,RR,RL四個旋轉操做維護了一棵平衡的二分搜索樹。本章節咱們繼續梳理一個高階的樹結構即:紅黑樹。想必你們都知道,紅黑樹如何維持平衡,如何進行顏色反轉讓人很難理解,雖然不少博文不少書對紅黑樹都有講解,可是想要掌握或者精通紅黑樹依然讓你們望而生畏。本文,咱們借鑑《算法-4》對紅黑樹的分析,從2-3樹入手來理解紅黑樹。至於爲何從2-3樹入手去理解紅黑樹是有緣由的,就像達爾文的進化論,任何一個物種都不會是從石頭中蹦出來的同樣。數據結構的發展一樣遵循着生物進化的理論。紅黑樹正是從2-3樹進化來的一種樹結構。html
後面會持續更新數據結構相關的博文。java
數據結構專欄:https://www.cnblogs.com/hello-shf/category/1519192.htmlnode
git傳送門:https://github.com/hello-shf/data-structure.gitgit
2-3樹相似於一棵完美二叉樹(滿二叉樹),不過就是2-3樹容許一個節點有三個孩子,正如其名字同樣。2-3樹爲了維持這種完美的平衡性的願景。具有以下要求:github
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樹的插入操做好好體會。ide
本章節從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樹的插入過程結合紅黑樹的性質,看看紅黑樹的旋轉和變色過程。
前面詳細介紹了2-3樹的分裂和融合過程,本小節,咱們來看看2-3樹向紅黑樹轉換的過程。2-3樹有兩類節點,1節點和2節點。還有一個臨時的節點3節點。下面看看2-3樹的這三種節點對應於紅黑樹的節點狀況。
如上所示:
1節點:對應於紅黑樹的黑色節點。
2節點:對應於紅黑樹黑色的父節點和紅色的左孩子節點。
3節點:對應於紅色的父節點和黑色的左右孩子節點。這裏須要說一下,爲何是紅色的父節點而不是黑色的呢?主要是由於2-3樹的3節點須要將分裂後的父節點進行向上融合,紅色的符合咱們向紅黑樹中插入任何一個節點默認都是紅色的實現方式(後面會介紹)。若是該父節點是紅黑樹的根節點的話,那他確定須要變色,這一點就不屬於2-3樹向紅黑樹的變換規則了,而屬於紅黑樹的性質。
本節是本文的重點,前面介紹了這麼多關於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 }
5、紅黑樹在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