紅黑樹,TreeMap,插入操作


紅黑樹                                                                                 

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

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

2、根節點是黑色

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

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

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

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

紅黑樹的插入操作                                                                    

如果插入第一個節點,我們直接用樹根記錄這個節點,並設置為黑色,否則作遞歸查找插入。

默認插入的節點顏色都是紅色,因為插入黑色節點會破壞根路徑上的黑色節點總數,但即使如此,也會出現連續紅色節點的情況。因此在一般的插入操作之后,出現紅黑樹約束條件不滿足的情況(稱為失去平衡)時,就必須要根據當前的紅黑樹的情況做相應的調整。和AVL樹的平衡調整通過旋轉操作的實現類似,紅黑樹的調整操作一般都是通過旋轉結合節點的變色操作來完成的。

  • 叔父節點是黑色(若是空節點則默認為黑色)

這種情況下通過旋轉和變色操作可以使紅黑樹恢復平衡。但是考慮當前節點n和父節點p的位置又分為四種情況:

A、n是p左子節點,p是g的左子節點。

B、n是p右子節點,p是g的右子節點。

C、n是p左子節點,p是g的右子節點。

D、n是p右子節點,p是g的左子節點。

  • 情況A:n是p左子節點,p是g的左子節點。針對該情況可以通過一次右旋轉操作,並將p設為黑色,g設為紅色完成重新平衡。

1

右旋操作的步驟是將p掛接在g節點原來的位置(如果g原是根節點,需要考慮邊界條件),將p的右子樹x掛到g的左子節點,再把g掛在p的右子節點上,完成右旋操作。這里將最終旋轉結果的子樹的根節點作為旋轉軸(p節點),也就是說旋轉軸在旋轉結束后稱為新子樹的根節點!

  • 情況B則需要使用左單旋操作來解決平衡問題,方法和情況A類似。

2

  • 情況C:n是p左子節點,p是g的右子節點。針對該情況通過一次左旋,一次右旋操作(旋轉軸都是n,注意不是p),並將n設為黑色,g設為紅色完成重新平衡。

3

需要注意的是,由於此時新插入的節點是n,它的左右子樹x,y都是空節點,但即使如此,旋轉操作的結果需要將x,y新的位置設置正確(如果不把p和g的對應分支設置為空節點的話,就會破壞樹的結構)。在之后的其他操作中,待旋轉的節點n的左右子樹可能就不是空節點了。

  • 情況D則需要使用一次右單旋,一次左單旋操作來解決平衡問題,方法和情況C類似。

4

  • 叔父節點是紅色

當叔父節點是紅色時,則不能直接通過上述方式處理了(把前邊的所有情況的u節點看作紅色,會發現節點u和g是紅色沖突的)。但是我們可以交換g與p,u節點的顏色完成當前沖突的解決。

5

但是僅僅這樣做顏色交換是不夠的,因為祖父節點g的父節點(記作gp)如果也是紅色的話仍然會有沖突(g和gp是連續的紅色,違反規則2)。為了解決這樣的沖突,我們需要從當前插入點n向根節點root回溯兩次。

第一次回溯時處理所有擁有兩個紅色節點的節點,並按照上圖中的方式交換父節點g與子節點p,u的顏色,並暫時忽略gp和p的顏色沖突。如果根節點的兩個子節點也是這種情況,則在顏色交換完畢后重新將根節點設置為黑色。

第二次回溯專門處理連續的紅色節點沖突。由於經過第一遍的處理,在新插入點n的路徑上一定不存在同為紅色的兄弟節點了。而仍出現gp和p的紅色沖突時,gp的兄弟節點(gu)可以斷定為黑色,這樣就回歸前邊討論的叔父節點為黑色時的情況處理。

6

由於發生沖突的兩個紅色節點位置可能是任意的,因此會出現上述的四種旋轉情況。不過我們把靠近葉子的紅色節點(g)看作新插入的節點,這樣面對A,B情況則把p的父節點gp作為旋轉軸,旋轉后gp會是新子樹的根,而面對C,D情況時把p作為旋轉軸即可,旋轉后p為新子樹的根(因此可以把四種旋轉方式封裝起來)。

在第二次回溯時,雖然每次遇到紅色沖突旋轉后都會提升g和gp節點的位置(與根節點的距離減少),但是無論g和gp誰是新子樹的根都不會影響新插入節點n到根節點root路徑的回溯,而且一旦新子樹的根到達根節點(parent指針為空)就可以停止回溯了。

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()方法                                                                   

在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;    //新增節點的顏色為紅色  
  
            //循環 直到 x不是根節點,且x的父節點不為紅色  
            while (x != null && x != root && x.parent.color == RED) {  
                //如果X的父節點(P)是其父節點的父節點(G)的左節點  
                if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {  
                    //獲取X的叔節點(U)  
                    Entry<K,V> y = rightOf(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 == 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;  
        }  
    }

我是天王蓋地虎的分割線                                                             

 

 

 

參考:http://blog.csdn.net/chenssy/article/details/26668941


免責聲明!

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



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