TreeMap及其紅黑樹算法個人解析

  工作中,有時會遇到需要對map進行排序的情況,java中常用的兩種帶排序的map,一種是LinkedHashMap,而另外種就是TreeMap了,LinkedHashMap是基於散列進行存儲的,這裏不過多討論了。而TreeMap則是使用紅黑樹來實現排序的,這裏我們重點研究下TreeMap的實現。

紅黑樹的基本概念
百度百科紅黑樹
  
TreeMap基本數據結構
爲了實現紅黑樹,TreeMap定義的靜態內部類entry如下

    static final class Entry<K,V> implements Map.Entry<K,V> {
        K key;
        V value;
        //左孩子節點,存放比當前節點小的節點
        Entry<K,V> left = null;
        //右孩子節點,存放比當前節點大的節點
        Entry<K,V> right = null;
       /**
        *父節點,理論上只需要左右孩子指針,就可以生成一棵樹,但是紅黑樹進行排序時,
        *涉及到節點換位,所以引入父指針來實現雙向指針,方便換位。
        */
        Entry<K,V> parent;
        //當前節點顏色
        boolean color = BLACK;

        Entry(K key, V value, Entry<K,V> parent) {
            this.key = key;
            this.value = value;
            this.parent = parent;
        }
 }

而TreeMap中的一些基本屬性如下

public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
    /**
     * 排序器,方便使用者自定義treeMap排序規則
     * 如果爲null,則使用自然排序
     */
    private final Comparator<? super K> comparator;
    //樹的根節點
    private transient Entry<K,V> root = null;
    //TreeMao中數據的個數
    private transient int size = 0;
    //對TreeMap更新的次數
    private transient int modCount = 0;
}

TreeMap put方法
描述完TreeMap的基本結構,現在步入正題,解析put方法吧,
TreeMap的put方法代碼如下

    public V put(K key, V value) {
        Entry<K,V> t = root;
        //如果根節點爲null,這當前節點直接設置成根節點,然後返回。
        if (t == null) {
            compare(key, key); 
            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        //cmp即爲2個節點key比較後的大小返回值,<0爲小,=0爲相等,>0爲大
        int cmp;
        //新插入節點的父節點
        Entry<K,V> parent;
        // 獲取排序器
        Comparator<? super K> cpr = comparator;
        //排序器不爲null情況下,進行設置節點
        if (cpr != null) {
           /**
            *此循環用來查找節點適合插入的位置,從根節點進行遍歷,
            *如果插入新節點的key小於遍歷的當前節點的key,則向左子節點
            *繼續進行遍歷,如果大於則向右子節點進行遍歷,等於則用新插入
            *的節點替換當前遍歷的節點,然後返回,如遍歷到葉子節點(注:
            *這裏的葉子節點指的是有數據,但是子節點都爲null的節點,而
            *TreeMap的紅黑樹算法中,默認紅黑樹葉子節點都爲null,以滿
            *足紅黑樹性質,同時方便運算),TreeMap中不存在插入key,
            *則循環結束。定位出新插入節點的父節點
            */
            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();
            //排序器爲null情況下,採用key的自然排序
            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);
        }
        //如果put運行到這裏,則代表樹中不存在與插入key相同key的節點,
        //所以需要新增節點,新增節點都是作爲樹的葉子節點添加
        Entry<K,V> e = new Entry<>(key, value, parent);
        //判斷新增節點是父節點的左節點還是右節點
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        //核心算法,用來對二叉樹進行平衡,也即是紅黑樹算法。
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

TreeMap 紅黑樹插入算法
這裏我們先講完fixAfterInsertion方法是怎麼實現新增節點,保證紅黑樹性質不變的,然後再描述爲何不使用普通的二叉樹樹或平衡二叉樹(avl樹)。fixAfterInsertion代碼如下,雖然代碼不多,但是理解起來頗爲艱難。
紅黑樹的性質還是簡單在這裏提一下吧,方便對照代碼
性質1. 節點是紅色或黑色。
性質2. 根節點是黑色。
性質3 每個葉節點(NIL節點,空節點)是黑色的。
性質4 每個紅色節點的兩個子節點都是黑色。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點)
性質5. 從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。

/**
*注意此算法爲CLR算法(按根左右的方式遍歷)
*注意此算法中,每個樹節點都有左右兩個子節點,儘管其中的一個或兩個可能是空葉子
*請注意分析put方法,添加的x節點一定位於樹的最後一層
*/
private void fixAfterInsertion(Entry<K,V> x) {
        //每個新增非null節點先默認爲紅
        x.color = RED;
        /**
        *此循環判斷遍歷節點x(初始爲新增節點)的位置及顏色是否合理,不合理則調整,調整完後,爲了保證調整後的樹也滿足紅黑樹性質,需再向上遍歷將x賦值爲x.parent或x.parent.parant,再次遍歷調整。循環終止條件爲遍歷的節點爲根節點,或x的父節點不爲紅(因爲父節點爲黑時,直接在父節點下添加紅色的子節點,依舊滿足紅黑樹的性質),當滿足此條件時,整棵樹已經滿足了紅黑樹性質。此算法循環中的操作最多隻涉及到三層,即x層,x.parent層,x.parent.parent層,如果x.parant.parent存在,則是調整以x.parant.parant爲根的三層樹結構中的節點,保證這棵子樹維持紅黑樹性質。
        */
        while (x != null && x != root && x.parent.color == RED) {
            //條件a:判斷x的父節點是否爲當前子樹的左節點
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
                /**
                設置y爲x父節點的右兄弟節點(因爲條件1已經決定如果進入此分支,
                *則x父節點一定位於以x.parent.parent爲根的左子樹上面)
                */
                Entry<K,V> y = rightOf(parentOf(parentOf(x)));
                //條件b:判斷父節點右兄弟節點是否爲紅
                if (colorOf(y) == RED) {
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {
                    //條件c:判斷x節點是否爲父節點的右節點
                    if (x == rightOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateLeft(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateRight(parentOf(parentOf(x)));
                }
            } else {
                /**
                設置y爲x父節點的左兄弟節點(因爲條件1已經決定如果進入此分支,
                *則x父節點一定位於以x.parent.parent爲根的右子樹上面)
                */
                Entry<K,V> y = leftOf(parentOf(parentOf(x)));
                //條件d:判斷父節點左兄弟節點是否爲紅
                if (colorOf(y) == RED) {
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {
                    //條件e:判斷x節點是否爲父節點的左節點
                    if (x == leftOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateRight(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateLeft(parentOf(parentOf(x)));
                }
            }
        }
        //保證紅黑樹根節點始終爲黑色,維持性質2
        root.color = BLACK;
    }

分析上述代碼由於while循環體內,最多隻對以x.parent.parent爲根的三層樹進行操作
(如果不存在,則操作的是x.parent爲根的兩層樹),而由條件abcde則分出6種可能
分支(注意這些分支隱含默認條件爲x父節點爲紅)

分支1.x父左,x父兄紅(處理:將x父節點,x的父兄節點,x父節點的父節點變色,使以x.parent.parent爲根的三層子樹滿足紅黑樹性質,因爲x.parent.parent的顏色發生了改變,如果x.parent.parent存在父節點且顏色爲紅,會導致紅黑樹性質改變,所以將x賦值爲x.parent.parent,再次執行循環判斷)
分支2.x父左,x父兄黑,x右(處理:將x賦值爲x.parent,然後對x進行使樹的節點相對位置顏色不變,此時已經將x賦值爲)
分支3.x父左,x父兄黑,x左(處理:先修改相應節點顏色,然後對x.parent.parent右旋轉,最終使樹的節點相對位置顏色不變,但是部分節點位置發生改變)
分支4.x父右,x父兄紅
分支5.x父右,x父兄黑,x右
分支6.x父右,x父兄黑,x左
這6種分支已經包含了在一個三層的紅黑樹中,在父節點爲紅色的情況下(在TreeMapd的插入算法中,因爲插入的新節點都默認爲紅色,如果父節點爲黑色直接插入節點不影響紅黑樹性質,而如果父節點爲紅色,則不滿足紅黑樹性質4,所以需要調整),新增節點所產生的所有可能性,現在我們就通過具體的例子來更加深入理解這個方法吧,這裏我選取的是按一定規律插入一組key,來覆蓋每一種可能分支,這組key是:128,64,192,32,48,16,8,160,224,232,228,240,230,236
插入過程如圖

這裏寫圖片描述

圖中圈住的圖,代表插入節點破壞了紅黑樹性質,然後調整節點滿足紅黑樹性質的過程。(圖片中只給出了每次循環的初始狀態和最終狀態)
插入128:作爲根節點直接插入
插入64:父節點爲黑,直接插入
插入192:父節點爲黑,直接插入
插入32:x=32,滿足分支1,按分支1進行處理,對128,64,192進行顏色反轉,然後x=128,因爲128爲根節點,循環結束,結束循環後將root設置爲黑色,雖然左右圖對比,128顏色未變,實際上代碼中128進行2次顏色反轉,最終樹形態如右圖。
插入48:x=48,滿足分支2,分支2進行了3步處理,第一步x=32,然後對x進行左旋操作,第二步對x.parant(48),x.parant.parant(64)進行顏色反轉,第三步對x.parent.parent(64)進行右旋操作。x.parent(48).color爲黑色,循環結束。
插入16:x=16,滿足分支1,按分支1處理,過程就不再描述,大家直接看結果圖即可
插入8:x=8,滿足分支3,按分支3處理,第一步對x.parent(16),x.parent.parent(32)進行顏色反轉,第二步對x.parent.parent(32)進行右旋操作。x.parent(16).color爲黑色,循環結束。因分支三操作實際上是分支2後兩步操作,所以不再給出中間圖,直接給出開始和結果圖,過程參照插入48第二圖開始。

父節點爲左的分支插入我們已經全部瞭解了,現在讓我們看看父節點爲右的插入,接上圖最終形態繼續插入160,224,232,228,240,230,236,對於左邊不再變化的節點樹,直接用子樹代替了。

這裏寫圖片描述

插入160:父節點爲黑,直接插入
插入224:父節點爲黑,直接插入
插入232:x=232,滿足分支4,對192,160,224進行顏色反轉,然後x=192,x.parent(128)爲黑,循環結束
插入228:x=228,滿足分支5,第一步x=x.parent(232),對x(232)右旋,第二步x.parent(228),x.parent.parent(224)進行顏色反轉,第三步x.parent.parent(224)左旋
,此時x.parent(228)爲黑,循環結束。

下面是插入240,230,236

這裏寫圖片描述

插入240:x=240,滿足分支4,x.parent(232),x.parent.parent(228),x.parent.parent.left(224)進行顏色反轉,x=x.parent.parent(228),繼續循環,滿足分支4,x.parent(192),x.parent.parent(128),x.parent.parent.left(48)顏色反轉,x=x.parent.parent(128),x爲root,循環結束,root顏色設置爲黑色。
插入230:父節點爲黑,直接插入
插入236:x=236,滿足分支4,x.parent(240),x.parent.parent(232),x.parent.parent.left(230)進行顏色反轉,x=x.parent.parent(232),繼續循環,滿足分支6,x.parent(228),x.parent.parent(192)顏色反轉,x=x.parent.parent(192)左旋,x.parent(228)爲黑色,循環結束。

TreeMap remove
TreeMap使用紅黑樹的理由
至此,TreeMap中的紅黑樹插入算法可以算解析完畢了,我們已經瞭解TreeMap中是怎麼實現紅黑樹的插入的。排序算法有多種,爲何TreeMap採用紅黑樹算法呢,紅黑樹的本身性質是其中很重要的因素,由於紅黑樹的性質,在查找,插入,及刪除時,紅黑樹的時間複雜度都爲O(logn),此外,由於它的設計,任何不平衡都會在三次旋轉之內解決。雖然還存在一些更復雜的數據結構,能夠在一次旋轉就實現平衡,但是紅黑樹本身在性能和複雜度上相對而言更均衡。

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