徹底理解紅黑樹及JavaJDK1.8TreeMap原始碼分析

誰主沉浮oo7發表於2020-10-10

1. 定義

紅黑樹也是二叉查詢樹,我們知道,二叉查詢樹這一資料結構並不難,而紅黑樹之所以難是難在它是自平衡的二叉查詢樹,在進行插入和刪除等可能會破壞樹的平衡的操作時,需要重新自處理達到平衡狀態。紅黑樹是一種含有紅黑結點並能自平衡的二叉查詢樹,又稱黑色完美平衡。

動畫演示:https://rbtree.phpisfuture.com/

2. 節點稱呼

3. 性質

  • 每個節點要麼是黑色,要麼是紅色。

  • 根節點一定是黑色。

  • 每個葉子節點(nil或null)都是黑色的。

  • 每個紅節點的兩個子節點一定是黑色的。(不可以同時存在兩個相連的紅結點,即:紅節點的父結點與子結點都是黑的)

  • 從任意節點出發到每個葉子節點的路徑都包含相同個數的黑色節點。

    * 如果一個結點存在黑子結點,那麼該結點肯定有兩個子結點。
    
    * 黑色完美平衡。
    

下面是一棵簡單的紅黑樹,Nil(java中為null)是葉子節點併為黑色:

上圖中的紅黑樹並不是完美平衡的二叉查詢樹,P節點的左邊比右邊高,但是左右黑色的層數是相等的,任意一個結點到葉子節點的黑色節點數都相同(性質5),也被成為黑色完美平衡。

4. 紅黑樹的自平衡

4.1 左旋

以某個結點作為支點(旋轉結點),其右子結點變為旋轉結點的父結點,右子結點的左子結點變為旋轉結點的右子結點,其他結點保持不變。

4.2 右旋

以某個結點作為支點(旋轉結點),其左子結點變為旋轉結點的父結點,左子結點的右子結點變為旋轉結點的左子結點,其他結點保持不變。

4.3 變色

結點的顏色由紅變黑或由黑變紅。

5. 紅黑樹的查詢

紅黑樹是一顆二叉平衡樹,查詢不會破壞平衡性,所以和二叉平衡術查詢方式一致。

  • 從根節點開始查詢,為空就返回null,為當前值就返回,否則繼續向下查詢。
  • 如果當前節點的key為要查詢的節點的key,那麼直接返回當前值。
  • 如果當前節點的key大於要查詢的節點的key,那麼繼續向當前節點的左子節點查詢。
  • 如果當前節點的key小於要查詢的節點的key,那麼繼續向當前節點的右子節點查詢。

6. 紅黑樹的插入

插入會破壞紅黑樹的黑色完美平衡,所以插入第一步要找到要插入的位置進行插入,第二步進行自平衡。

6.1 查詢插入位置

所有插入操作都是在葉子結點進行的。

  • 插入節點的顏色肯定為紅色。因為插入節點為黑色,就會破壞黑色完美平衡,使得到葉子節點的黑色數+1,而紅色不會破壞。
  • 基本與紅黑樹的查詢相同:

從根節點開始,如果根節點為空,則插入在根節點,否則根節點為當前節點。

  • 如果當前節點為null,則返回當前節點的父節點進行插入。
  • 如果當前節點的key等與插入節點的key,則更新當前節點的value。
  • 如果當前節點的key大於插入節點的key,則繼續向當前節點的左子節點繼續查詢。
  • 如果當前節點的key小於插入節點的key,則繼續向當前節點的右子節點繼續查詢。

6.2 插入的自平衡

插入主要指標指向插入結點,通過4. 紅黑樹的自平衡將紅黑樹達到的平衡即可

左旋

條件:當前節點的父節點是紅色 & 當前節點的叔叔節點是黑色或者不存在 & 當前結點是其父節點的右子結點。

步驟:

  • 將父節點左旋
  • 將指標指向父結點

右旋

條件:當前節點的父節點是紅色 & 當前節點的叔叔節點是黑色或者不存在 & 當前結點是其父節點的左子結點。

步驟:

  • 將父節點變為黑色
  • 將祖父結點變為紅色
  • 將祖父結點右旋
  • 將指標指向祖父結點

變色

條件:當前節點的父節點是紅色並且當前節點的叔叔節點也是紅色。

步驟:

  • 當前結點是根結點直接變為黑色
  • 當前結點不是根結點
  • 將父節點與叔叔節點變為黑色
  • 將祖父結點變為紅色
  • 將指標指向祖父結點

JDK1.8中插入自平衡的原始碼實現:

private void fixAfterInsertion(Entry<K,V> x) {
    x.color = RED;

    while (x != null && x != root && x.parent.color == RED) {
        // 插入的父節點是左子節點
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            // y是插入節點的祖父節點的右子節點(叔叔節點)
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));
            // y是紅色
            if (colorOf(y) == RED) {
                // 變色處理
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                // 指標指向插入節點的祖父節點
                x = parentOf(parentOf(x));
            } else {
            // y是黑色的
                // 插入節點是是父節點的右子節點
                if (x == rightOf(parentOf(x))) {
                    // 父節點左旋
                    x = parentOf(x);
                    rotateLeft(x);
                }
                // 插入節點是是父節點的左節點
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                // 祖父節點右旋
                rotateRight(parentOf(parentOf(x)));
            }
        } else {
        // 插入的父節點是右子節點
            // y是插入節點的祖父節點的左子節點(叔叔節點)
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            // y是紅色
            if (colorOf(y) == RED) {
                // 變色處理
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                // 指標指向插入節點的祖父節點
                x = parentOf(parentOf(x));
            } else {
                // 插入節點是是父節點的左子節點
                if (x == leftOf(parentOf(x))) {
                    // 父親節點右旋
                    x = parentOf(x);
                    rotateRight(x);
                }
                // 插入節點是是父節點的右子節點
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                // 祖父節點左旋
                rotateLeft(parentOf(parentOf(x)));
            }
        }
    }
    root.color = BLACK;
}

7. 紅黑樹刪除

刪除操作與插入差不多,查詢、刪除、自平衡。查詢目標結點顯然可以複用查詢操作,當不存在目標結點時,忽略本次操作;當存在目標結點時,刪除後就得做自平衡處理了。刪除了結點後我們還需要找結點來替代刪除結點的位置,不然子樹跟父輩結點斷開了,除非刪除結點剛好沒子結點,那麼就不需要替代。

7.1 查詢刪除位置

基本與紅黑樹的查詢相同:

  • 從根節點開始,如果根節點為空,則刪除在根節點,否則根節點為當前節點。
  • 如果當前節點為null,則返回當前節點的父節點進行插入。
  • 如果當前節點的key等與刪除節點的key,則找到當前節點。
  • 如果當前節點的key大於刪除節點的key,則繼續向當前節點的左子節點繼續查詢。
  • 如果當前節點的key小於刪除節點的key,則繼續向當前節點的右子節點繼續查詢。

7.2 刪除結點

刪除節點的可能情況:

JDK1.8中TreeMap刪除可能性原始碼實現:

private void deleteEntry(Entry<K,V> p) {
    modCount++;
    size--;

    // If strictly internal, copy successor's element to p and then make p
    // point to successor.
    // 如果刪除節點有兩個子節點
    if (p.left != null && p.right != null) {
        // 找到替代節點(很簡單,自己看TreeMap原始碼)
        Entry<K,V> s = successor(p);
        p.key = s.key;
        p.value = s.value;
        p = s;
    } // p has 2 children

    // Start fixup at replacement node, if it exists.
    // 如果有一個替換節點
    Entry<K,V> replacement = (p.left != null ? p.left : p.right);

    // 如果存在替換節點
    if (replacement != null) {
        // Link replacement to parent
        replacement.parent = p.parent;
        if (p.parent == null)
            root = replacement;
        else if (p == p.parent.left)
            p.parent.left  = replacement;
        else
            p.parent.right = replacement;

        // Null out links so they are OK to use by fixAfterDeletion.
        p.left = p.right = p.parent = null;

        // Fix replacement
        if (p.color == BLACK)
            fixAfterDeletion(replacement);
    }
    // 如果刪除節點是根節點
    else if (p.parent == null) { // return if we are the only node.
        root = null;
    } else { 
    // 沒有子節點
        if (p.color == BLACK)
            fixAfterDeletion(p);

        if (p.parent != null) {
            if (p == p.parent.left)
                p.parent.left = null;
            else if (p == p.parent.right)
                p.parent.right = null;
            p.parent = null;
        }
    }
}

7.3 刪除後的自平衡

刪除自平衡處理:

JDK1.8中TreeMap刪除自平衡原始碼實現:

private void fixAfterDeletion(Entry<K,V> x) {
    while (x != root && colorOf(x) == BLACK) {
        // 刪除節點是左子節點
        if (x == leftOf(parentOf(x))) {
            // sib是刪除節點父節點的右子節點(兄弟節點)
            Entry<K,V> sib = rightOf(parentOf(x));
            // 兄弟節點是紅色
            if (colorOf(sib) == RED) {
                // 情況1.1處理
                setColor(sib, BLACK);
                setColor(parentOf(x), RED);
                rotateLeft(parentOf(x));
                sib = rightOf(parentOf(x));
            }
            // sib兄弟節點有兩個黑色的子節點,情況2處理
            if (colorOf(leftOf(sib))  == BLACK &&
                colorOf(rightOf(sib)) == BLACK) {
                // 變色
                setColor(sib, RED);
                // 指標指向刪除節點的父節點
                x = parentOf(x);
            } else {
                // 兄弟節點的右子節點是黑色
                if (colorOf(rightOf(sib)) == BLACK) {
                    // 情況3.1.1處理
                    setColor(leftOf(sib), BLACK);
                    setColor(sib, RED);
                    rotateRight(sib);
                    sib = rightOf(parentOf(x));
                }
                // 情況3.1.2處理
                setColor(sib, colorOf(parentOf(x)));
                setColor(parentOf(x), BLACK);
                setColor(rightOf(sib), BLACK);
                rotateLeft(parentOf(x));
                // 跳出迴圈
                x = root;
            }
        } else { // symmetric
        // 刪除節點是右子節點
            // sib是刪除節點父節點的左子節點(兄弟節點)
            Entry<K,V> sib = leftOf(parentOf(x));
            // 兄弟節點是紅色
            if (colorOf(sib) == RED) {
                // 情況1.2處理
                setColor(sib, BLACK);
                setColor(parentOf(x), RED);
                rotateRight(parentOf(x));
                sib = leftOf(parentOf(x));
            }
            // sib兄弟節點有兩個黑色的子節點,情況2處理
            if (colorOf(rightOf(sib)) == BLACK &&
                colorOf(leftOf(sib)) == BLACK) {
                // 變色
                setColor(sib, RED);
                // 指標指向刪除節點的父節點
                x = parentOf(x);
            } else {
                // 兄弟節點的左子節點是黑色
                if (colorOf(leftOf(sib)) == BLACK) {
                    // 情況3.2.1處理
                    setColor(rightOf(sib), BLACK);
                    setColor(sib, RED);
                    rotateLeft(sib);
                    sib = leftOf(parentOf(x));
                }
                // 情況3.2.2處理
                setColor(sib, colorOf(parentOf(x)));
                setColor(parentOf(x), BLACK);
                setColor(leftOf(sib), BLACK);
                rotateRight(parentOf(x));
                // 跳出迴圈
                x = root;
            }
        }
    }

    setColor(x, BLACK);
}

參考

結語

歡迎關注微信公眾號『碼仔zonE』,專注於分享Java、雲端計算相關內容,包括SpringBoot、SpringCloud、微服務、Docker、Kubernetes、Python等領域相關技術乾貨,期待與您相遇!

相關文章