1. 程式人生 > 其它 >【演算法4】3.3.紅黑樹

【演算法4】3.3.紅黑樹

平衡查詢樹

理想情況下,我們希望能夠保持二分查詢樹的平衡性。
在一棵含有 N 個結點的樹中,我們希望樹高為 \(log_2 N\)

2-3 樹

2-3 樹由兩種結點組成:

  • 2- 結點:有一個鍵和兩條連結
  • 3- 結點:有兩個鍵和三條連結

向一棵 2-3 樹插入一個鍵值對:

  • 如果查詢結束於一個 2- 結點,那麼直接將鍵值對插入該結點形成一個 3- 結點即可
  • 如果查詢結束於一個 3- 結點,那麼需要臨時構造一個 4- 結點,再將中鍵推入父節點。遞迴此過程直到遇到一個 2- 結點(樹的高度不變)或分解根結點(樹高度 +1)

紅黑樹

雖然 2-3 樹能夠在動態插入保持平衡性,但是其直接實現需要維護兩種型別的結點、在不同型別結點間進行轉換和複製資訊、
在結點中對每個鍵進行比較等,這會增加程式碼的複雜度和開銷。

紅黑樹是一棵二叉查詢樹,它使用紅色左連線及其連線的兩個結點來表示 2-3 樹的 3- 結點。

紅黑樹的另一種定義是含有紅黑連線並滿足以下條件的二叉查詢樹:

  • 紅色連線均為左連線
  • 沒有任何一個結點同時和兩條紅連線相連
  • 該樹是完美黑色平衡的,即任意空連結到根結點的路徑上的黑鏈數量相同

紅黑樹實現

旋轉和顏色變換

左旋轉:將帶有紅色右連線的根結點轉變成帶有紅色左連線的根結點

右旋轉:將帶有紅色左連線的根結點轉變成帶有紅色右連線的根結點

插入新結點時,我們總是使用紅色連線將新結點與父結點連線。

向一個 2- 結點插入新鍵:

  • 如果插入到左連線,則會直接形成一個 3- 結點
  • 如果插入到右連線,則需要先進行左旋轉才會形成一個正確的 3- 結點

向一個 3- 結點插入新鍵:

  • 如果新鍵大於原樹中的兩個結點,則將新鍵插入到右連線。此時需要分解此臨時的 4- 結點,即將兩條紅色連線變成黑色並將父結點的連線變成紅色(對應到 2-3 將中鍵推入父結點並生成兩個 2- 子結點)。
  • 如果新鍵小於原樹中的兩個結點,則將新鍵連線到左子結點的左連線。此時會出現兩條連續的紅連線,需要對上層連線進行右旋轉,得到第一種情況
  • 如果新鍵介於原樹種的兩個結點,則將新增連線到左子結點的右連線。對下層連線進行左旋轉會得到第二種情況

進行顏色變換後,根結點上的連線可能會變成紅色,但根結點上不會再有其他結點,每次插入後需要將根結點上的連線重置成黑色。

對應到程式碼實現,查詢會結束於一個 2- 結點,我們會用紅色左/右連線插入新結點,然後依次執行以下步驟:

  • 如果左連線是黑色且右連線是紅色,進行左旋轉(全紅是要進行顏色變換)
  • 如果左連線是紅色且左子結點的左連線也是紅色,進行右旋轉
  • 如果兩個連線都是紅色,執行顏色變換

程式碼實現

結點定義:

private static final boolean RED = true;
private static final boolean BLACK = false;
private class Node {
    private K key;
    private V value;
    private boolean color; // 指向該結點連線的顏色
    private Node left;
    private Node right;

    public Node(K key, V value, boolean color) {
        this.key = key;
        this.value = value;
        this.color = color;
    }
}

private boolean isRed(Node node) {
    if (node == null) return BLACK;
    return node.color;
}

旋轉和顏色變換:

// 傳入指向父結點連線
// 返回父節點,並在呼叫函式重置指向父結點的連線
// 左旋轉:將紅色右連線變成紅色左連線
private Node rotateLeft(Node h) {
    Node x = h.right;
    h.right = x.left;
    x.left = h;
    x.color = h.color;
    x.left.color = RED;
    x.N = h.N;
    h.N = size(h);
    return x;
}

// 右旋轉
private Node rotateRight(Node h) {
    Node x = h.left;
    h.left = x.right;
    x.right = h;
    x.color = h.color;
    x.right.color = RED;
    x.N = h.N;
    h.N = size(h);
    return x;
}

// 顏色變換
// 左右連結串列由紅變黑,父連線變紅
private void flipColors(Node h) {
    h.left.color = BLACK;
    h.right.color = BLACK;
    h.color = RED;
}

private int size(Node node) {
    if (node == null) {
        return 0;
    }
    return size(node.left) + size(node.right) + 1;
}

插入演算法:

// 插入演算法
public void put(K key, V value) {
    root = put(root, key, value);
    root.color = BLACK;
}

private Node put(Node node, K key, V value) {
    if (node == null) {
        return new Node(key, value, 1, RED);
    }

    int cmp = node.key.compareTo(key);
    if (cmp < 0) {
        node.left = put(node.left, key, value);
    } else if (cmp > 0){
        node.right = put(node.right, key, value);
    } else {
        node.value = value;
    }

    if (!isRed(node.left) && isRed(node.right)) node = rotateLeft(node);
    if (isRed(node.left) && isRed(node.left.left)) node = rotateRight(node);
    if (isRed(node.left) && isRed(node.right)) flipColors(node);

    node.N = size(node);
    return node;
}

參閱