Java Collections Framework 源碼分析(5.3 - TreeMap, 紅黑樹的刪除)

本篇是 TreeMap 和紅黑樹源碼分析的最後一篇了,此次會結合 TreeMap 的源碼教你們紅黑樹刪除節點的算法。紅黑樹的刪除算法要比插入更爲複雜些,可是也沒必要擔憂,本文會用簡單明瞭的解釋,並結合 JDK 的源碼讓你瞭解紅黑樹的刪除算法。java

在正文開始以前,還請你們確保本身理解以前兩篇文章中講述的知識點,若是有些遺忘也不妨再次快速的複習一下。node

Java Collections Framework 源碼分析(5.1 - Map, TreeMap, 紅黑樹)算法

Java Collections Framework 源碼分析(5.2 - TreeMap, 紅黑樹的插入)segmentfault

TreeMap 的 remove 方法

Map 上的 remove 方法的做用是從容器內移除鍵值對,咱們先看一下 TreeMap 上的實現:微信

public V remove(Object key) {
    Entry<K,V> p = getEntry(key);
    if (p == null)
        return null;

    V oldValue = p.value;
    deleteEntry(p);
    return oldValue;
}

經過 getEntry 方法從紅黑樹中獲取對應的 Entry 對象,若是對應 key 的 Entry 不存在則直接返回 null。不然會執行 deleteEntry 方法,並將返回舊的值。讓咱們繼續往下看 deleteEntry 方法的實現。數據結構

deleteEntry

deleteEntry 的代碼來看主要分爲兩個部分:源碼分析

  1. 從紅黑樹(平衡二叉樹)中刪除節點。
  2. 從新調整樹的狀態,恢復平衡。

讓咱們先了解一下平衡二叉樹刪除節點的算法,具體算法其實很簡單,須要區分兩種不一樣的狀態。咱們先說簡單的,若是當前刪除的節點只有一個非空子節點,那麼只須要直接刪除就好了,即把本身子節點和父節點創建鏈接關係便可。而當本身有兩個非空子節點時,則須要按照下面的順序進行刪除操做:學習

  1. 找到當前刪除節點的的前驅節點或是後繼節點(前驅與後繼的概念稍後會說)。
  2. 前驅(或是後繼)節點的值複製給須要刪除的節點
  3. 按照第一種狀況刪除那個前驅(或是後繼)節點

在詳細解釋以前,咱們先說前驅後繼的概念。由於平衡二叉樹實質上是一種排序的數據結構,若是把它拉成一條直線,其實就是一個鏈表。而前驅的意思就是小於且最接近當前節點的節點,相應的後繼就是大於且最接近的節點,具體能夠看下面的圖:spa

0.png

假設圖中40(紅色)的節點爲當前節點,那麼35(藍色)節點爲前驅節點45(綠色)節點爲後繼節點操作系統

瞭解了這些背景知識以後,能夠看一下 deleteEntry 的代碼了。

// If strictly internal, copy successor's element to p and then make p
// point to successor.
if (p.left != null && p.right != null) {
    Entry<K,V> s = successor(p);
    p.key = s.key;
    p.value = s.value;
    p = s;
} // p has 2 children

這部分代碼按照咱們以前所說的算法,先執行了有兩個非空子節點狀況下的邏輯,同時這裏選擇的是使用 successor 後繼節點進行替換。很容易看出在使用 successor 得到當前節點的後繼節點後,將後繼節點的值複製給了當前節點,而後將須要刪除節點的引用指向了後繼節點。

successor 方法我就不在解釋了,我建議你能夠去看一下,看看是否符合咱們以前的定義。相應的,在 TreeMap 的源碼中還有一個 predecessor 方法是獲取當前節點前驅節點,也值得看一下。

而後咱們接着往下看:

// 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);
}

這裏是進行節點刪除的具體代碼,此時須要刪除的 P 節點有可能已是指向後繼節點了,可是不管如何,刪除的邏輯都是同樣的,都是從新創建父節點與子節點之間的關聯,並移除與要刪除節點間的關聯。至此第一步刪除節點的操做已經完成了,接下來就是要對樹進行從新平衡,以符合紅黑樹的要求。

fixAfterDeletion

從上面代碼片斷中能夠看出,在進行節點刪除以後,調用了 fixAfterDeletion 方法,還記得上一篇中有個相似的 fixAfterInsertion 嗎?不難猜出,這個 fixAfterDeletion 就是在刪除節點後對二叉樹從新平衡的方法,讓咱們先參考 wiki 上的算法定義。

相對插入而言刪除的算法稍微更復雜些,須要執行 6 步操做。但也不用慌,耐心往下看(算法描述依然採用以前的 N,P 等縮寫)。

  1. N 是否爲根節點,若是是,那麼就直接終止,不然執行第 2 步操做。
  2. S 節點,若是 S 節點顏色爲紅色:

    1. P 改成紅色
    2. S 改成黑色
    3. N 是否爲 P 的左節點

      1. 是:將 P 左轉
      2. 否:將 P 右轉

執行第 3 步操做

  1. P,S,S.left,S.right 這些節點的顏色都爲黑色:

    1. S 改成紅色
    2. 將 P 做爲參數,從第 1 步開始從新執行

不然執行第 4 步

  1. P 爲紅色,S,S.left,S.right 都爲黑色:

    1. S 改成紅色
    2. P 改成黑色
    3. 終止操做

不然執行第 5 步

  1. S 的顏色爲黑色

    1. N 爲 P 的左節點,S.right 爲黑色,S.left 爲紅色

      1. S 改成紅色
      2. S.left 改成黑色
      3. 將 S 右轉
    2. N 爲 P 的右節點,S.right 爲紅色,S.left 爲黑色

      1. S 改成紅色
      2. S.right 改成黑色
      3. 將 S 左轉

執行第 6 步操做

  1. 將 S 的顏色改成 P 的顏色

P 改成黑色

1. N 爲 P 的左節點
    1. S.right 改成黑色
    2. 將 P 左轉
2. N 爲 P 的右節點
    1. S.left 改成黑色
    2. 將 P 右轉

上手一看算法邏輯可能很繁瑣,其實仔細看一下,不少都是對稱的,你只須要記住一半就好了。如今結合 TreeMap 的代碼看一下:

private void fixAfterDeletion(Entry<K,V> x) {
    while (x != root && colorOf(x) == BLACK) {
        if (x == leftOf(parentOf(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);
            } else {
                if (colorOf(rightOf(sib)) == BLACK) {
                    setColor(leftOf(sib), BLACK);
                    setColor(sib, RED);
                    rotateRight(sib);
                    sib = rightOf(parentOf(x));
                }
                setColor(sib, colorOf(parentOf(x)));
                setColor(parentOf(x), BLACK);
                setColor(rightOf(sib), BLACK);
                rotateLeft(parentOf(x));
                x = root;
            }
        }
......

能夠看到 fixAfterDeletetion 方法的邏輯與咱們描述的算法在順序上稍有不一樣,它在一開始的分支條件是區分當前節點是左節點仍是右節點。緊接着的:

if (colorOf(sib) == RED) {
    setColor(sib, BLACK);
    setColor(parentOf(x), RED);
    rotateLeft(parentOf(x));
    sib = rightOf(parentOf(x));
}

這部分能夠看到對應咱們算法的第 2 步。須要注意的是 sib = rightOf(parentOf(x));,那是由於發生旋轉後,sib 也發生了變化,須要從新獲取。接下來的:

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

也能看到是對應算法的第 3 步,將 x 設爲了 parent,而後從新開始 while 循環。以後的分支也均可以對應到算法的剩餘步驟,我把這部分代碼解讀的工做流給你了,不妨本身拿筆和紙出來,將代碼和我所描述的算法一一對應,看看能不能對上。

結語

至此,TreeMap 和紅黑樹的全部代碼都分析完了。最後這篇節點刪除算法拖了好久,期間有人也私信問我,爲何要學習紅黑樹?爲何要學習數據結構?咱們工做中就是 CRUD ,什麼排序,什麼查找,都是用現成的呀,有問題嗎?這是個頗有趣的問題,在我工做過程當中有不少人問過我相似的問題,能夠把數據結構和紅黑樹替換成其餘的許多名詞,例如操做系統,編譯原理,JVM 等等,等等。我想後面會花時間用一篇單獨的文章來回答這個,而在這裏我只想說對於這些底層知識的掌握,決定了你能力的上限,換而言之也決定了你作什麼和不能作什麼 。

接下來應該是 Java Collections Framework 最後一部分了,也就是 HashMap 的源碼解析,但願你不要錯過。

歡迎關注個人微信號「且把金針度與人」,獲取更多高質量文章
QR.png

相關文章
相關標籤/搜索