算法基礎--從TreeMap看紅黑樹

紅黑樹(Red Black Tree) 是一種自平衡二叉查找樹,相對於普通的二叉樹具備經過自旋和變色來保持樹兩端保持平衡的特色,從而得到較高的查找性能。 紅黑樹的最壞狀況運行時間也是很是良好的,而且在實踐中是高效的: 它能夠在O(log n)時間內作查找,插入和刪除。html

  • 二叉查找樹

    在正式介紹紅黑樹前,先簡要介紹下二叉查找樹(BST),二叉排序樹或者是一棵空樹,或者是具備下列性質的二叉樹:node

    • 若左子樹不空,則左子樹上全部節點的值均小於它的根節
    • 若右子樹不空,則右子樹上全部節點的值均大於它的根節點的值
    • 左、右子樹也分別爲二叉排序樹
    • 沒有鍵值相等的節點(這個看實際需求,非固定)

以下圖就是一個典型的二叉查找樹 bash

二叉查找樹
那麼這種樹有什麼好處呢,如同它的名字通常,它對查找很是便利,在上面數據中若是你想查找是否存在11,你只須要分別和10,15,13,11比較就能夠得出,數據越多,它查找的優點就更體現出來。它查找所需的最大次數等同於二叉查找樹的高度。 可是它也有缺陷所在,當構成它的數據致使樹兩端不平衡時,查找性能就大打折扣了
二叉查找樹
當插入的數據是有序的時候,生成的二叉樹就相似與一個鏈表,這種狀況下查找時,就須要遍歷所有數據

  • 總結

    • 最好的狀況是 O(logn):在數據符合徹底二叉樹相似狀況下,其查找性能接近於二分查找,理想的樹的高度爲logN。
    • 最差時候會是 O(n):極端狀況下好比插入的數據是有序的,生成的二叉查找樹就是一個鏈表,這樣樹的高度就爲N。
  • 紅黑樹

    基於上面提到BST存在的問題,一種新的樹--平衡二叉樹(Balanced BST)被提了出來。平衡二叉樹在插入和刪除的時候,會經過旋轉操做將樹的高度一直保持在logN。具備表明性的平衡二叉樹有兩種,分別爲AVL樹和紅黑樹,AVL樹由於性能更差的緣故,在實際運用的狀況下遠不如紅黑樹。數據結構

    紅黑樹(Red-Black Tree,如下簡稱RBTree)的實際應用很是普遍,常見的函數庫,如C++ STL中,不少部分(包括set, multiset, map, multimap)應用了紅黑樹的變體,以及Java中的TreeMap,TreeSet, Java8中的HashMap的實現也將鏈表替換成了紅黑樹。函數

RBTree爲了保持樹嚴格的平衡性質,在原來BST的基礎上添加了一下五點性質:性能

  • 根節點是黑色。
  • 樹的任一節點是紅色或黑色。
  • 每一個紅色節點的兩個子節點都是黑色。
  • 從任一節點到其每一個葉子的全部路徑都包含相同數目的黑色節點。
  • 空節點默認是黑色的
class TreeMapEntry{
        K key;
        V value;
        TreeMapEntry<K,V> left;
        TreeMapEntry<K,V> right;
        TreeMapEntry<K,V> parent;
        boolean color = BLACK;
}
RBTree節點的數據結構
複製代碼

RBTree在本質上仍是一棵BST樹,可是它在插入和刪除數據的時候會經過變色和自旋來保持樹的平衡,即保證樹的高度在[logN,logN+1],將樹的查找時間複雜度始終保持在logN,同時RBTree的插入和刪除時間複雜度也都是logN,因此RBTree的查找接近於理想的BST。學習

  • 紅黑樹的自旋

    RBTree的自旋主要目的是爲了讓節點的顏色符合上面的性質,從而使樹的高度達到平衡。RBTree的旋轉分爲左旋和右旋,左邊子節點升到父節點位置爲右旋,右邊子節點升到父節點爲左旋。
    左旋示意圖
    如上圖中,當某個節點(100)左旋時,它自身往右降一級,左節點不變,右節點(150)斷開而後連上右節點(150)的左節點(125),而後右節點(150)當家作主稱爲父節點,至此完成一個左旋

右旋示意圖
同理,當某個節點(100)右旋時,它自身往左降一級,右節點不變,左節點(50)斷開而後連上左節點(50)的右節點(75),而後左節點(50)當家作主稱爲父節點,至此完成一個右旋

RBTree的自旋主要是由於插入或刪除後節點的顏色不符合上述的五條性質,致使樹總體不平衡,須要經過自旋對樹進行降層保持樹的平衡網站

Java的RBTree就是一個典型的紅黑樹例子,下面也就TreeMap的源碼來對解析RBTree的插入和刪除操做ui

  • RBTree的插入

每一個新插入的節點都是紅色的,若是插入的父節點是黑色的,那麼操做結束。若是父節點是紅色,那麼則違反了規則3:每一個紅色節點的兩個子節點都是黑色,則須要改變父類的顏色,若是父類顏色和祖父類衝突,那麼就須要繼續變色,甚至是自旋來使樹節點顏色符合規則。spa

public V put(K key, V value) {
        TreeMapEntry<K,V> t = root;
        // 若是當前沒有數據,就用此點當作根節點
        if (t == null) {
            compare(key, key);
            root = new TreeMapEntry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        TreeMapEntry<K,V> parent;
        // 比較大小的方式,若是已經自定義過就用本身設定的,否則就用系統默認的比較方式
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        //找到插入的父節點,生成當街節點,而後根據大小放在左節點仍是右節點
        TreeMapEntry<K,V> e = new TreeMapEntry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
       // 數據插入完成後開始對樹進行顏色平衡處理
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }
複製代碼

fixAfterInsertion

//這裏對顏色的處理其實只看一半就好了,你會發現else後面的代碼和上面是同樣的,只不過 
 //左右作一下鏡像處理
 private void fixAfterInsertion(TreeMapEntry<K,V> x) {
        //新添加的節點設爲紅色
        x.color = RED;
        /** **/
        由於規則3:每一個紅色節點的兩個子節點都是黑色。新添加的節點都爲紅色,
        那麼循環條件 要加上父類不爲紅色
        當新加入的點爲根節點時也不必循環了,直接最後面設置爲黑色便可
        **/
        while (x != null && x != root && x.parent.color == RED) {
              //若是當前父節點爲祖父節點的左節點
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
              //這個y是祖父節點的另外一個節點,從關係上來講,就是添加節點的叔叔節點
                TreeMapEntry<K,V> y = rightOf(parentOf(parentOf(x)));
                /**
                若是叔叔節點爲紅色,那麼一樣由於規則3可知祖父節點必定爲黑色
                此時把父節點設爲黑色和叔叔節點 設爲黑色,把祖父節點設爲紅色
                這麼作的主要目的是爲了符合規則4:從任一節點到其每一個葉子的全部路徑都包含相同數目的黑色節點
                把祖父節點變成紅色後,由於不知道祖父的父節點顏色,由於可能會違反規則3(祖父的父節點爲紅色),因此須要對祖父節點進行循環校驗
                **/
                if (colorOf(y) == RED) {
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {
                    /*
                     * 若是叔叔節點爲黑色,由於當前節點和父節點都爲紅色,祖父節點也必定爲黑色
                     * 這種狀況必定是通過上面那種狀況變色後得出來的,由於叔叔和祖父是黑色,父親節點是紅色,
                     * 這種狀況就違反了規則4:從任一節點到其每一個葉子的全部路徑都包含相同數目的黑色節點
                     * 由於此時的叔叔節點比父節點多了一個黑色,這種狀況只有多是由於原來父節點是黑色的,
                     * 因爲添加了新節點後由於上面的變換setColor(parentOf(parentOf(x)), RED);致使的
                     *
                     * 這種狀況下單純變色已經無論用了,只能經過自旋來平衡
                     * 先把父節點變黑,祖父節點變紅
                     * 這時候經過右旋把父節點的右孩子變成祖父節點的左孩子,這時候達到顏色平衡,詳情後面看動圖例子
                     *
                     *
                     * 上面的操做是正常狀況下的操做,可是在這個操做前須要先作一個判斷
                     * 若是當前節點是父類的右孩子,那麼須要對父節點進行左旋轉
                     * 由於若是不作這個操做的話,因爲當前節點是紅色的,上面操做【右旋把父節點的右孩子變成祖父節點的左孩子】
                     * 也就是把當前節點給了祖父的左孩子,可是由於祖父節點已經被設爲了紅色,這樣兩個紅色節點就違反了規則3,
                     * 因此若是當前節點是父類的右孩子時須要對父節點進行左旋,
                     * 左旋後當前節點變成了父節點,父節點變成了左孩子,紅色的節點也移到了左邊,移到祖父節點的也是黑色的,不會起衝突
                     * 這樣,最多經過兩次自旋就能夠解決衝突
                     */
                    if (x == rightOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateLeft(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateRight(parentOf(parentOf(x)));
                }
            } else {
                //這裏就不贅述了,原理如出一轍,只是方向相反罷了
                TreeMapEntry<K,V> y = leftOf(parentOf(parentOf(x)));
                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;
    }
複製代碼
  • 實例

    枯燥的講解永遠沒有生動的例子有效,下面簡單舉幾個例子

  • 單純添加一個數據,不變色也不自旋

添加150

  • 添加一個數據後,進行變色

while (x != null && x != root && x.parent.color == RED) {
   TreeMapEntry<K,V> y = rightOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
複製代碼

變色

經過結合代碼能夠看到添加新節點25後,由於父節點(50)是紅色的,進入循環。又由於叔叔節點是紅色的,因此將父節點和叔叔節點設爲黑色,祖父節點設爲紅色,再將當前節點x設爲祖父節點,由於x成了根節點,因此退出循環,在最後將根節點設爲黑色

  • 添加一個數據後,進行變色和一次自旋,只通過了一次自旋,註釋的代碼並無調用

if (colorOf(y) == RED) {
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {
                  //  if (x == rightOf(parentOf(x))) {
                  //      x = parentOf(x);
                  //     rotateLeft(x);
                  //  }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateRight(parentOf(parentOf(x)));
                }
複製代碼

一次自旋

這裏能夠看到添加一個新數據5後,第一步由於父節點(10)和叔叔節點(26)爲紅色,因此作了一次變色,把父節點(10)和叔叔節點(26)變黑,祖父節點(25)變紅,當前節點x變爲祖父節點(25)

這裏x(25)變紅色後和父節點(50)顏色起了衝突。可是爲何這裏不能繼續經過變色來平衡呢,由於這裏若是將25或者50節點中一個變爲黑色後,那麼最左側這一條路徑的黑色節點數量就比其餘路徑黑色節點數量多,因此這時候就須要進行自旋

因此將父節點(50)設爲黑色,將祖父節點(100)設爲紅色,而後右旋,。右旋完成後,由於父節點(50)變成了黑色,退出了循環,平衡完成

  • 添加一個數據後,進行變色和兩次自旋

if (colorOf(y) == RED) {
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {
                   if (x == rightOf(parentOf(x))) {
                       x = parentOf(x);
                       rotateLeft(x);
                   }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateRight(parentOf(parentOf(x)));
                }
複製代碼

兩次自旋
這裏第一次變色就不說了,通過第一次變色後,當前節點X爲75

通過變色後能夠看到叔叔節點(150)爲黑色,因此須要進行自旋。可是又由於x(75)是父節點(50)的右孩子,因此須要對父節點(50)進行左旋,同時將當前節點x設爲50。

第一次左自旋後,當前節點x爲50,而後將左旋後的父節點(75)設爲黑色,祖父節點(100)設爲紅色,而後對祖父節點(100)進行右自旋,右自旋後當前節點的父節點爲(75)爲黑色,退出循環,平衡完成

  • 總結

學習紅黑樹的過程當中最困惑的不是自旋和變色,而是自旋和變色的時機,鑽了很多死衚衕。這裏總結一下(僅當是左邊樹狀況下,右邊樹狀況只要將左右顛倒便可):

一、當叔叔節點爲紅色時,將新加節點和父節點變色便可

二、當叔叔節點爲黑色時,若是當前節點位於左節點,那麼將父節點變黑,祖父節點變紅,而後右旋便可。

三、當叔叔節點爲黑色時,若是當前節點位於右節點,那麼須要先以父節點左旋,而後/2操做便可

  • RBTree的刪除

刪除操做相對於插入多了一層複雜度,但仍是有跡可循。

刪除操做會先刪除節點,若是是葉子結點就直接刪除,若是非月子節點,會先遍歷找到該點刪除後的繼承點。在刪除節點後,須要作修復操做,使樹從新達到顏色和高度平衡。

修復操做是隻有在刪除黑色節點時纔有,由於刪除黑色節點會違反規則4:從任一節點到其每一個葉子的全部路徑都包含相同數目的黑色節點。須要作的處理是從兄弟節點上借調黑色的節點過來,若是兄弟節點沒有黑節點能夠借調的話,就只能往上追溯,將每一級的黑節點數減去一個,使得整棵樹符合紅黑樹的定義。

刪除操做的整體思想是從兄弟節點借調黑色節點使樹保持局部的平衡,若是局部的平衡達到了,就看總體的樹是不是平衡的,若是不平衡就接着向上追溯調整。

private void deleteEntry(TreeMapEntry<K,V> p) {
        modCount++;
        size--;
      /**
       * 若是被刪除的節點P非葉子節點,那麼須要在刪除他以前找到合適的點來繼承這個節點位置
       */
        if (p.left != null && p.right != null) {
             //找到繼承點後,將被刪除點的值都賦給繼承點,這裏注意了,繼承點s的關係賦值給了p            
             //變成了要刪除的點
             //這個節點是找位於p點右子樹的最小數節點
            TreeMapEntry<K,V> s = successor(p);
            p.key = s.key;
            p.value = s.value;
            p = s;
        } // p has 2 children

      /**
       * 若是進入了上面的循環,最後的p是位於刪除節點右子樹的最左端點 ,並且有且最多隻有一 
       * 個右子節點,這時候這個``replacement``就是就是p.right
       * 
       * 若是沒有通過上面的循環,那麼被刪除節點有且最多隻有一個子節點
       * 並且這個惟一的子節點必然是葉子節點(由於若是該節點還有子節點就違反規則4了)
       * 這時候用這個葉子節點替換便可
       */
        TreeMapEntry<K,V> replacement = (p.left != null ? p.left : p.right);
        if (replacement != null) {
            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;
            p.left = p.right = p.parent = null;
             /**
               * 這裏兩種可能
               * 1:若是該P有兩孩子,那麼他變成了找繼承者出來的s,若是他是黑色節點,
               * 那麼等因而刪除節點後面缺了一個黑色s(拿去頂刪除節點位置了),因此須要調整樹
               * 
               * 2:若是該p沒有兩個孩子節點,那麼他就是被刪除的節點,刪除了黑色節點就意味着有一邊樹少了一個黑色
               * 比另外一邊樹總體路徑就少了一個黑色節點,因此也須要調整樹
               * 
               * 若是兩種狀況都是紅色的話,對總體沒有影響,因此不須要變色
               * 
               */
            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.
             /**
               * 若是被刪除節點p沒有孩子節點,若是p點是黑色,由於刪除了黑色節點違反了規則4
               * 那麼須要調整樹,若是p點爲紅色,那麼直接刪除便可
               */
            if (p.color == BLACK)
                fixAfterDeletion(p);
            //顏色調整完畢後將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;
            }
        }
    }
複製代碼

找尋刪除節點後合適的繼承點

static <K,V> TreeMapEntry<K,V> successor(TreeMapEntry<K,V> t) {
        if (t == null)
            return null;
        else if (t.right != null) {
            TreeMapEntry<K,V> p = t.right;
            while (p.left != null)
                p = p.left;
            return p;
        } else {
            TreeMapEntry<K,V> p = t.parent;
            TreeMapEntry<K,V> ch = t;
            while (p != null && ch == p.right) {
                ch = p;
                p = p.parent;
            }
            return p;
        }
    }
複製代碼

fixAfterDeletion

//這裏和前面同樣,看一半邏輯就好了,另外一半邏輯是鏡像對稱的
 private void fixAfterDeletion(TreeMapEntry<K,V> x) {
        while (x != root && colorOf(x) == BLACK) {
            if (x == leftOf(parentOf(x))) {
                TreeMapEntry<K,V> sib = rightOf(parentOf(x));
                    /**
                      * 這裏和插入操做不同的地方是着重判斷兄弟節點的顏色,而不是叔叔的
                      * 若是兄弟節點是紅色,那麼由於本身這邊刪除了一個黑色,總體黑色就比兄弟那邊少一個、
                      * 這時候須要將父節點變紅,兄弟節點變黑,經過左旋從兄弟節點那邊借一個黑色節點過來
                      */
                if (colorOf(sib) == RED) {
                    狀況一
                    setColor(sib, BLACK);
                    setColor(parentOf(x), RED);
                    rotateLeft(parentOf(x));
                    sib = rightOf(parentOf(x));
                }
                 /**
                   * 這裏注意,就算兄弟節點沒有孩子節點,他的左右樹鏈接的是null,null節點也是黑色的
                   * 這裏的判斷只是判斷他是否有紅色孩子節點
                   * 若是兄弟節點是紅色的,通過上面步驟變化,這裏兄弟節點變成了紅色
                   */
                if (colorOf(leftOf(sib))  == BLACK &&
                    colorOf(rightOf(sib)) == BLACK) {
                    狀況二
                    //在兄弟節點是黑色且兩孩子都是黑色的狀況下,須要將兄弟節點變紅
                    //繼續循環調整其父節點
                    setColor(sib, RED);
                    x = parentOf(x);
                } else {
                   /**
                    * 進入這個右孩子是黑色的判斷,那麼左孩子必定是紅色(由於null節點也是黑色)
                    * 將兄弟節點變紅,左孩子(紅色)變黑,右旋將紅色節點移到右子樹
                    */
                    if (colorOf(rightOf(sib)) == BLACK) {
                    狀況三
                        setColor(leftOf(sib), BLACK);
                        setColor(sib, RED);
                        rotateRight(sib);
                        //通過右旋後  找出新的叔叔節點
                        sib = rightOf(parentOf(x));
                    }
                    狀況四
                /**
                 * 到了這一步,兄弟節點是黑色的,兄弟節點的左孩子是黑色的,右孩子是紅色的
                 * 這時候刪除節點後須要從兄弟節點那邊先借節點過來
                 * 先把兄弟節點顏色賦值爲父節點顏色,再把兄弟節點的右孩子和父節點變爲黑色
                 * 由於左旋前父節點之前都是平衡的,兄弟節點左旋後替代父節點的位置就要和父節點顏色一致
                 * 而後兄弟節點的位置由兄弟節點的右孩子接替了,顏色要變成黑色,
                 * 左孩子,也就是左旋前的父節點也要變得和上面接替的右孩子同樣變成黑色,這樣就平衡了
                 * 最後設爲root 退出循環,
                 */
                    setColor(sib, colorOf(parentOf(x)));
                    setColor(parentOf(x), BLACK);
                    setColor(rightOf(sib), BLACK);
                    rotateLeft(parentOf(x));
                    x = root;
                }
            } else { // symmetric
                TreeMapEntry<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.被刪除的節點沒有孩子節點,即葉子節點。可直接刪除。
    • 2.被刪除的節點只有一個孩子節點,那麼直接刪除該節點,而後用它的孩子節點頂替它的位置。這種狀況下他不用找繼承點,replacement爲他的孩子節點。
    • 3.被刪除的節點有兩個孩子節點。這種狀況下要找他的繼承點successor。找到繼承點後用繼承點S替換被刪除點X,replacementM則爲繼承點S的左或右孩子節,若S沒有孩子後續操做和操做1同樣,若是有則後續操做和操做2同樣,這樣就回到了最初的問題。
  • 第二步:經過自旋和從新填色等一系列操做來修正樹,使之從新成爲一棵紅黑樹。

  • 實例

上面動圖演示的那個網站的紅黑樹源碼是從左子樹找最大點來當繼承點,而TreeMap是從右子樹找最小點來當繼承點,因此動圖就不適合用了,因此我根據TreeMap畫了個自定義view,可是沒有動態效果了,僅供展現。

  • 刪除沒有孩子節點的紅色節點就不演示了,直接刪除便可

  • 刪除有一個孩子節點的紅色節點,也只須要將那個孩子節點替換爲刪除節點便可。

  • 刪除有兩個孩子的紅色節點(50),且右孩子節點沒有左右孩子節點時,以下圖:

ZLZA0@H`RB13}Q_4)D4Z)CS.png

if (p.left != null && p.right != null) {
            TreeMapEntry<K,V> s = successor(p);
            p.key = s.key;
            p.value = s.value;
            p = s;
        } // p has 2 children
複製代碼

上圖這種狀況下,由於被刪除點P(50)有兩個孩子節點,因此就須要爲他找繼承successor, 這個點就是P右子樹的最小點也就是75。而後把被刪除點的值變爲繼承點的值,而後被刪除點P就變成了原successor(75)

由於如今的P沒有左右子樹,因此replacement也就爲null,此時P點爲黑色點,因此須要fixAfterDeletion平衡樹

P點在右子樹,因此找到兄弟節點Sib(25),Sib節點爲黑色且左右孩子(null)都爲黑色,此時向上追溯,將P點的父節點也就是原P(紅色的50,現紅色的75)繼續循環。但此時P點就變成了紅色,退出循環,在最後setColor(x, BLACK);將紅色的75變爲黑色,最後刪除P(75)即完成了樹的平衡。

  • 刪除有兩個子節點的紅色節點(100),且子節點還有子節點以下圖:

這個例子我舉完才發現和上面的步驟如出一轍,只不過找尋successor的時候多向下找尋了一層而已,詳細操做看上個例子解析

總結下:當被刪除的節點爲紅色時,操做很簡單,若是隻有一個子節點,就將子節點替換上來。若是有兩個子節點,就經過找尋successor,將successor的值賦給被刪除點,而後將被刪除點替換爲successor便可。

  • 刪除沒有孩子節點的黑色節點,且兄弟節點爲紅色時,以下圖:

兄弟節點爲紅色的光棍黑色節點

這種狀況下比較簡單,由於P點無孩子節點,也就不存在successorreplacement。由於P點這邊樹刪除了一個黑色節點,就比兄弟樹少了一個黑色節點,此時就須要從兄弟那邊借一個黑色節點過來。

因此將sib(75)設爲黑色,父節點(60)設爲紅色,而後左旋,這樣左子樹就從右子樹借了個黑色節點(70)過來。而後此時的sib點從新賦值給了70,爲了助於理解,這裏看下圖

左旋後

if (colorOf(leftOf(sib))  == BLACK &&
                        colorOf(rightOf(sib)) == BLACK) {
                    setColor(sib, RED);
                    x = parentOf(x);
複製代碼

左旋後如圖,P點是待刪除點,70成了新的sib點。此時能夠發現若是P點被刪了那麼最左側這邊樹總體就少了一個黑色節點。此時sib的左節點P由於子節點都爲黑色(null)因此將sib(70)設爲黑色,將P點設爲其父節點,由於父節點(60)爲紅色,因此退出循環,最後setColor(x, BLACK);將60設爲黑色,再將P點刪除即完成平衡。

  • 刪除沒有孩子節點的黑色節點(25),且兄弟節點爲黑色時,以下圖:

兄弟節點爲黑色的光棍節點

和上個例子同樣,也不存在找繼承點的狀況,直接來平衡樹。

由於sib(70)是黑色,sib()的左右節點都爲黑色(null),因此直接sib(75)設爲紅色,而後追溯用father(60)帶入繼續循環。

和上一步原理同樣,此時的sib(150)黑色,左右節點也爲黑色,因而將sib(150)設爲紅色,將父節點(100)帶入繼續循環,但由於100已是root節點,因此退出循環,刪除p點完成平衡

  • 刪除沒有孩子節點的黑色節點(65),兄弟節點爲黑色且其右節點爲黑色時,以下圖:

兄弟節點爲黑色且其右節點爲黑色的光棍黑色節點

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;
                }
複製代碼

這裏看似樹很簡單,可是這個操做是最複雜的一個,這裏要進行兩步自旋,下面就主要解釋下做用。

當sib節點有兩個子節點,可是其顏色不一致時,此時左旋從sib借黑色節點時可能會把紅色節點給過去,因此要若是紅色節點在左邊時先須要作個右旋將紅色節點移到右邊。

右旋完成後,如今要從sib這邊借黑色節點,由於左旋後sib節點就成了原來的父節點,sib右節點就成了原來的sib節點,因此爲了保持顏色平衡,讓他們分別繼承他們要接替位置的顏色,而原來的父節點就接替了被刪除點P的位置,因此將其顏色設爲黑色,至此,平衡完成。

  • 刪除有子節點的黑色節點,以下圖:

圖一
圖二
圖三

當刪除一個有子節點的黑色節點時,將這個操做分解後問題就變成了上面【刪除沒有孩子節點的黑色節點】和【刪除紅色節點】,而後繼續上面的操做便可,這裏給出了三個例子。

圖一:刪除點P(60):找到其繼承點successor(70),賦值變換後這個問題就變成了:刪除一個黑色節點70,且他的兄弟節點爲黑色

圖二:刪除點P(60):找到其繼承點successor(65),replacement點爲70,賦值變換後這個問題就變成了:刪除一個紅色節點70

圖二:刪除點根節點(100):找到其繼承點successor(125),賦值變換後這個問題就變成了:刪除一個黑色節點125

總結:當刪除的黑色節點無子節點時,根據兄弟節點的顏色來作具體操做,當刪除有子節點的黑色節點時,能夠經過找繼承點將問題分別爲刪除一個無子節點的黑色或紅色節點,因此問題就順利簡化了。

  • 總結

東西仍是本身再寫一遍才能看出本身是否真正理解了,在看了幾篇文章後我覺得我理解了紅黑樹,因此我嘗試着按本身理解去寫一篇筆記,結果寫的過程當中發現看似理解,實則根本描述不出其緣由,於是從新又去梳理了一遍,有了不少新的感悟。寫的比較凌亂,如若是以爲文章有錯誤,歡迎指出並交流。

附上紅黑樹演示圖網址

相關文章
相關標籤/搜索