平衡二叉樹之紅-黑樹學習

1、紅黑樹

平衡二叉樹(最早的AVL樹)的劣勢在於:

  • 刪除:對於平衡二叉樹來說,在最壞情況下,需要維護從被刪節點到根節點這條路徑上所有節點的平衡性,旋轉的量級是OlogN。但是紅黑樹就不一樣了,最多隻需3次旋轉就會重新平衡,旋轉的量級是O(1)。
  • 保持平衡:平衡二叉樹高度平衡,這也就意味着在大量插入和刪除節點的場景下,平衡二叉樹爲了保持平衡需要調整的頻率會更高。
    所以在大量查找的情況下,平衡二叉樹的效率更高,也是首要選擇。在大量增刪的情況下,紅黑樹是首選。

就是因爲平衡ALV樹每次維護結點的平衡執行的旋轉頻率過高,不適合於大量增刪的需求,才提出了紅黑樹。紅黑樹相對於AVL樹來說,犧牲了部分平衡性以換取插入/刪除操作時少量的旋轉操作,整體來說性能要優於AVL樹。
在這裏插入圖片描述

1.1、什麼是紅黑樹(5個性質)

紅黑樹是每個節點都帶有顏色屬性的二叉查找樹,顏色爲紅色或黑色(只要是兩種不同的狀態即可)。在滿足二叉查找樹強制一般要求以外,對於任何有效的紅黑樹我們增加了如下的額外要求

  • 1、節點是紅色或黑色。
  • 2、根是黑色。
  • 3、所有葉子都是黑色(葉子是NIL節點)。(java裏邊是用null表示NIL節點)
  • 4、每個紅色節點必須有兩個黑色的子節點。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點。)
  • 5、從任一節點到其每個葉子的所有簡單路徑都包含相同數目的黑色節點。

黑色高度
從根節點到葉節點的路徑上黑色節點的個數,叫做樹的黑色高度。所以性質5也可以描述爲從根節點到葉節點路徑的黑色高度必須相同。

小結性質
性質 4 的意思是:從每個根到節點的路徑上不會有兩個連續的紅色節點,但黑色節點是可以連續的。
因此若一條路徑上給定黑色節點的個數 N,最短路徑的情況是連續的 N 個黑色,樹的高度爲 N - 1;最長路徑的情況爲節點紅黑相間,樹的高度爲 2(N - 1) 。
性質 5 是成爲紅黑樹最主要的條件,紅黑樹的插入、刪除操作都是爲了遵守這個規定。
紅黑樹並不是標準平衡二叉樹,它以性質 5 作爲一種平衡方法,使自己的性能得到了提升。

紅黑樹例子:
在這裏插入圖片描述
它的統計性能要好於平衡二叉樹(AVL樹),因此,紅黑樹在很多地方都有應用。比如在 Java 集合框架中,很多部分(HashMap, TreeMap, TreeSet 等)都有紅黑樹的應用,這些集合均提供了很好的性能。

1.2、紅黑樹的實現

我們知道平衡二叉樹最關鍵的是保持其平衡,那麼平衡是要通過旋轉來實現的。而紅黑樹不僅要實現自平衡還有遵循紅黑規則(5個性質),那麼我們就不難推出:紅黑樹是通過旋轉和節點的顏色變換來完成自平衡的

1.2.1、旋轉

同AVL樹一樣,分左旋右旋。下面我們查看以下TreeMap左旋右旋的源碼:

1、左旋
在這裏插入圖片描述

 /** From CLR */
    private void rotateLeft(Entry<K,V> p) {//左旋,對指定結點p的左旋
        if (p != null) {
            Entry<K,V> r = p.right;//獲取失衡點A的右孩子c
            p.right = r.left;//失衡點A的右孩子變更爲原來右孩子c的左孩子D
            if (r.left != null)
                r.left.parent = p;
            r.parent = p.parent;//連接上層結點
            if (p.parent == null)
                root = r;//
            else if (p.parent.left == p)
                p.parent.left = r;
            else
                p.parent.right = r;
            r.left = p;//失衡點A原來的右孩子C變爲根節點(相對來說),其左孩子變更爲失衡點A
            p.parent = r;
        }
    }

2、右旋
在這裏插入圖片描述

    /** From CLR */
    private void rotateRight(Entry<K,V> p) {//右旋,對指定結點p的右旋
        if (p != null) {
            Entry<K,V> l = p.left;//獲取失衡點A的左孩子B
            p.left = l.right;//失衡點A的左孩子變更爲其原來的左孩子B的右孩子E
            if (l.right != null) l.right.parent = p;
            l.parent = p.parent;//連接上層結點
            if (p.parent == null)
                root = l;
            else if (p.parent.right == p)
                p.parent.right = l;
            else p.parent.left = l;
            l.right = p;//失衡點A原來的左孩子B變爲根節點,其右孩子變爲失衡點A
            p.parent = l;
        }
    }

1.2.2、顏色變換

我們插入節點勢必要考慮如何插入能保證紅黑樹的5個性質不變,也就是說我們插入必須遵循這5個規則。有些插入情況必須要對之前的節點進行顏色變換。

1.3、紅黑樹的插入

紅黑樹的插入主要分兩步:

  • 1、首先和二叉查找樹的插入一樣,查找、插入,利用了遞歸,可以參考查找算法
  • 2、然後調整結構,保證滿足紅黑樹狀態
    • 2.1、對結點進行重新着色(顏色變換
    • 2.2、以及對樹進行相關的旋轉操作

紅黑樹的插入在二叉查找樹插入的基礎上,爲了重新恢復平衡,繼續做了插入修復操作。
下面我們考慮以下插入時如何解決着色的問題。

類型 分析
①紅黑樹爲空,插入爲根節點 由規則1,直接染色爲黑色
②紅黑樹不空,插入節點爲子節點
1、插入節點的父節點爲紅色 如果插入紅色違背規則5;插入黑色,違背規則4、5
2、插入節點的父節點爲黑色 如果插入紅色,不違背規則5;插入黑色,違背規則5

從上表我們可以發現,無論父節點是紅色還是黑色,插入黑色都會違背規則5,可能違背規則4,必然要進行修復。而插入紅色有可能違背規則5,也有可能不違背而不需要進行修復。那麼從邏輯上我們肯定是優先考慮插入紅色,然後把問題簡化爲——什麼情況下插入紅色節點會破壞紅黑樹結構性質而需要修復?怎麼修復?

因此我們的插入規則是:插入節點是根節點,則插入節點爲黑色。不是,則插入節點顏色爲紅色。這個時候我們只需要關心父節點是否爲紅色。

下面約定一下我習慣的叫法(不喜勿噴。。):
雙親Parent我喜歡叫父節點,父節點兄弟叫Uncle,父節點的孩子我叫Son,父節點的父節點我用G表示爺爺。

1.4、左子樹的插入節點

情況1、父節點爲黑色,插入紅色節點

情況1
我們插入節點爲紅色,不違背5個規則。

情況2、父節點爲紅色,插入紅色節點

  • 是在左子樹插入
  • 是在右子樹插入
    因爲二叉樹是對稱的。執行的操作是相對的。我們搞定了右邊,必然能搞定左邊。

我喜歡從左邊開始考慮,那就先學左邊的吧。

情況2.1 父節點的兄弟,Uncle也是紅色,祖父必爲黑色

情況2.1
在這裏插入圖片描述
紅色節點的孩子不能是紅色,這時不管 Son 是 父節點F 的左孩子還是右孩子,只要同時把 父節點F 和 Uncle節點U 染成黑色,爺爺G 染成紅色即可。這樣這個子樹左右兩邊黑色個數一致,也滿足特徵 4。

但是這樣改變後 G 染成紅色,G 的父親如果是紅色豈不是又違反特徵 4 了?
因此需要從插入節點往上,一直檢查,如以 節點 G 爲新的調整節點,再次進行調整操作,以此循環,直到父親節點不是紅的,就沒有問題了。

情況2.2 父節點的兄弟,Uncle是黑色,祖父必爲黑色

第一種 插入的是左孩子

插入紅色節點Son,違背規則4,但是單純依靠顏色變換,發現將Uncle節點U變爲紅色,也就是把該條路徑的黑色高度減少了1,違背了規則5。那如果把F塗成黑色,又會導致F所在路徑黑色高度加一。那麼該怎麼辦呢?
在這裏插入圖片描述

這個時候,我們需要用到旋轉了。在這裏插入圖片描述從而滿足了規則。

第二種 插入的是右孩子(先轉變爲第一種)

當插入的節點Son是F的右孩子時,同平衡二叉樹AVL類似,需要先左旋,轉化爲上述的情況,然後進行同樣的操作。
在這裏插入圖片描述
這個時候,就相當於Son變爲了父節點,插入節點爲F,插入節點爲左孩子的情況。即第一種情況。

1.5、右子樹的插入節點(與左子樹操作相對即可)

右子樹的插入則執行相反操作即可。

1.6、代碼實現(TreeMap源碼)

  /** From CLR */
    private void fixAfterInsertion(Entry<K,V> x) {//x是要插入的節點
        x.color = RED;//直接染色爲紅色

        while (x != null && x != root && x.parent.color == RED) {//當其父節點爲紅色,就需要調整,否則直接插入即可。
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {//父節點爲左孩子,處理左子樹部分。
                Entry<K,V> y = rightOf(parentOf(parentOf(x)));//獲取Uncle
                if (colorOf(y) == RED) {//判斷Uncle的顏色,紅色就是第一種情況,只需要將父親和Uncle設置爲黑色。祖父變爲紅色,然後一直向上檢測
                    setColor(parentOf(x), BLACK);//父節點黑色
                    setColor(y, BLACK);//Uncle黑色
                    setColor(parentOf(parentOf(x)), RED);//爺爺節點黑色
                    x = parentOf(parentOf(x));//將當前節點更新爲爺爺節點,繼續檢測
                } else {//Uncle是黑色,先得判斷x是左孩子插入還是右孩子插入
                    if (x == rightOf(parentOf(x))) {//右孩子插入,則額外需要對父親左旋一次,化簡爲第一種情況:Son變爲父親,父親變爲Son;
                        x = parentOf(x);
                        rotateLeft(x);
                    }//然後用第一種情況:將父節點設置爲黑色, 爺爺設置爲紅色,然後右旋。第一種情況是必須執行的。無論左孩子還是右孩子插入。
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateRight(parentOf(parentOf(x)));
                }
            } else {//處理右子樹的操作與左子樹相對。
                Entry<K,V> y = leftOf(parentOf(parentOf(x)));//獲取Uncle
                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;//根節點保證爲黑色。
    }

1.7 紅黑樹的刪除節點

我們先來回顧以下二叉查找樹是如何刪除節點的:

情況類型 處理方法
情況1:要刪除的節點P的子樹都是空 直接刪除P節點即可
情況2:要刪除的節點P的子樹只有一顆子樹是空的 將刪除節點P的位置替換爲其非空子樹
情況3:要刪除的節點P的兩顆子樹都不空 先找到P節點的後繼結點S,交換S和P節點,轉化爲了情況1或者2.

回顧以下知識點就開始了。
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
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) {//情況3,左右子樹不空則需要轉化
            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);//判斷情況1還是情況2

        if (replacement != null) {//情況2,有一顆子樹不空,替代要刪除的節點即可。
            // 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 { //  No children. Use self as phantom replacement and unlink.情況1,沒有子樹
            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;
            }
        }
    }

1.8、刪除節點以後重新調整顏色?(超重要的理解)

首先我們先利用枚舉:

  • 刪除的節點是紅色,情況1、2下——>不會違背規則,不需要調整;而情況3下,會轉化爲情況1或者情況2進行刪除,也不違背規則,不需要調整。
  • 刪除的節點是黑色——>違背了規則5,減少了某些路徑上的黑色高度,需要調整

所以我們只需要考慮當刪除的節點是黑色時,如何調整即可。

調整策略分析

爲了保證刪除節點父節點左右兩邊黑色節點數一致,需要重點關注父節點沒刪除的那一邊節點是不是黑色(即考慮刪除節點的兄弟節點那邊的樹)。如果刪除後父節點另一邊比刪除的一邊黑色節點多,就要想辦法搞到平衡,具體的平衡方法有如下幾種方法:

  • 把父節點另一邊(即刪除節點的兄弟樹)其中一個節點弄成紅色,實現讓兄弟樹也少一個黑色
  • 或者把另一邊多的黑色節點轉過來一個

刪除節點在父節點的左子樹還是右子樹,調整方式都是對稱的,這裏以當前節點爲父節點的左孩子爲例進行分析。

第一步

如果刪除節點(黑色節點如12)以後的X的兄弟是紅色,則兄弟的兒子都是黑色,執行圖中操作:
在這裏插入圖片描述
進入第二步。

第二步

1、如果X現在的兄弟是黑色,且兄弟的兩個孩子都是黑色,執行如圖操作。然後跳到第三步。
在這裏插入圖片描述

注意這一類型需要將X更新爲其父節點。

2、如果X現在的兄弟Y是黑色,兄弟節點Y的孩子至多有一個是黑的,執行如圖操作,然後跳到第三步。(這一步必然讓X指向根節點
在這裏插入圖片描述
這是2類的實例圖片,不同於上面的圖。
在這裏插入圖片描述

第三步

  • 如果研究的不是根節點並且是黑的,重新進入第一種情況,研究上一級樹;
  • 如果研究的是根節點或者這個節點不是黑的,就退出
    把研究的這個節點塗成黑的。
    在這裏插入圖片描述
    流程圖實現理解:
    在這裏插入圖片描述

1.9、TreeMap的刪除後調整的源碼:

  /** From CLR */
    private void fixAfterDeletion(Entry<K,V> x) {
        while (x != root && colorOf(x) == BLACK) {//刪除節點以後的當前節點爲黑色才需要調整
            if (x == leftOf(parentOf(x))) {//X爲左孩子
                Entry<K,V> sib = rightOf(parentOf(x));//獲取右兄弟

                if (colorOf(sib) == RED) {//判斷右兄弟是否爲紅色,是則必然兩個孩子黑色。執行第一步:
                    setColor(sib, BLACK);//兄弟設置爲黑色
                    setColor(parentOf(x), RED);//父節點設置爲紅色
                    rotateLeft(parentOf(x));//左旋父節點
                    sib = rightOf(parentOf(x));//更新右兄弟。
                }
				//進入第二步
                if (colorOf(leftOf(sib))  == BLACK &&
                    colorOf(rightOf(sib)) == BLACK) {//兄弟節點的孩子兩個都是黑色
                    setColor(sib, RED);//將兄弟設置爲紅色
                    x = parentOf(x);//更新X節點,準備研究上一層樹結構
                } else {
                    if (colorOf(rightOf(sib)) == BLACK) {//右兄弟孩子是右黑左紅才執行這種操作,左黑右紅不需要。
                        setColor(leftOf(sib), BLACK);//將右兄弟左孩子設置爲黑色
                        setColor(sib, RED);//右兄弟設置爲紅色
                        rotateRight(sib);//右兄弟右旋
                        sib = rightOf(parentOf(x));//更新右兄弟
                    }
                    //右兄弟左黑右紅
                    setColor(sib, colorOf(parentOf(x)));//兄弟節點與X節點的父節點一致
                    setColor(parentOf(x), BLACK);//設置X節點爲黑色
                    setColor(rightOf(sib), BLACK);//兄弟右孩子設置爲黑色
                    rotateLeft(parentOf(x));//X父節點左旋
                    x = root;//研究根節點。
                }
            } else { // symmetric 相對操作。
                Entry<K,V> sib = leftOf(parentOf(x));

                if (colorOf(sib) == RED) {
                    setColor(sib, BLACK);
                    setColor(parentOf(x), RED);
                    rotateRight(parentOf(x));
                    sib = leftOf(parentOf(x));
                }

                if (colorOf(rightOf(sib)) == BLACK &&
                    colorOf(leftOf(sib)) == BLACK) {
                    setColor(sib, RED);
                    x = parentOf(x);
                } else {
                    if (colorOf(leftOf(sib)) == BLACK) {
                        setColor(rightOf(sib), BLACK);
                        setColor(sib, RED);
                        rotateLeft(sib);
                        sib = leftOf(parentOf(x));
                    }
                    setColor(sib, colorOf(parentOf(x)));
                    setColor(parentOf(x), BLACK);
                    setColor(leftOf(sib), BLACK);
                    rotateRight(parentOf(x));
                    x = root;
                }
            }
        }

        setColor(x, BLACK);//根節點或者研究節點設置爲黑色。
    }

完全和分析一致。很棒。

1.10、對比AVL樹、一般的二叉查找樹

來吧,讓我們來看一下知乎大佬說的:
在這裏插入圖片描述

在這裏插入圖片描述
在這裏插入圖片描述

參考博客:
https://juejin.im/entry/58371f13a22b9d006882902d#comment
維基百科
https://www.jianshu.com/p/e136ec79235c
https://blog.csdn.net/u012142247/article/details/80250166

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