紅黑樹 - Java 實現

紅黑樹也是一種平衡搜索樹,其具有如下 5 個限定條件:

  1. 節點要麼爲紅色,要麼爲黑色;
  2. 樹根色;
  3. 外部節點均爲色;
  4. 紅色節點孩子色;
  5. 從任一外部節點到根節點的沿途,黑節點的數目(黑高)相等。

包含 n 個內部節點的紅黑樹的高度不超過 O(logn)。

樹定義

public class RedBlackTree<T extends Comparable<? super T>> {
	// 顏色值
    private static final int RED = 0;
    private static final int BLACK = 1;

	// 紅黑樹節點
    private static class RedBlackNode<T> {
        T data;
        RedBlackNode<T> left, right, parent;
        int color;

        public RedBlackNode(T data, RedBlackNode<T> left, RedBlackNode<T> right, RedBlackNode<T> parent) {
            this.data = data;
            this.left = left;
            this.right = right;
            this.parent = parent;
            this.color = RED;
        }
    }

    // 樹根
    private RedBlackNode<T> root;


    // 是否爲黑色節點(外部節點也爲黑色)
    private boolean isBlack(RedBlackNode<T> node) {
        return (node == null || node.color == BLACK);
    }

    // 是否爲紅色節點
    private boolean isRed(RedBlackNode<T> node) {
        return (node != null && node.color == RED);
    }

    // node 是否爲左孩子
    private boolean isLeftChild(RedBlackNode<T> node) {
        return (node.parent != null && node.parent.left == node);
    }

    // node 是否爲右孩子
    private boolean isRightChild(RedBlackNode<T> node) {
        return (node.parent != null && node.parent.right == node);
    }

    // 獲取 node 的叔叔節點
    private RedBlackNode<T> uncle(RedBlackNode<T> node) {
        return (isLeftChild(node.parent) ? node.parent.parent.right : node.parent.parent.left);
    }

    ...
}

搜索節點

// 用於表示搜索結果(作爲內部類)
private static class SearchResult<T> {
    RedBlackNode<T> target;       // 目標節點
    RedBlackNode<T> parent;       // 目標節點之父
    // getter & setter ...
}
// 查找關鍵碼 e:返回目標節點及其父節點(插入、刪除時有用)
public SearchResult<T> search(T e) {
    SearchResult<T> result = new SearchResult<>();
    RedBlackNode<T> current = root, parent = null;

    while (current != null) {
        int compareResult = e.compareTo(current.data);

		// 找到了
        if (compareResult == 0) {
            break;
        }

		// 記錄父節點,然後:比當前節點小->往左;否則->往右
        parent = current;
        current = compareResult < 0 ? current.left : current.right;
    }

    result.target = current;
    result.parent = parent;

    return result;
}

插入節點

插入節點時,將新插入節點染爲紅色。(黑高不變)

但可能造成如下問題,導致紅黑樹失衡:

  1. 如果新插入節點爲樹根,則違背 條件2(樹根需爲黑色);
  2. 如果新插入節點之父爲紅色(“雙紅”問題),則違背 條件4(紅色節點的孩子需爲黑色);

解決 “雙紅” 問題

設新插入節點爲 v,v 之父爲 p,p 之父爲 g ,p 之兄弟、g 的另一個孩子、v 的叔叔 爲 u 。

此時,v 爲紅色,p 爲紅色,g 爲黑色 (g 不可能爲紅色,因爲其孩子節點 p 已經爲紅色 — 條件4)。

以下根據 u 的顏色分兩種情況:

1. u 爲黑色

根據 g、p、v 之間的連接關係有可分爲 4種 情況。

此時, v 的兄弟、 v 的兩個孩子的黑高,均與 u 的黑高相等。

在這裏插入圖片描述
經過適當的重平衡操作之後(如,單旋、雙旋),設子樹的根爲 b,b 的左右孩子分別爲 a 、c 。則將 b 染爲黑色,將 a、c 均染爲紅色。
在這裏插入圖片描述

如此調整之後, 局部子樹的黑高度將復原,這意味着全樹的平衡也必然得以恢復。同時,新子樹的根節點 b爲黑色, 也不致引發新的雙紅現象。 至此,整個插入操作遂告完成。

2. u 爲紅色

同樣,根據 g、p、v 之間的連接關係有可分爲 4種 情況。

此時, u 的左、右孩子非空且均爲黑色,且它們的黑高必與 v 的兄弟以及 v 的兩個孩子的黑高相等。

在這裏插入圖片描述
只需將紅節點 p 和 u 轉爲黑色,黑節點 g 轉爲紅色, v 保持紅色。此時不需旋轉操作。

在這裏插入圖片描述
如此調整之後局部子樹的黑高保持不變。

設 g 之父爲 gg,當 gg 爲黑色時,調整操作完成;當 gg 爲紅色時,再次引發“雙紅”問題。此時,可以等效地將 g 視作新插入的節點,同樣分以上兩類情況如法處置。

綜上,只有在第一種情況時才需做1~2次旋轉;而且一旦旋轉後,修復過程必然隨即完成。故就全樹拓撲結構而言,每次插入後僅涉及常數次調整。

// 插入關鍵碼
public void insert(T e) {
	// 先確定目標節點是否存在
    SearchResult<T> result = search(e);

	// 已存在
    if (result.target != null) {
        return;
    }

	// 插入節點
    RedBlackNode<T> node = new RedBlackNode<>(e, null, null, parent);

	//  建立父節點到新插入節點的連接
	RedBlackNode<T> parent = result.parent;

    if (parent == null) {						 // 新插入節點爲樹根
        root = node;
    } else {									 // 新插入節點不是樹根
        if (e.compareTo(parent.data) < 0) {
            parent.left = node;
        } else {
            parent.right = node;
        }
    }

	// 解決雙紅問題
    solveDoubleRed(node);
}
// 解決“雙紅”問題:父子同爲紅色
private void solveDoubleRed(RedBlackNode<T> node) {
    // 已至樹根:將其染黑即可
    if (node.parent == null) {
        root.color = BLACK;
        return;
    }

	// 父節點
    RedBlackNode<T> parent = node.parent;

    // 父節點爲黑色,不存在“雙紅”
    if (isBlack(parent)) {
        return;
    }

    // 父節點爲紅色,則祖父節點必然存在且爲黑色
    RedBlackNode<T> grandparent = parent.parent;

	// 根據叔叔節點的顏色,分爲 2 種情況處理
    RedBlackNode<T> uncle = uncle(node);

    // 叔叔節點爲黑色:1~2 次旋轉和 3次染色 即可完成調整
    if (isBlack(uncle)) {
        RedBlackNode<T> greatgrandparent = grandparent.parent;	// 曾祖父節點
        RedBlackNode<T> rootOfSubtree;			// 重平衡之後,子樹的根
        if (greatgrandparent == null) {			// 重平衡之後,子樹的根即是整棵樹的樹根
            rootOfSubtree = root = rotateAt(node);
        } else {								// 否則,將重平衡之後的子樹接入,成爲原曾祖父的孩子
            if (isLeftChild(grandparent)) {
                rootOfSubtree = greatgrandparent.left = rotateAt(node);
            } else {
                rootOfSubtree = greatgrandparent.right = rotateAt(node);
            }
        }
        
        rootOfSubtree.color = BLACK;

		// rootOfSubtree 的左右孩子爲 node、parent、grandparent 中的兩個,所以必然不爲空
        rootOfSubtree.left.color = RED;
        rootOfSubtree.right.color = RED;
        
        rootOfSubtree.parent = greatgrandparent;
    }
    
    // 若叔叔節點爲紅色,則只需染色,無需旋轉,然後繼續向上解決“雙紅”
    // 當遇到上一種情況是,解決之,即調整操作完成。
    else {
        parent.color = BLACK;
        uncle.color = BLACK;
        grandparent.color = RED;

		// 遞歸解決之
        solveDoubleRed(grandparent);
    }
}

刪除節點

設,實際被刪除的節點爲 x,節點 x 的接替者(x 的某一個孩子)爲 r 。此時,可以確定 x 最多隻有一個非空孩子(因爲 x 是實際被刪除的節點 — 注意“實際”二字,可見 二叉搜索樹 - Java 實現 - 刪除節點)。

如果 x 爲紅色,則所有條件依然滿足(不影響黑高),不需調整。

如果 x 爲黑色,則 條件4 和 條件5 可能不滿足。此時,需要根據 r 的顏色決定:

  • 如果 r 爲紅色,則將其翻轉爲黑色,即可滿足 條件4 和 條件5 。
  • 如果 r 爲黑色,則出現“雙黑”問題(x 和 r 均爲黑色),情況複雜。

解決 “雙黑” 問題

當出現“雙黑”問題時,設 x 的兄弟節點爲 s,則 s 必然非空(x 爲黑色非空節點,如果 s 爲空,則 條件5 不滿足 — 黑高不相等)。設 x 之父爲 p,p 和 s 的顏色不確定。以下根據 p 和 s 的顏色不同組合分 4種情況處理。

(1)s 爲黑色,且 s 有紅色孩子,p 不確定顏色

設 s 的一個紅色孩子爲 t 。根據 p、s、t 之間的連接關係,又可分爲 左左、左右、右右、右左 4種情況。此處僅以 左左 爲例(不管是何種情況,均可通過後面的“統一重平衡”操作完成調整):

在這裏插入圖片描述
首先,對 t、s、p 實施重平衡操作;設重平衡之後的子樹的根爲 b,其左右孩子分別爲 a、c 。則然後,將 a、c 均染爲黑色,讓 b 繼承 p 原來的顏色。

經以上處理之後,紅黑樹的所有條件,都在這一局部以及全局得到滿足,故刪除操作遂告完成。

(2)s 爲黑色,且 s 無紅色孩子,p 爲紅色

在這裏插入圖片描述
(另一種情況對稱)

因爲 x 即將被刪除,即 p 的右子樹的黑高將減一,則將 p 的左子樹的黑高也減一,然後然 p 染爲黑色即可(保證黑高不變 — 條件5)。此處,即是將 s 和 p 的顏色對調。

紅黑樹的所有條件都在此局部得以恢復。由於子樹的黑高保持不變,紅故黑樹條件亦必在全局得以恢復,刪除操作即告完成。

(3) s 爲黑色,且 s 無紅色孩子,p 爲黑色

在這裏插入圖片描述
(另一種情況對稱)

因爲 x 即將被刪除,即 p 的右子樹的黑高將減一,則將 p 的左子樹的黑高也減一,即將 s 染爲紅色,p 保持黑色不變。至此,局部子樹 p 滿足所有條件,但因其黑高減一,在全局不一定滿足 條件5 。

此時的狀態可等效地理解爲:節點 p 的一個黑色父節點剛被刪除,且 p 爲接替者,故可遞歸解決“雙黑”問題。

(4) s 爲紅色,則 p 必爲黑色

在這裏插入圖片描述
(另一種情況對稱)

此處,以節點 p 爲軸做一次旋轉,並交換節點 s 與 p 的顏色。局部子樹滿足條件,但這是在 x 還未被刪除的前提下才成立的。

此處之妙在於,它被轉換成了情況(1)或情況(2)。這就意味着, 接下來至多再做一步迭代調整,整個雙黑修正的任務即可大功告成。

綜上,一旦在某步迭代中做過節點的旋轉調整,整個修復過程便會隨即完成。因此與雙紅修正一樣, 雙黑修正的整個過程,也僅涉及常數次的拓撲結構調整操作。

// 找到 root 子樹中關鍵碼最小的節點及其父節點
private SearchResult<T> findMinimum(RedBlackNode<T> root) {
    SearchResult<T> result = new SearchResult<>();
    RedBlackNode<T> parent = null;

	// 一直往左即可找到最小者
    if (root != null) {
        while (root.left != null) {
            parent = root;
            root = root.left;
        }
    }

    result.target = root;
    result.parent = parent;

    return result;
}
// 刪除
public void remove(T e) {
	// 先確認目標節點是否存在
    SearchResult<T> result = search(e);

	// 不存在
    if (result.target == null) {
        return;
    }

    RedBlackNode<T> target = result.target;       // 待刪除節點
    RedBlackNode<T> parent = result.parent;       // 待刪除節點之父

    // 有左右兩個非空孩子
    // 以待刪除節點爲根,找到其右子樹中關鍵碼最小的節點,讓它代替受死
    if (target.left != null && target.right != null) {
        SearchResult<T> result1 = findMinimum(target.right);
        target.data = result1.target.data;
        target = result1.target;                // 真正被刪除的節點
        parent = result1.parent;
    }

    // 現在,待刪除節點最多隻有一個非空孩子
    // 讓該孩子節點繼承父節點的位置
    RedBlackNode<T> child = target.left != null ? target.left : target.right;

    if (parent != null) {					// 待刪除節點不是樹根
        if (parent.left == target) {
            parent.left = child;
        } else {
            parent.right = child;
        }
    } else {								// 待刪除節點是樹根
        root = child;
    }

	// 重平衡操作
	
    // 若刪除的節點爲樹根:將新的樹根染爲黑色
    if (parent == null) {
        root.color = BLACK;
        return;
    }

    // 若`實際`被刪除的節點爲紅色,則其接替者(其孩子)必爲黑色,此時黑高不變,無需調整
    // 若`實際`被刪除的節點爲黑色,且若其接替者(其孩子)爲紅色,則令接替者變爲黑色即可
    if (isBlack(target) && isRed(child)) {
        child.color = BLACK;
        return;
    }

    // 若`實際`被刪除的節點爲黑色,且若其接替者(其孩子)爲黑色,則出現“雙黑”問題
    solveDoubleBlack(child, parent);
}
// 解決“雙黑”問題:整個操作只需常數次旋轉操作即可完成調整
// parent 爲 node 之父,之所以需要傳遞 parent,是爲了解決 node 爲 null 的情況
private void solveDoubleBlack(RedBlackNode<T> node, RedBlackNode<T> parent) {
	// node 爲根節點,無需操作
    if (parent == null) {
        return;
    }

    // node 的兄弟
    RedBlackNode<T> sibling = (node == parent.left) ? parent.right : parent.left;

	// sibling 爲黑色
    if (isBlack(sibling)) {
        // sibling 的紅色孩子,若左、右孩子皆紅,左者優先;都爲黑時爲 null
        RedBlackNode<T> redChild = null;
        if (sibling.left != null && isRed(sibling.left)) {
            redChild = sibling.left;
        } else if (sibling.right != null && isRed(sibling.right)) {
            redChild = sibling.right;
        }
        // 有紅色孩子:情況 1
        if (redChild != null) {
            int color = parent.color;
            RedBlackNode<T> grandparent = parent.parent;	// 曾祖父節點
            RedBlackNode<T> rootOfSubtree;			// 重平衡之後,子樹的根
            if (grandparent == null) {				// 重平衡之後,子樹的根即是整棵樹的樹根
                rootOfSubtree = root = rotateAt(redChild );
            } else {								// 否則,將重平衡之後的子樹接入,成爲原曾祖父的孩子
                if (isLeftChild(grandparent)) {
                    rootOfSubtree = grandparent.left = rotateAt(redChild );
                } else {
                    rootOfSubtree = grandparent.right = rotateAt(redChild );
                }
            }
            
            // 將左右孩子染黑
            rootOfSubtree.left.color = BLACK;
            rootOfSubtree.right.color = BLACK;
            
            // 繼承原 p 節點的顏色
            rootOfSubtree.color = color;
            
            rootOfSubtree.parent = grandparent;
        }
        // 無紅色孩子
        else {
            sibling.color = RED;	// 將 silibing 染紅(情況2、3中,s 均被染紅)
            // 父節點爲紅色:情況 2
            if (isRed(parent)) {
                parent.color = BLACK;
            }
            // 父節點爲黑色:情況 3
            else {
                solveDoubleBlack(parent, parent.parent);
            }
        }
    }
    // sibling 爲紅色:情況 4
    else {
    	// sibling 轉黑, parent 轉紅
        sibling.color = BLACK;
        parent.color = RED;
        
        // 取 s 同側的孩子
        RedBlackNode<T> child = isLeftChild(sibling) ? sibling.left : sibling.right;
        RedBlackNode<T> grandparent = parent.parent;
        RedBlackNode<T> rootOfSubtree;
        // 旋轉:將情況 4 轉爲 情況 1/情況2
        if (grandparent == null) {
            rootOfSubtree = root = rotateAt(child);
        } else {
            if (isLeftChild(grandparent)) {
                rootOfSubtree = grandparent.left = rotateAt(child);
            } else {
                rootOfSubtree = grandparent.right = rotateAt(child);
            }
        }
        
        rootOfSubtree.parent = grandparent;
		
		// 遞歸解決:此時已是情況1/情況2
        solveDoubleBlack(node, parent);
    }
}

重平衡操作

具體解釋見:AVL 樹 - Java 實現 - 重平衡操作

// “3+4”重構:重構子樹
private RedBlackNode<T> connect34(RedBlackNode<T> T0, RedBlackNode<T> a, RedBlackNode<T> T1, RedBlackNode<T> b,
                                  RedBlackNode<T> T2, RedBlackNode<T> c, RedBlackNode<T> T3) {
    a.left = T0;
    a.right = T1;
    if (T0 != null) {
        T0.parent = a;
    }
    if (T1 != null) {
        T1.parent = a;
    }

    c.left = T2;
    c.right = T3;
    if (T2 != null) {
        T2.parent = c;
    }
    if (T3 != null) {
        T3.parent = c;
    }

    b.left = a;
    b.right = c;
    a.parent = c.parent = b;

    return b;   // 新子樹的根
}
// 視具體情況完成重平衡操作
// 返回重構之後子樹的根
private RedBlackNode<T> rotateAt(RedBlackNode<T> node) {
    RedBlackNode<T> parent = node.parent;
    RedBlackNode<T> grandparent = parent.parent;

    if (isLeftChild(parent)) {      // 左
        if (isLeftChild(node)) {    // 左
            parent.parent = grandparent.parent;
            return connect34(node, parent, grandparent, node.left, node.right, parent.right, grandparent.right);
        } else {                    // 右
            node.parent = grandparent.parent;
            return connect34(parent, node, grandparent, parent.left, node.left, node.right, grandparent.right);
        }
    } else {                        // 右
        if (isRightChild(node)) {   // 右
            parent.parent = grandparent.parent;
            return connect34(grandparent, parent, node, grandparent.left, parent.left, node.left, node.right);
        } else {                    // 左
            node.parent = grandparent.parent;
            return connect34(grandparent, node, parent, grandparent.left, node.left, node.right, parent.right);
        }
    }
}

與 AVL 樹的差別

對紅黑樹來說,在節點插入和節點刪除後的重平衡過程中,一旦完成了旋轉操作,整個重平衡過程必然隨即完成(需要遞歸重平衡的情況下,不涉及旋轉操作)。也就是說,紅黑樹的插入、刪除操作僅涉及常數次拓撲結構的調整操作!這是紅黑樹與AVL樹的一項本質差別。

在 AVL 樹的插入操作中,也只需常數次的調整操作,但在其刪除操作中,可能需要多次調整操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章