數據結構之紅黑樹

2-3樹

在瞭解紅黑樹以前,咱們先來認識2-3樹,在算法(第4版)裏也是先從2-3樹切入到紅黑樹的。而且瞭解2-3樹對於理解B類樹也會有幫助,由於2-3樹能夠說就是基礎的B類樹。java

2-3樹的特性:node

  • 知足二分搜索樹的基本性質
  • 節點能夠存放一個元素或者兩個元素,或者說數據項
  • 每一個節點有2個或者3個子節點,這也是2-3樹的名稱由來
  • 2-3樹是一棵絕對平衡的樹,對於任意節點的左右子樹的高度必定是相等的

2-3樹爲了維持絕對平衡,須要知足如下條件:算法

  1. 2節點有且只能有兩個子節點,並只能包含一個數據項
  2. 3節點有且只能有三個子節點,並只能包含兩個數據項,大小關係從左至右依次遞增
  3. 添加數據項時不能將該數據項添加到一個空節點上,由於新的節點只能經過分裂或者融合產生
  4. 當2-3樹只有2節點的時候,其只能是一棵滿二叉樹

2-3樹的兩類節點:
數據結構之紅黑樹數據結構

  • 能夠看到,2節點有兩個子節點,5和15,且自身只包含一個數據項,即10。3節點則有三個子節點,自身只能包含兩個數據項,從左至右依次遞增:5 < 6 < 7 < 8 < 9

下圖是一顆完整的2-3樹:
數據結構之紅黑樹ide

從上圖中能夠看到2-3樹是知足二分搜索樹的基本性質的,只有兩個節點的狀況,如 42 這個節點,右子節點小於父節點,左子節點大於父節點。而有三個節點時,右子節點仍然小於父節點,中間的子節點大於父節點的左數據項,小於父節點的右數據項(如圖中18大於17,小於33),左子節點則大於父節點。性能


2-3樹的絕對平衡性

以前咱們提到了2-3樹插入節點時不能將該節點插入到一個空節點上,新的節點只能經過分裂或者融合產生。咱們知道對二分搜索樹依次添加有序的數據時,如依次添加 一、二、三、四、5,會產生連續的節點,使得二分搜索樹退化成鏈表。this

爲了不退化成鏈表,具備平衡特性的樹狀結構,會採起一些手段來維持樹的平衡,例如AVL是經過旋轉節點,而2-3樹則是經過分裂和融合。當咱們依次添加 一、二、三、四、5 到2-3樹時,其流程以下:
數據結構之紅黑樹3d

  1. 添加元素1,建立一個2節點類型的根節點code

  2. 添加元素2,此時元素1和2存在同一個節點中,成爲一個3節點。爲何添加元素2時,不能生成一個新的節點做爲元素1所在節點的右子節點呢?由於「添加數據項時不能將該數據項添加到一個空節點上,新的節點只能經過分裂或者融合產生」blog

  3. 添加元素3,元素一、二、3,暫時存在同一個節點中,造成一個4節點

  4. 分裂,2-3樹中最多隻有3節點,不能存在4節點,因此暫時造成的4節點要進行分裂,將中間的元素做爲根節點,左右兩個元素各爲其左右子節點。這時可見造成了一棵滿二叉樹

  5. 添加元素4,根據元素的大小關係,將會存放到元素3所在的節點。由於新添加的元素不能添加到一個空節點上,因此元素4將根據搜索樹的性質找到最後一個節點與其融合。即元素3和4將融合爲一個三節點。而且根據大小關係元素4要位於元素3的右側

  6. 添加元素5,同插入元素4,元素5一路查找到元素三、4所在的三節點,與其融合,暫時造成一個4節點

  7. 分裂,元素三、四、5所在的4節點同上面元素一、二、3造成的4節點同樣,進行分裂操做。根據大小關係,4元素將會做爲根節點,元素三、5則各爲其左右子節點

  8. 融合,前面的分裂操做已經致使該2-3樹不知足其第四條性質「當2-3樹只有2節點的時候,其只能是一棵滿二叉樹」,因此該2-3樹將要向上融合以知足2-3樹的性質。咱們只須要將元素4所在節點與其父節點即元素2所在的節點進行融合便可。這時,元素二、4就造成了一個3節點

若是咱們繼續往2-3樹中添加元素6和7,那麼最終造成的2-3樹以下圖所示:
數據結構之紅黑樹

若是在這個案例中咱們使用的是二分搜索樹,那麼該二分搜索樹將會退化爲一個鏈表,而2-3樹則經過分裂、融合的方式成爲了一顆滿二叉樹。


紅黑樹與2-3樹的等價性

瞭解了2-3樹後,咱們來看下紅黑樹與2-3樹的等價性,嚴格來講是左傾紅黑樹纔是與2-3樹是等價的。與2-3樹同樣,紅黑樹具備二分搜索樹的性質,而且也是自平衡的,但不是絕對平衡,甚至平衡性比AVL樹還要差一些。

以前提到了2-3樹是絕對平衡的,對於任意節點的左右子樹的高度必定是相等的。而AVL樹則是任意節點的左右子樹高度相差不超過 1 便可,屬於高度平衡的二分搜索樹。

紅黑樹則是從根節點到葉子節點的最長路徑不超過最短路徑的2倍,其高度僅僅只會比AVL樹高度大一倍,因此在性能上,降低得並很少。因爲紅黑樹也是自平衡的樹,也會採起一些機制來維持樹的平衡。

紅黑樹的定義:

  1. 每一個節點或者是紅色的,或者是黑色的
  2. 根節點是黑色的
  3. 每個葉子節點(最後的空節點)是黑色的
  4. 若是一個節點是紅色的,那麼它的左右子節點都是黑色的
  5. 從任意一個節點到葉子節點,通過的黑色節點是同樣的

這裏的第三點要求「每個葉子節點(最後的空節點)是黑色的」,稍微有些奇怪,它主要是爲了簡化紅黑樹的代碼實現而設置的。咱們也能夠理解爲,只要是空的節點,它就是黑色的。

下圖是一顆典型的紅黑樹:
數據結構之紅黑樹

在瞭解了2-3樹以後,咱們知道2-3樹是經過分裂和融合來產生新的節點並維持平衡的。2-3樹有兩類節點,2節點和3節點。除此以外,還會有一種臨時的4節點。接下來咱們看看2-3樹向紅黑樹轉換的過程,下圖展現了2-3樹的這三種節點對應於紅黑樹的節點:
數據結構之紅黑樹

  • 2節點:對應於紅黑樹的黑色節點
  • 3節點:對應於紅黑樹中黑色的父節點和紅色的左子節點
  • 臨時的4節點:對應於紅色的父節點和黑色的左右子節點。這裏須要說一下,爲何是紅色的父節點而不是黑色的呢?主要是由於2-3樹的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,其左旋轉過程以下:
數據結構之紅黑樹

  • Tips:本文中設定紅黑樹是左傾的

左旋轉的具體實現代碼以下:

//   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;
}
相關文章
相關標籤/搜索