在瞭解紅黑樹以前,咱們先來認識2-3樹,在算法(第4版)裏也是先從2-3樹切入到紅黑樹的。而且瞭解2-3樹對於理解B類樹也會有幫助,由於2-3樹能夠說就是基礎的B類樹。java
2-3樹的特性:node
2-3樹爲了維持絕對平衡,須要知足如下條件:算法
2-3樹的兩類節點:數據結構
下圖是一顆完整的2-3樹:ide
從上圖中能夠看到2-3樹是知足二分搜索樹的基本性質的,只有兩個節點的狀況,如 42 這個節點,右子節點小於父節點,左子節點大於父節點。而有三個節點時,右子節點仍然小於父節點,中間的子節點大於父節點的左數據項,小於父節點的右數據項(如圖中18大於17,小於33),左子節點則大於父節點。性能
以前咱們提到了2-3樹插入節點時不能將該節點插入到一個空節點上,新的節點只能經過分裂或者融合產生。咱們知道對二分搜索樹依次添加有序的數據時,如依次添加 一、二、三、四、5,會產生連續的節點,使得二分搜索樹退化成鏈表。this
爲了不退化成鏈表,具備平衡特性的樹狀結構,會採起一些手段來維持樹的平衡,例如AVL是經過旋轉節點,而2-3樹則是經過分裂和融合。當咱們依次添加 一、二、三、四、5 到2-3樹時,其流程以下:3d
添加元素1,建立一個2節點類型的根節點code
添加元素2,此時元素1和2存在同一個節點中,成爲一個3節點。爲何添加元素2時,不能生成一個新的節點做爲元素1所在節點的右子節點呢?由於「添加數據項時不能將該數據項添加到一個空節點上,新的節點只能經過分裂或者融合產生」blog
添加元素3,元素一、二、3,暫時存在同一個節點中,造成一個4節點
分裂,2-3樹中最多隻有3節點,不能存在4節點,因此暫時造成的4節點要進行分裂,將中間的元素做爲根節點,左右兩個元素各爲其左右子節點。這時可見造成了一棵滿二叉樹
添加元素4,根據元素的大小關係,將會存放到元素3所在的節點。由於新添加的元素不能添加到一個空節點上,因此元素4將根據搜索樹的性質找到最後一個節點與其融合。即元素3和4將融合爲一個三節點。而且根據大小關係元素4要位於元素3的右側
添加元素5,同插入元素4,元素5一路查找到元素三、4所在的三節點,與其融合,暫時造成一個4節點
分裂,元素三、四、5所在的4節點同上面元素一、二、3造成的4節點同樣,進行分裂操做。根據大小關係,4元素將會做爲根節點,元素三、5則各爲其左右子節點
若是咱們繼續往2-3樹中添加元素6和7,那麼最終造成的2-3樹以下圖所示:
若是在這個案例中咱們使用的是二分搜索樹,那麼該二分搜索樹將會退化爲一個鏈表,而2-3樹則經過分裂、融合的方式成爲了一顆滿二叉樹。
瞭解了2-3樹後,咱們來看下紅黑樹與2-3樹的等價性,嚴格來講是左傾紅黑樹纔是與2-3樹是等價的。與2-3樹同樣,紅黑樹具備二分搜索樹的性質,而且也是自平衡的,但不是絕對平衡,甚至平衡性比AVL樹還要差一些。
以前提到了2-3樹是絕對平衡的,對於任意節點的左右子樹的高度必定是相等的。而AVL樹則是任意節點的左右子樹高度相差不超過 1 便可,屬於高度平衡的二分搜索樹。
紅黑樹則是從根節點到葉子節點的最長路徑不超過最短路徑的2倍,其高度僅僅只會比AVL樹高度大一倍,因此在性能上,降低得並很少。因爲紅黑樹也是自平衡的樹,也會採起一些機制來維持樹的平衡。
紅黑樹的定義:
這裏的第三點要求「每個葉子節點(最後的空節點)是黑色的」,稍微有些奇怪,它主要是爲了簡化紅黑樹的代碼實現而設置的。咱們也能夠理解爲,只要是空的節點,它就是黑色的。
下圖是一顆典型的紅黑樹:
在瞭解了2-3樹以後,咱們知道2-3樹是經過分裂和融合來產生新的節點並維持平衡的。2-3樹有兩類節點,2節點和3節點。除此以外,還會有一種臨時的4節點。接下來咱們看看2-3樹向紅黑樹轉換的過程,下圖展現了2-3樹的這三種節點對應於紅黑樹的節點:
根據這個對應關係,咱們將這樣一顆2-3樹:
轉換成紅黑樹,就是這樣子的,能夠看到其中的紅色節點都對應着2-3樹的3節點:
若是這樣看着不太好對應的話,咱們也能夠將其繪製成這個樣子,就更容易理解紅黑樹與2-3樹是等價的了:
從2-3樹過渡到紅黑樹後,接下來,咱們就着手實現一個紅黑樹。首先,編寫紅黑樹的基礎結構代碼,如節點定義等。具體代碼以下所示:
package tree; /** * 紅黑樹 * * @author 01 * @date 2021-01-22 **/ public class RBTree<K extends Comparable<K>, V> { /** * 由於只有紅色和黑色,這裏用兩個常量來表示 */ private static final boolean RED = true; private static final boolean BLACK = false; /** * 定義紅黑樹的節點結構 */ private class Node { public K key; public V value; public Node left, right; // 表示節點是紅色仍是黑色 public boolean color; public Node(K key, V value) { this.key = key; this.value = value; left = null; right = null; // 默認新節點都是紅色 color = RED; } } /** * 根節點 */ private Node root; /** * 紅黑樹中的元素個數 */ private int size; public RBTree() { root = null; size = 0; } public int getSize() { return size; } public boolean isEmpty() { return size == 0; } /** * 判斷節點node的顏色 */ private boolean isRed(Node node) { if (node == null) { // 空節點咱們都認爲是黑色的葉子節點 return BLACK; } return node.color; } }
前面介紹了紅黑樹的五個定義,這些定義使得紅黑樹可以維持自平衡。咱們都清楚,當對一顆樹添加或刪除節點時,就有可能會破壞這棵樹的平衡。紅黑樹也不例外,因此這個時候就須要做出一些調整,來讓紅黑樹繼續知足這五個定義。調整的方法有兩種,變色和旋轉,其中旋轉又分爲左旋轉和右旋轉。
變色:
從上面咱們編寫的紅黑樹的基礎結構代碼能夠看到,在添加一個節點時,默認是紅色。若是新添加的這個紅色節點不能知足紅黑樹的定義,那麼咱們就須要對其進行變色。例如,當添加的節點是一個根節點時,爲了保持根節點爲黑色,就須要將其顏色變爲黑色:
左旋轉:
在上圖中,身爲右子節點的Y取代了X的位置,而X變成了本身的左子節點,所以爲左旋轉。例如,咱們往根節點 1 添加一個元素 2,其左旋轉過程以下:
左旋轉的具體實現代碼以下:
// node x // / \ 左旋轉 / \ // T1 x ---------> node T3 // / \ / \ // T2 T3 T1 T2 private Node leftRotate(Node node) { Node x = node.right; // 左旋轉 node.right = x.left; x.left = node; x.color = node.color; node.color = RED; return x; }
假設咱們要對 37 這個 node 進行左旋轉,其右子節點 X 爲 42,根據上面的代碼,其左旋轉的具體過程以下:
在上一小節中,咱們瞭解了變色和左旋轉。基於以前的例子,當咱們再添加一個節點 66 時,該節點會被添加到右邊成爲右子節點,此時只須要作一下顏色的翻轉便可,以下所示:
對應的代碼以下:
/** * 顏色翻轉 */ private void flipColors(Node node) { node.color = RED; node.left.color = BLACK; node.right.color = BLACK; }
咱們再看另外一種添加節點的狀況,就是添加的節點比左子節點還要小,此時該節點就會掛到左子節點下:
對於這種狀況,咱們就要進行右旋轉:
在上圖中,身爲左子節點的Y取代了X的位置,而X變成了本身的右子節點,所以爲右旋轉。
對於上面那種狀況,右旋轉的流程以下:
還有一種狀況就是添加的元素比 node 和 X 都要大,此時就會掛載到 X 的右邊,此時就須要多作一步左旋轉操做。以下所示:
右旋轉的實現代碼以下:
// node x // / \ 右旋轉 / \ // x T2 -------> y node // / \ / \ // y T1 T1 T2 private Node rightRotate(Node node) { Node x = node.left; // 右旋轉 node.left = x.right; x.right = node; x.color = node.color; node.color = RED; return x; }
通過以上小節,如今咱們已經知道了紅黑樹維持平衡所需的變色和旋轉操做,以及相應的實現代碼。這些都屬於添加、刪除節點時用於維持平衡的子流程,因此接下來,就讓咱們實現一下往紅黑樹中添加新元素的代碼。以下:
/** * 向紅黑樹中添加新的元素(key, value) */ public void add(K key, V value) { root = add(root, key, value); // 保證根節點始終爲黑色節點 root.color = BLACK; } /** * 向以node爲根的紅黑樹中插入元素(key, value),遞歸算法 * 返回插入新節點後紅黑樹的根 */ private Node add(Node node, K key, V value) { if (node == null) { size++; // 默認插入紅色節點 return new Node(key, value); } if (key.compareTo(node.key) < 0) { node.left = add(node.left, key, value); } else if (key.compareTo(node.key) > 0) { node.right = add(node.right, key, value); } else { node.value = value; } // 是否須要左旋轉 if (isRed(node.right) && !isRed(node.left)) { node = leftRotate(node); } // 是否須要右旋轉 if (isRed(node.left) && isRed(node.left.left)) { node = rightRotate(node); } // 是否須要翻轉下顏色 if (isRed(node.left) && isRed(node.right)) { flipColors(node); } return node; }