Java提高篇(二七)-----TreeMap


TreeMap的實現是紅黑樹算法的實現,所以要了解TreeMap就必須對紅黑樹有一定的了解,其實這篇博文的名字叫做:根據紅黑樹的算法來分析TreeMap的實現,但是為了與Java提高篇系列博文保持一致還是叫做TreeMap比較好。通過這篇博文你可以獲得如下知識點:

1、紅黑樹的基本概念。

2、紅黑樹增加節點、刪除節點的實現過程。

3、紅黑樹左旋轉、右旋轉的復雜過程。

4、Java 中TreeMap是如何通過put、deleteEntry兩個來實現紅黑樹增加、刪除節點的。

我想通過這篇博文你對TreeMap一定有了更深的認識。好了,下面先簡單普及紅黑樹知識。

一、紅黑樹簡介

紅黑樹又稱紅-黑二叉樹,它首先是一顆二叉樹,它具體二叉樹所有的特性。同時紅黑樹更是一顆自平衡的排序二叉樹。

我們知道一顆基本的二叉樹他們都需要滿足一個基本性質--即樹中的任何節點的值大於它的左子節點,且小於它的右子節點。按照這個基本性質使得樹的檢索效率大大提高。我們知道在生成二叉樹的過程是非常容易失衡的,最壞的情況就是一邊倒(只有右/左子樹),這樣勢必會導致二叉樹的檢索效率大大降低(O(n)),所以為了維持二叉樹的平衡,大牛們提出了各種實現的算法,如:AVLSBT伸展樹TREAP紅黑樹等等。

平衡二叉樹必須具備如下特性:它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹。也就是說該二叉樹的任何一個等等子節點,其左右子樹的高度都相近。

2014051700001

紅黑樹顧名思義就是節點是紅色或者黑色的平衡二叉樹,它通過顏色的約束來維持着二叉樹的平衡。對於一棵有效的紅黑樹二叉樹而言我們必須增加如下規則:

1、每個節點都只能是紅色或者黑色

2、根節點是黑色

3、每個葉節點(NIL節點,空節點)是黑色的。

4、如果一個結點是紅的,則它兩個子節點都是黑的。也就是說在一條路徑上不能出現相鄰的兩個紅色結點。

5、從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。

這些約束強制了紅黑樹的關鍵性質: 從根到葉子的最長的可能路徑不多於最短的可能路徑的兩倍長。結果是這棵樹大致上是平衡的。因為操作比如插入、刪除和查找某個值的最壞情況時間都要求與樹的高度成比例,這個在高度上的理論上限允許紅黑樹在最壞情況下都是高效的,而不同於普通的二叉查找樹。所以紅黑樹它是復雜而高效的,其檢索效率O(log n)。下圖為一顆典型的紅黑二叉樹。

2014051700002

對於紅黑二叉樹而言它主要包括三大基本操作:左旋、右旋、着色。

2014051700004                          2014051700005

左旋                                                                    右旋

(圖片來自:http://www.cnblogs.com/yangecnu/p/Introduce-Red-Black-Tree.html


本節參考文獻:http://baike.baidu.com/view/133754.htm?fr=aladdin-----百度百科

注:由於本文主要是講解Java中TreeMap,所以並沒有對紅黑樹進行非常深入的了解和研究,如果諸位想對其進行更加深入的研究Lz提供幾篇較好的博文:

1紅黑樹系列集錦

2、紅黑樹數據結構剖析

3、紅黑樹

二、TreeMap數據結構

>>>>>>回歸主角:TreeMap<<<<<<

TreeMap的定義如下:

public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable

TreeMap繼承AbstractMap,實現NavigableMap、Cloneable、Serializable三個接口。其中AbstractMap表明TreeMap為一個Map即支持key-value的集合, NavigableMap(更多)則意味着它支持一系列的導航方法,具備針對給定搜索目標返回最接近匹配項的導航方法 。

TreeMap中同時也包含了如下幾個重要的屬性:

//比較器,因為TreeMap是有序的,通過comparator接口我們可以對TreeMap的內部排序進行精密的控制
        private final Comparator<? super K> comparator;
        //TreeMap紅-黑節點,為TreeMap的內部類
        private transient Entry<K,V> root = null;
        //容器大小
        private transient int size = 0;
        //TreeMap修改次數
        private transient int modCount = 0;
        //紅黑樹的節點顏色--紅色
        private static final boolean RED = false;
        //紅黑樹的節點顏色--黑色
        private static final boolean BLACK = true;

對於葉子節點Entry是TreeMap的內部類,它有幾個重要的屬性:

//
        K key;
        //
        V value;
        //左孩子
        Entry<K,V> left = null;
        //右孩子
        Entry<K,V> right = null;
        //父親
        Entry<K,V> parent;
        //顏色
        boolean color = BLACK;

注:前面只是開胃菜,下面是本篇博文的重中之重,在下面兩節我將重點講解treeMap的put()、delete()方法。通過這兩個方法我們會了解紅黑樹增加、刪除節點的核心算法。

三、TreeMap put()方法

在了解TreeMap的put()方法之前,我們先了解紅黑樹增加節點的算法。

紅黑樹增加節點

紅黑樹在新增節點過程中比較復雜,復雜歸復雜它同樣必須要依據上面提到的五點規范,同時由於規則1、2、3基本都會滿足,下面我們主要討論規則4、5。假設我們這里有一棵最簡單的樹,我們規定新增的節點為N、它的父節點為P、P的兄弟節點為U、P的父節點為G。

2014051700007

對於新節點的插入有如下三個關鍵地方:

  • 1、插入新節點總是紅色節點 。
  • 2、如果插入節點的父節點是黑色, 能維持性質 。
  • 3、如果插入節點的父節點是紅色, 破壞了性質. 故插入算法就是通過重新着色或旋轉, 來維持性質 。

    為了保證下面的闡述更加清晰和根據便於參考,我這里將紅黑樹的五點規定再貼一遍:

  • 1、每個節點都只能是紅色或者黑色

  • 2、根節點是黑色

  • 3、每個葉節點(NIL節點,空節點)是黑色的。

  • 4、如果一個結點是紅的,則它兩個子節點都是黑的。也就是說在一條路徑上不能出現相鄰的兩個紅色結點。

  • 5、從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。

  • 一、為跟節點
  • 若新插入的節點N沒有父節點,則直接當做根據節點插入即可,同時將顏色設置為黑色。(如圖一(1))

    二、父節點為黑色

    這種情況新節點N同樣是直接插入,同時顏色為紅色,由於根據規則四它會存在兩個黑色的葉子節點,值為null。同時由於新增節點N為紅色,所以通過它的子節點的路徑依然會保存着相同的黑色節點數,同樣滿足規則5。(如圖一(2))

    2014051700008

    (圖一)

    三、若父節點P和P的兄弟節點U都為紅色

    對於這種情況若直接插入肯定會出現不平衡現象。怎么處理?P、U節點變黑、G節點變紅。這時由於經過節點P、U的路徑都必須經過G所以在這些路徑上面的黑節點數目還是相同的。但是經過上面的處理,可能G節點的父節點也是紅色,這個時候我們需要將G節點當做新增節點遞歸處理。

    2014051700009

    四、若父節點P為紅色,叔父節點U為黑色或者缺少,且新增節點N為P節點的右孩子

    對於這種情況我們對新增節點N、P進行一次左旋轉。這里所產生的結果其實並沒有完成,還不是平衡的(違反了規則四),這是我們需要進行情況5的操作。

    20140517000010

  • 五、父節點P為紅色,叔父節點U為黑色或者缺少,新增節點N為父節點P左孩子

    這種情況有可能是由於情況四而產生的,也有可能不是。對於這種情況先已P節點為中心進行右旋轉,在旋轉后產生的樹中,節點P是節點N、G的父節點。但是這棵樹並不規范,它違反了規則4,所以我們將P、G節點的顏色進行交換,使之其滿足規范。開始時所有的路徑都需要經過G其他們的黑色節點數一樣,但是現在所有的路徑改為經過P,且P為整棵樹的唯一黑色節點,所以調整后的樹同樣滿足規范5。

    20140517000011

    上面展示了紅黑樹新增節點的五種情況,這五種情況涵蓋了所有的新增可能,不管這棵紅黑樹多么復雜,都可以根據這五種情況來進行生成。下面就來分析Java中的TreeMap是如何來實現紅黑樹的。

    TreeMap put()方法實現分析

    在TreeMap的put()的實現方法中主要分為兩個步驟,第一:構建排序二叉樹,第二:平衡二叉樹。

  • 對於排序二叉樹的創建,其添加節點的過程如下:

  • 1、以根節點為初始節點進行檢索。

  • 2、與當前節點進行比對,若新增節點值較大,則以當前節點的右子節點作為新的當前節點。否則以當前節點的左子節點作為新的當前節點。

  • 3、循環遞歸2步驟知道檢索出合適的葉子節點為止。

  • 4、將新增節點與3步驟中找到的節點進行比對,如果新增節點較大,則添加為右子節點;否則添加為左子節點。

  • 按照這個步驟我們就可以將一個新增節點添加到排序二叉樹中合適的位置。如下:

  • public V put(K key, V value) {
               //用t表示二叉樹的當前節點
                Entry<K,V> t = root;
                //t為null表示一個空樹,即TreeMap中沒有任何元素,直接插入
                if (t == null) {
                    //比較key值,個人覺得這句代碼沒有任何意義,空樹還需要比較、排序?
                    compare(key, key); // type (and possibly null) check
                    //將新的key-value鍵值對創建為一個Entry節點,並將該節點賦予給root
                    root = new Entry<>(key, value, null);
                    //容器的size = 1,表示TreeMap集合中存在一個元素
                    size = 1;
                    //修改次數 + 1
                    modCount++;
                    return null;
                }
                int cmp;     //cmp表示key排序的返回結果
                Entry<K,V> parent;   //父節點
                // split comparator and comparable paths
                Comparator<? super K> cpr = comparator;    //指定的排序算法
                //如果cpr不為空,則采用既定的排序算法進行創建TreeMap集合
                if (cpr != null) {
                    do {
                        parent = t;      //parent指向上次循環后的t //比較新增節點的key和當前節點key的大小
                        cmp = cpr.compare(key, t.key); //cmp返回值小於0,表示新增節點的key小於當前節點的key,則以當前節點的左子節點作為新的當前節點
                        if (cmp < 0) t = t.left; //cmp返回值大於0,表示新增節點的key大於當前節點的key,則以當前節點的右子節點作為新的當前節點
                        else if (cmp > 0) t = t.right; //cmp返回值等於0,表示兩個key值相等,則新值覆蓋舊值,並返回新值
                        else
                            return t.setValue(value);
                    } while (t != null);
                }
                //如果cpr為空,則采用默認的排序算法進行創建TreeMap集合
                else {
                    if (key == null)     //key值為空拋出異常
                        throw new NullPointerException();
                    /* 下面處理過程和上面一樣 */
                    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);
                }
                //將新增節點當做parent的子節點
                Entry<K,V> e = new Entry<>(key, value, parent);
                //如果新增節點的key小於parent的key,則當做左子節點
                if (cmp < 0)
                    parent.left = e;
              //如果新增節點的key大於parent的key,則當做右子節點
                else
                    parent.right = e;
                /*
                 *  上面已經完成了排序二叉樹的的構建,將新增節點插入該樹中的合適位置
                 *  下面fixAfterInsertion()方法就是對這棵樹進行調整、平衡,具體過程參考上面的五種情況
                 */
                fixAfterInsertion(e);
                //TreeMap元素數量 + 1
                size++;
                //TreeMap容器修改次數 + 1
                modCount++;
                return null;
            }
    上面代碼中do{}代碼塊是實現排序二叉樹的核心算法,通過該算法我們可以確認新增節點在該樹的正確位置。找到正確位置后將插入即可,這樣做了其實還沒有完成,因為我知道TreeMap的底層實現是紅黑樹,紅黑樹是一棵平衡排序二叉樹,普通的排序二叉樹可能會出現失衡的情況,所以下一步就是要進行調整。fixAfterInsertion(e); 調整的過程務必會涉及到紅黑樹的左旋、右旋、着色三個基本操作。代碼如下:
  • /**
         * 新增節點后的修復操作
         * x 表示新增節點
         */
         private void fixAfterInsertion(Entry<K,V> x) {
                x.color = RED;    //新增節點的顏色為紅色
    
            </span><span style="color: #008000">//</span>循環 直到 x不是根節點,且x的父節點不為紅色
            <span style="color: #0000ff">while</span> (x != <span style="color: #0000ff">null</span> &amp;&amp; x != root &amp;&amp; x.parent.color ==<span style="color: #000000"> RED) {
                </span><span style="color: #008000">//</span><span style="color: #008000">如果X的父節點(P)是其父節點的父節點(G)的左節點</span>
                <span style="color: #0000ff">if</span> (parentOf(x) ==<span style="color: #000000"> leftOf(parentOf(parentOf(x)))) {
                    </span><span style="color: #008000">//</span><span style="color: #008000">獲取X的叔節點(U)</span>
                    Entry&lt;K,V&gt; y =<span style="color: #000000"> rightOf(parentOf(parentOf(x)));
                   <strong><font color="#ffc000" size="3"> </font></strong></span><strong><font color="#ffc000"><font size="3"><span style="color: #008000">//</span><span style="color: #008000">如果X的叔節點(U) 為紅色(情況三)</span></font></font></strong>
                    <span style="color: #0000ff">if</span> (colorOf(y) ==<span style="color: #000000"> RED) {     
                        </span><span style="color: #008000">//</span><span style="color: #008000">將X的父節點(P)設置為黑色</span>
    

    setColor(parentOf(x), BLACK);
    //將X的叔節點(U)設置為黑色
    setColor(y, BLACK);
    //將X的父節點的父節點(G)設置紅色
    setColor(parentOf(parentOf(x)), RED);
    x
    = parentOf(parentOf(x));
    }
    //如果X的叔節點(U為黑色);這里會存在兩種情況(情況四、情況五)
    else {
    //如果X節點為其父節點(P)的右子樹,則進行左旋轉(情況四)
    if (x == rightOf(parentOf(x))) {
    //將X的父節點作為X
    x = parentOf(x);
    //右旋轉
    rotateLeft(x);
    }
    //(情況五)
    //將X的父節點(P)設置為黑色
    setColor(parentOf(x), BLACK);
    //將X的父節點的父節點(G)設置紅色
    setColor(parentOf(parentOf(x)), RED);
    //以X的父節點的父節點(G)為中心右旋轉
    rotateRight(parentOf(parentOf(x)));
    }
    }
    //如果X的父節點(P)是其父節點的父節點(G)的右節點
    else {
    //獲取X的叔節點(U)
    Entry<K,V> y = leftOf(parentOf(parentOf(x)));
    //如果X的叔節點(U) 為紅色(情況三)
    if (colorOf(y) == RED) {
    //將X的父節點(P)設置為黑色
    setColor(parentOf(x), BLACK);
    //將X的叔節點(U)設置為黑色
    setColor(y, BLACK);
    //將X的父節點的父節點(G)設置紅色
    setColor(parentOf(parentOf(x)), RED);
    x
    = parentOf(parentOf(x));
    }
    //如果X的叔節點(U為黑色);這里會存在兩種情況(情況四、情況五)
    else {
    //如果X節點為其父節點(P)的右子樹,則進行左旋轉(情況四)
    if (x == leftOf(parentOf(x))) {
    //將X的父節點作為X
    x = parentOf(x);
    //右旋轉
    rotateRight(x);
    }
    //(情況五)
    //將X的父節點(P)設置為黑色
    setColor(parentOf(x), BLACK);
    //將X的父節點的父節點(G)設置紅色
    setColor(parentOf(parentOf(x)), RED);
    //以X的父節點的父節點(G)為中心右旋轉
    rotateLeft(parentOf(parentOf(x)));
    }
    }
    }
    //將根節點G強制設置為黑色
    root.color = BLACK;
    }

    對這段代碼的研究我們發現,其處理過程完全符合紅黑樹新增節點的處理過程。所以在看這段代碼的過程一定要對紅黑樹的新增節點過程有了解。在這個代碼中還包含幾個重要的操作。左旋(rotateLeft())、右旋(rotateRight())、着色(setColor())。

    左旋:rotateLeft()

  • 所謂左旋轉,就是將新增節點(N)當做其父節點(P),將其父節點P當做新增節點(N)的左子節點。即:G.left ---> N ,N.left ---> P。

  • private void rotateLeft(Entry<K,V> p) {
            if (p != null) {
                //獲取P的右子節點,其實這里就相當於新增節點N(情況四而言)
                Entry<K,V> r = p.right;
                //將R的左子樹設置為P的右子樹
                p.right = r.left;
                //若R的左子樹不為空,則將P設置為R左子樹的父親
                if (r.left != null)
                    r.left.parent = p;
                //將P的父親設置R的父親
                r.parent = p.parent;
                //如果P的父親為空,則將R設置為跟節點
                if (p.parent == null)
                    root = r;
                //如果P為其父節點(G)的左子樹,則將R設置為P父節點(G)左子樹
                else if (p.parent.left == p)
                    p.parent.left = r;
                //否則R設置為P的父節點(G)的右子樹
                else
                    p.parent.right = r;
                //將P設置為R的左子樹
                r.left = p;
                //將R設置為P的父節點
                p.parent = r;
            }
        }
  • 右旋:rotateRight()

  • 所謂右旋轉即,P.right ---> G、G.parent ---> P。

  • private void rotateRight(Entry<K,V> p) {
            if (p != null) {
                //將L設置為P的左子樹
                Entry<K,V> l = p.left;
                //將L的右子樹設置為P的左子樹
                p.left = l.right;
                //若L的右子樹不為空,則將P設置L的右子樹的父節點
                if (l.right != null) 
                    l.right.parent = p;
                //將P的父節點設置為L的父節點
                l.parent = p.parent;
                //如果P的父節點為空,則將L設置根節點
                if (p.parent == null)
                    root = l;
                //若P為其父節點的右子樹,則將L設置為P的父節點的右子樹
                else if (p.parent.right == p)
                    p.parent.right = l;
                //否則將L設置為P的父節點的左子樹
                else 
                    p.parent.left = l;
                //將P設置為L的右子樹
                l.right = p;
                //將L設置為P的父節點
                p.parent = l;
            }
        }

    左旋、右旋的示意圖如下:

    2014051700004                              2014051700005

    (左旋)                                                         (右旋)

    (圖片來自:http://www.cnblogs.com/yangecnu/p/Introduce-Red-Black-Tree.html

    着色:setColor()

    着色就是改變該節點的顏色,在紅黑樹中,它是依靠節點的顏色來維持平衡的。

    private static <K,V> void setColor(Entry<K,V> p, boolean c) {
            if (p != null)
                p.color = c;
        }

    四、TreeMap delete()方法

    紅黑樹刪除節點

    針對於紅黑樹的增加節點而言,刪除顯得更加復雜,使原本就復雜的紅黑樹變得更加復雜。同時刪除節點和增加節點一樣,同樣是找到刪除的節點,刪除之后調整紅黑樹。但是這里的刪除節點並不是直接刪除,而是通過走了“彎路”通過一種捷徑來刪除的:找到被刪除的節點D的子節點C,用C來替代D,不是直接刪除D,因為D被C替代了,直接刪除C即可。所以這里就將刪除父節點D的事情轉變為了刪除子節點C的事情,這樣處理就將復雜的刪除事件簡單化了。子節點C的規則是:右分支最左邊,或者 左分支最右邊的。

    20140517000012

    紅-黑二叉樹刪除節點,最大的麻煩是要保持 各分支黑色節點數目相等。 因為是刪除,所以不用擔心存在顏色沖突問題——插入才會引起顏色沖突。

    紅黑樹刪除節點同樣會分成幾種情況,這里是按照待刪除節點有幾個兒子的情況來進行分類:

    1、沒有兒子,即為葉結點。直接把父結點的對應兒子指針設為NULL,刪除兒子結點就OK了。

    2、只有一個兒子。那么把父結點的相應兒子指針指向兒子的獨生子,刪除兒子結點也OK了。

    3、有兩個兒子。這種情況比較復雜,但還是比較簡單。上面提到過用子節點C替代代替待刪除節點D,然后刪除子節點C即可。

    下面就論各種刪除情況來進行圖例講解,但是在講解之前請允許我再次啰嗦一句,請時刻牢記紅黑樹的5點規定:

    1、每個節點都只能是紅色或者黑色

    2、根節點是黑色

    3、每個葉節點(NIL節點,空節點)是黑色的。

    4、如果一個結點是紅的,則它兩個子節點都是黑的。也就是說在一條路徑上不能出現相鄰的兩個紅色結點。

    5、從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。

    (注:已經講三遍了,再不記住我就懷疑你是否適合搞IT了 O(∩_∩)O~)

    誠然,既然刪除節點比較復雜,那么在這里我們就約定一下規則:

    1、下面要講解的刪除節點一定是實際要刪除節點的后繼節點(N),如前面提到的C。

    2、下面提到的刪除節點的樹都是如下結構,該結構所選取的節點是待刪除節點的右樹的最左邊子節點。這里我們規定真實刪除節點為N、父節點為P、兄弟節點為W兄弟節點的兩個子節點為X1、X2。如下圖(2.1)。

  • 20140517000013

    現在我們就上面提到的三種情況進行分析、處理。

    情況一、無子節點(紅色節點)

    這種情況對該節點直接刪除即可,不會影響樹的結構。因為該節點為葉子節點它不可能存在子節點-----如子節點為黑,則違反黑節點數原則(規定5),為紅,則違反“顏色”原則(規定4)。 如上圖(2.2)。

    情況二、有一個子節點

    這種情況處理也是非常簡單的,用子節點替代待刪除節點,然后刪除子節點即可。如上圖(2.3)

    情況三、有兩個子節點

    這種情況可能會稍微有點兒復雜。它需要找到一個替代待刪除節點(N)來替代它,然后刪除N即可。它主要分為四種情況。

    1、N的兄弟節點W為紅色

    2、N的兄弟w是黑色的,且w的倆個孩子都是黑色的。

    3、N的兄弟w是黑色的,w的左孩子是紅色,w的右孩子是黑色。

    4、N的兄弟w是黑色的,且w的右孩子時紅色的。

    情況3.1、N的兄弟節點W為紅色

    W為紅色,那么其子節點X1、X2必定全部為黑色,父節點P也為黑色。處理策略是:改變W、P的顏色,然后進行一次左旋轉。這樣處理就可以使得紅黑性質得以繼續保持。N的新兄弟new w是旋轉之前w的某個孩子,為黑色。這樣處理后將情況3.1、轉變為3.2、3.3、3.4中的一種。如下:

    20140517000014

  • 情況3.2、N的兄弟w是黑色的,且w的倆個孩子都是黑色的。

  • 這種情況其父節點可紅可黑,由於W為黑色,這樣導致N子樹相對於其兄弟W子樹少一個黑色節點,這時我們可以將W置為紅色。這樣,N子樹與W子樹黑色節點一致,保持了平衡。如下

  • 20140517000015

    將W由黑轉變為紅,這樣就會導致新節點new N相對於它的兄弟節點會少一個黑色節點。但是如果new x為紅色,我們直接將new x轉變為黑色,保持整棵樹的平衡。否則情況3.2 會轉變為情況3.1、3.3、3.4中的一種。

  • 情況3.3、N的兄弟w是黑色的,w的左孩子是紅色,w的右孩子是黑色。

  • 針對這種情況是將節點W和其左子節點進行顏色交換,然后對W進行右旋轉處理。

  • 20140517000016

  • 此時N的新兄弟X1(new w)是一個有紅色右孩子的黑結點,於是將情況3轉化為情況4.

  • 情況3.4、N的兄弟w是黑色的,且w的右孩子時紅色的。

  • 交換W和父節點P的顏色,同時對P進行左旋轉操作。這樣就把左邊缺失的黑色節點給補回來了。同時將W的右子節點X2置黑。這樣左右都達到了平衡。

  • 20140517000017

  • 總結

  • 個人認為這四種情況比較難理解,首先他們都不是單一的某種情況,他們之間是可以進行互轉的。相對於其他的幾種情況,情況3.2比較好理解,僅僅只是一個顏色的轉變,通過減少右子樹的一個黑色節點使之保持平衡,同時將不平衡點上移至N與W的父節點,然后進行下一輪迭代。情況3.1,是將W旋轉將其轉成情況2、3、4情況進行處理。而情況3.3通過轉變后可以化成情況3.4來進行處理,從這里可以看出情況3.4應該最終結。情況3.4、右子節點為紅色節點,那么將缺失的黑色節點交由給右子節點,通過旋轉達到平衡。

  • 通過上面的分析,我們已經初步了解了紅黑樹的刪除節點情況,相對於增加節點而言它確實是選的較為復雜。下面我將看到在Java TreeMap中是如何實現紅黑樹刪除的。

  • TreeMap deleteEntry()方法實現分析

  • 通過上面的分析我們確認刪除節點的步驟是:找到一個替代子節點C來替代P,然后直接刪除C,最后調整這棵紅黑樹。下面代碼是尋找替代節點、刪除替代節點。

  • private void deleteEntry(Entry<K,V> p) {
            modCount++;      //修改次數 +1
            size--;          //元素個數 -1
    

    /
    * 被刪除節點的左子樹和右子樹都不為空,那么就用 p節點的中序后繼節點代替 p 節點
    * successor(P)方法為尋找P的替代節點。規則是右分支最左邊,或者 左分支最右邊的節點
    * ---------------------(1)
    /
    if (p.left != null && p.right != null) {
    Entry
    <K,V> s = successor(p);
    p.key
    = s.key;
    p.value
    = s.value;
    p
    = s;
    }

        </span><span style="color: #008000">//</span><span style="color: #008000">replacement為替代節點,如果P的左子樹存在那么就用左子樹替代,否則用右子樹替代</span>
        Entry&lt;K,V&gt; replacement = (p.left != <span style="color: #0000ff">null</span> ?<span style="color: #000000"> p.left : p.right);
    

    /
    * 刪除節點,分為上面提到的三種情況
    * -----------------------(2)
    /
    //如果替代節點不為空
    if (replacement != null) {
    replacement.parent
    = p.parent;
    /*
    replacement來替代P節點
    /
    //若P沒有父節點,則跟節點直接變成replacement
    if (p.parent == null)
    root
    = replacement;
    //如果P為左節點,則用replacement來替代為左節點
    else if (p == p.parent.left)
    p.parent.left
    = replacement;
    //如果P為右節點,則用replacement來替代為右節點
    else
    p.parent.right
    = replacement;

            </span><span style="color: #008000">//</span><span style="color: #008000">同時將P節點從這棵樹中剔除掉</span>
            p.left = p.right = p.parent = <span style="color: #0000ff">null</span><span style="color: #000000">;
    
            </span><span style="color: #008000">/*</span><span style="color: #008000">
             * 若P為紅色直接刪除,紅黑樹保持平衡
             * 但是若P為黑色,則需要調整紅黑樹使其保持平衡
             </span><span style="color: #008000">*/</span>
            <span style="color: #0000ff">if</span> (p.color ==<span style="color: #000000"> BLACK)
                fixAfterDeletion(replacement);
        } </span><span style="color: #0000ff">else</span> <span style="color: #0000ff">if</span> (p.parent == <span style="color: #0000ff">null</span>) {     <span style="color: #008000">//</span><span style="color: #008000">p沒有父節點,表示為P根節點,直接刪除即可</span>
            root = <span style="color: #0000ff">null</span><span style="color: #000000">;
        } </span><span style="color: #0000ff">else</span> {      <span style="color: #008000">//</span><span style="color: #008000">P節點不存在子節點,直接刪除即可</span>
            <span style="color: #0000ff">if</span> (p.color == BLACK)         <span style="color: #008000">//</span><span style="color: #008000">如果P節點的顏色為黑色,對紅黑樹進行調整</span>
    

    fixAfterDeletion(p);

            </span><span style="color: #008000">//</span><span style="color: #008000">刪除P節點</span>
            <span style="color: #0000ff">if</span> (p.parent != <span style="color: #0000ff">null</span><span style="color: #000000">) {
                </span><span style="color: #0000ff">if</span> (p ==<span style="color: #000000"> p.parent.left)
                    p.parent.left </span>= <span style="color: #0000ff">null</span><span style="color: #000000">;
                </span><span style="color: #0000ff">else</span> <span style="color: #0000ff">if</span> (p ==<span style="color: #000000"> p.parent.right)
                    p.parent.right </span>= <span style="color: #0000ff">null</span><span style="color: #000000">;
                p.parent </span>= <span style="color: #0000ff">null</span><span style="color: #000000">;
            }
        }
    }</span></pre>
    

    (1)除是尋找替代節點replacement,其實現方法為successor()。如下:

    static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
            if (t == null)
                return null;
            /*
             * 尋找右子樹的最左子樹
             */
            else if (t.right != null) {
                Entry<K,V> p = t.right;
                while (p.left != null)
                    p = p.left;
                return p;
            } 
            /*
             * 選擇左子樹的最右子樹
             */
            else {
                Entry<K,V> p = t.parent;
                Entry<K,V> ch = t;
                while (p != null && ch == p.right) {
                    ch = p;
                    p = p.parent;
                }
                return p;
            }
        }

    (2)處是刪除該節點過程。它主要分為上面提到的三種情況,它與上面的if…else if… else一一對應 。如下:

    1、有兩個兒子。這種情況比較復雜,但還是比較簡單。上面提到過用子節點C替代代替待刪除節點D,然后刪除子節點C即可。

    2、沒有兒子,即為葉結點。直接把父結點的對應兒子指針設為NULL,刪除兒子結點就OK了。

    3、只有一個兒子。那么把父結點的相應兒子指針指向兒子的獨生子,刪除兒子結點也OK了。

    刪除完節點后,就要根據情況來對紅黑樹進行復雜的調整:fixAfterDeletion()。

    private void fixAfterDeletion(Entry<K,V> x) {
            // 刪除節點需要一直迭代,知道 直到 x 不是根節點,且 x 的顏色是黑色
            while (x != root && colorOf(x) == BLACK) {
                if (x == leftOf(parentOf(x))) {      //若X節點為左節點
                    //獲取其兄弟節點
                    Entry<K,V> sib = rightOf(parentOf(x));
    
                </span><span style="color: #008000">/*</span><span style="color: #008000">
                 * 如果兄弟節點為紅色----(情況3.1)
                 * 策略:改變W、P的顏色,然后進行一次左旋轉
                 </span><span style="color: #008000">*/</span>
                <span style="color: #0000ff">if</span> (colorOf(sib) ==<span style="color: #000000"> RED) {     
                    setColor(sib, BLACK);     
                    setColor(parentOf(x), RED);  
                    rotateLeft(parentOf(x));
                    sib </span>=<span style="color: #000000"> rightOf(parentOf(x));
                }
    
                </span><span style="color: #008000">/*</span><span style="color: #008000">
                 * 若兄弟節點的兩個子節點都為黑色----(情況3.2)
                 * 策略:將兄弟節點編程紅色
                 </span><span style="color: #008000">*/</span>
                <span style="color: #0000ff">if</span> (colorOf(leftOf(sib))  == BLACK &amp;&amp;<span style="color: #000000">
                    colorOf(rightOf(sib)) </span>==<span style="color: #000000"> BLACK) {
                    setColor(sib, RED);
                    x </span>=<span style="color: #000000"> parentOf(x);
                } 
                </span><span style="color: #0000ff">else</span><span style="color: #000000"> {
                    </span><span style="color: #008000">/*</span><span style="color: #008000">
                     * 如果兄弟節點只有右子樹為黑色----(情況3.3)
                     * 策略:將兄弟節點與其左子樹進行顏色互換然后進行右轉
                     * 這時情況會轉變為3.4
                     </span><span style="color: #008000">*/</span>
                    <span style="color: #0000ff">if</span> (colorOf(rightOf(sib)) ==<span style="color: #000000"> BLACK) {
                        setColor(leftOf(sib), BLACK);
                        setColor(sib, RED);
                        rotateRight(sib);
                        sib </span>=<span style="color: #000000"> rightOf(parentOf(x));
                    }
                    </span><span style="color: #008000">/*</span><span style="color: #008000">
                     *----情況3.4
                     *策略:交換兄弟節點和父節點的顏色,
                     *同時將兄弟節點右子樹設置為黑色,最后左旋轉
                     </span><span style="color: #008000">*/</span><span style="color: #000000">
                    setColor(sib, colorOf(parentOf(x)));
                    setColor(parentOf(x), BLACK);
                    setColor(rightOf(sib), BLACK);
                    rotateLeft(parentOf(x));
                    x </span>=<span style="color: #000000"> root;
                }
            } 
            
            </span><span style="color: #008000">/**</span><span style="color: #008000">
             * X節點為右節點與其為做節點處理過程差不多,這里就不在累述了
             </span><span style="color: #008000">*/</span>
            <span style="color: #0000ff">else</span><span style="color: #000000"> {
                Entry</span>&lt;K,V&gt; sib =<span style="color: #000000"> leftOf(parentOf(x));
    
                </span><span style="color: #0000ff">if</span> (colorOf(sib) ==<span style="color: #000000"> RED) {
                    setColor(sib, BLACK);
                    setColor(parentOf(x), RED);
                    rotateRight(parentOf(x));
                    sib </span>=<span style="color: #000000"> leftOf(parentOf(x));
                }
    
                </span><span style="color: #0000ff">if</span> (colorOf(rightOf(sib)) == BLACK &amp;&amp;<span style="color: #000000">
                    colorOf(leftOf(sib)) </span>==<span style="color: #000000"> BLACK) {
                    setColor(sib, RED);
                    x </span>=<span style="color: #000000"> parentOf(x);
                } </span><span style="color: #0000ff">else</span><span style="color: #000000"> {
                    </span><span style="color: #0000ff">if</span> (colorOf(leftOf(sib)) ==<span style="color: #000000"> BLACK) {
                        setColor(rightOf(sib), BLACK);
                        setColor(sib, RED);
                        rotateLeft(sib);
                        sib </span>=<span style="color: #000000"> leftOf(parentOf(x));
                    }
                    setColor(sib, colorOf(parentOf(x)));
                    setColor(parentOf(x), BLACK);
                    setColor(leftOf(sib), BLACK);
                    rotateRight(parentOf(x));
                    x </span>=<span style="color: #000000"> root;
                }
            }
        }
    
        setColor(x, BLACK);
    }</span></pre>
    

    這是紅黑樹在刪除節點后,對樹的平衡性進行調整的過程,其實現過程與上面四種復雜的情況一一對應,所以在這個源碼的時候一定要對着上面提到的四種情況看。

  • 五、寫在最后

  • 這篇博文確實是有點兒長,在這里非常感謝各位看客能夠靜下心來讀完,我想你通過讀完這篇博文一定收獲不小。同時這篇博文很大篇幅都在闡述紅黑樹的實現過程,對Java 的TreeMap聊的比較少,但是我認為如果理解了紅黑樹的實現過程,對TreeMap那是手到擒來,小菜一碟。

    同時這篇博文我寫了四天,看了、參考了大量的博文。同時不免會有些地方存在借鑒之處,在這里對其表示感謝。LZ大二開始學習數據結構,自認為學的不錯,現在發現數據結構我還有太多的地方需要學習了,同時也再一次體味了算法的魅力!!!!

     


    參考資料:

    1、紅黑樹數據結構剖析:http://www.cnblogs.com/fanzhidongyzby/p/3187912.html

    2、紅黑二叉樹詳解及理論分析 :http://blog.csdn.net/kartorz/article/details/8865997

    3、教你透徹了解紅黑樹 blog.csdn.net/v_july_v/article/details/6105630

    4、經典算法研究系列:五、紅黑樹算法的實現與剖析 :http://blog.csdn.net/v_JULY_v/article/details/6109153

    5、示例,紅黑樹插入和刪除過程:http://saturnman.blog.163.com/blog/static/557611201097221570/

    6、紅黑二叉樹詳解及理論分析 :http://blog.csdn.net/kartorz/article/details/8865997


    免責聲明!

    本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



     
    粵ICP備18138465號   © 2018-2025 CODEPRJ.COM