Java集合——TreeMap(一)

午夜12點發表於2018-05-04

簡述

TreeMap底層是紅黑樹,在java8 HashMap也引入了紅黑樹,那麼什麼是紅黑樹?紅黑樹是一種二叉搜尋樹,它在每個結點上增加了一個儲存位來表示結點的顏色,可以是RED或BLACK。通過對任何一條從根到葉子的簡單路徑上各個結點的顏色進行約束,紅黑樹確保沒有一條路徑會比其他路徑長出2倍,因而是近似於平衡的。(出自演算法導論)

二叉搜搜尋樹

既然紅黑樹是一種二叉搜尋樹,那麼我們先來了解其性質:
①.左子樹上所有結點的值小於或等於其根結點的值
②.右子樹上所有結點的值大於或等於其根結點的值
③.任意節點的左、右子樹也分別為二叉搜尋樹
如下:

Java集合——TreeMap(一)
雖然圖a,圖b都是二叉搜尋樹,但是若同時找值為8的結點。這兩種結構明顯圖a查詢所費時間更少。為了保證樹的結構左右兩端資料大致平衡降低二叉樹的查詢難度一般會採用一種演算法機制實現節點資料結構的平衡,實現了這種演算法的有比如AVL、紅黑樹,使用平衡二叉樹能保證資料的左右兩邊的結點層級相差不會大於1,通過這樣避免樹形結構由於增加刪除變成線性連結串列影響查詢效率,保證資料平衡的情況下查詢資料的速度近於二分法查詢。

紅黑樹

紅黑樹是每個節點都帶有顏色屬性的二叉搜尋樹,顏色或紅色或黑色。除了符合二叉搜尋的基本特性外,它還附加如下性質:
①.節點是紅色或黑色
②.根節點是黑色
③.每個葉節點(NIL節點,空節點)是黑色的
④.每個紅色節點的兩個子節點都是黑色。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點)
⑤.從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點
一顆典型的紅黑樹:

Java集合——TreeMap(一)
當我們對紅黑樹進行插入和刪除等操作時,可能會破壞紅黑樹的性質。為了保證紅黑樹始終是一顆紅黑樹,其通過旋轉、變色進行調整.而旋轉又分成兩種形式:

左旋:

Java集合——TreeMap(一)

右旋:

Java集合——TreeMap(一)

TreeMap

言歸正傳回到treeMap,我們先看下它的繼承關係圖:

Java集合——TreeMap(一)

TreeMap重要欄位


    // 比較器用於排序,若為null使用自然排序維持key順序 
    private final Comparator comparator; 
    
    // 根節點
    private transient Entry root;
    
    // 節點數
    private transient int size = 0;
    
    // 修改次數,fail-fast
    private transient int modCount = 0;
    
    //節點顏色
    private static final boolean RED   = false;
    private static final boolean BLACK = true;
    
    /**
     * 節點
     */
    static final class Entry implements Map.Entry {
        K key;                    //鍵
        V value;                  //值    
        Entry left;               //左子樹
        Entry right;              //右子樹
        Entry parent;             //父親
        boolean color = BLACK;    //顏色

        Entry(K key, V value, Entry parent) {...}

        public K getKey() {...}

        public V getValue() {...}

        public V setValue(V value) {...}

        public boolean equals(Object o) {...}

        public int hashCode() {...}

        public String toString() {...}
    }
複製程式碼

構造方法


    /**
     * 無參構造,自然排序(從小到大)。要求key實現Comparable介面,會呼叫key重寫的compareTo方法進行比較
     * 若key沒有實現comparable介面,執行時報錯(java.lang.ClassCastException)
     */
    public TreeMap() {
        comparator = null;
    }
    
    /**
     * 指定比較器,若不為null會呼叫其compare方法進行比較,無需鍵實現comparable介面
     */
    public TreeMap(Comparator comparator) {
        this.comparator = comparator;
    }
    
    /**
     * 將map轉為treeMap,比較器為null,注意key
     */
    public TreeMap(Map m) {
        comparator = null;
        putAll(m);
    }
    
    /**
     * 將map轉為treeMap,比較器為SortMap中的comparator
     */
    public TreeMap(SortedMap m) {
        comparator = m.comparator();
        try {
            buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
        } catch (java.io.IOException cannotHappen) {
        } catch (ClassNotFoundException cannotHappen) {
        }
    }
複製程式碼

put方法


    public V put(K key, V value) {
        // 獲取根節點
        Entry t = root;
        // 若TreeMap為空則直接插入
        if (t == null) {
            //校驗:若比較器為null則key必須實現Comparable介面,若不為null,key可為null
            compare(key, key); // type (and possibly null) check
            //設為頭節點
            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        // 記錄key排序比較結果
        int cmp;
        // 記錄父節點
        Entry parent;
        // split comparator and comparable paths
        Comparator cpr = comparator;
        // 若存在比較器,迴圈查詢位置cmp小於0往左找,大於0往右找,直至等於0進行替換
        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);
        }
        // 若不存在比較器,key必須實現Comparable介面
        else {
            //null無法實現Comparable介面沒有compareTo方法故拋異常
            if (key == null)
                throw new NullPointerException();
            //獲取比較器,處理方式與上面一致    
            @SuppressWarnings("unchecked")
                Comparable k = (Comparable) 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);
        }
        //若當前TreeMap中沒有此key則新建結點,無論上述哪個分支成立parent一定指向當前某個葉子結點
        Entry e = new Entry<>(key, value, parent);
        //小於0則為左子樹
        if (cmp < 0)
            parent.left = e;
        //大於0則為右子樹    
        else
            parent.right = e;
        //保證紅黑樹性質    
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    } 
複製程式碼

get方法

get方法與put思路大致相同


    public V get(Object key) {
        Entry p = getEntry(key);
        //找到對應節點返回其值,沒有找到返回null
        return (p==null ? null : p.value);
    } 
    
    final Entry getEntry(Object key) {
        // Offload comparator-based version for sake of performance
        // 若比較器不為null
        if (comparator != null)
            return getEntryUsingComparator(key);
        // 若比較器為null,則key必須實現Comparable介面,null不能拋異常   
        if (key == null)
            throw new NullPointerException();
        //用key的compareTo方法,從根節點尋找,若沒有找返回null
        @SuppressWarnings("unchecked")
            Comparable k = (Comparable) key;
        Entry p = root;
        while (p != null) {
            int cmp = k.compareTo(p.key);
            if (cmp < 0)
                p = p.left;
            else if (cmp > 0)
                p = p.right;
            else
                return p;
        }
        return null;
    }
    
    final Entry getEntryUsingComparator(Object key) {
        @SuppressWarnings("unchecked")
            K k = (K) key;
        Comparator cpr = comparator;
        //處理方式一致
        if (cpr != null) {
            Entry p = root;
            while (p != null) {
                int cmp = cpr.compare(k, p.key);
                if (cmp < 0)
                    p = p.left;
                else if (cmp > 0)
                    p = p.right;
                else
                    return p;
            }
        }
        return null;
    }
複製程式碼

containsValue方法

採用類似中序遍歷(LDR左根右)方式來遍歷整個紅黑樹找到相應value


    public boolean containsValue(Object value) {
        for (Entry e = getFirstEntry(); e != null; e = successor(e))
            if (valEquals(value, e.value))
                return true;
        return false;
    } 
    
    /**
     * 返回最小節點
     */
    final Entry getFirstEntry() {
        Entry p = root;
        if (p != null)
            while (p.left != null)
                p = p.left;
        return p;
    }
    
    /**
     * 找後繼節點
     */
    static  TreeMap.Entry successor(Entry t) {
        if (t == null)
            return null;
        //若存在右子樹,則返回右子樹中最小節點    
        else if (t.right != null) {
            Entry p = t.right;
            while (p.left != null)
                p = p.left;
            return p;
        //若不存在,從當前節點往上找,若其父節點不為null且它是父節點的右子樹則繼續找父節點
        //直至條件不成立,返回父節點
        } else {
            Entry p = t.parent;
            Entry ch = t;
            while (p != null && ch == p.right) {
                ch = p;
                p = p.parent;
            }
            return p;
        }
    }
複製程式碼

successor方法找節點的後繼節點:
①.若節點為空沒有後繼
②.若節點有右子樹,後繼為右子樹的最左節點
③.若節點沒有右子樹,後繼為該節點所在左子樹的第一個祖先節點

第一個無需多言,第二個也容易,看圖p的後繼節點s:

Java集合——TreeMap(一)
第三個:
若其父節點為空,返回null;
若其有父節點且為父節點左子樹,返回其父節點;
若其有父節點且為父節點右子樹,其所在左子樹的第一個祖先節點看圖(p的後繼為s),一個個往上找將p與A看成整體相對於B是其右子樹,再往上找將P、B、A看成整體相對於C還是其右子樹,再找P、B、A、C整體相對於S是其左子樹,返回這個整體的第一個祖先節點即節點S
Java集合——TreeMap(一)

remove方法


    public V remove(Object key) {
        //獲取key所對應的節點
        Entry p = getEntry(key);
        //若節點為空返回null
        if (p == null)
            return null;
        //若不為null,刪除節點返回其值 
        V oldValue = p.value;
        deleteEntry(p);
        return oldValue;
    } 
    
    private void deleteEntry(Entry p) {
        modCount++;
        size--;
        
        //若p左子樹和右子樹都不為null,將p的key與value替換成後繼的,將p指向後繼
        if (p.left != null && p.right != null) {
            Entry s = successor(p);
            p.key = s.key;
            p.value = s.value;
            p = s;
        } // p has 2 children

        // replacement為替代節點
        Entry replacement = (p.left != null ? p.left : p.right);
        
        if (replacement != null) {
            replacement.parent = p.parent;
            //若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;
            //刪除p節點
            p.left = p.right = p.parent = null;

            // 若p為黑色則需要調整
            if (p.color == BLACK)
                fixAfterDeletion(replacement);
        //若p沒有父節點即p為根節點,根節點置空        
        } else if (p.parent == null) { // return if we are the only node.
            root = null;
        //p沒有子節點
        } else { //  No children. Use self as phantom replacement and unlink.
            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;
            }
        }
    }
複製程式碼

分三種情況:
①.葉子結點:直接將其父節點對應孩子置空,若刪除左葉子結點則將其父結點左子樹置空,若刪除右葉子結點則將其父結點右子樹置空

刪除節點A:

Java集合——TreeMap(一)
②.一個孩子:用子節點替代需刪除節點

刪除節點G:

Java集合——TreeMap(一)

③.兩個孩子:先找到後繼,找到後,替換當前節點的內容為後繼節點,然後再刪除後繼節點,因為這個後繼節點一定沒有左孩子,所以就將兩個孩子的情況轉換為了前面兩種情況

刪除節點B:

Java集合——TreeMap(一)

小結

①.TreeMap底層是紅黑樹,集合有序執行緒不安全。
②.若比較器為空則key一定不能為null,若比較器不為空則key可以為null由TreeMap其比較器而定
③.containsValue方法採用中序遍歷(LDR左根右)方式遍歷整個TreeMap
在上一篇文章(java8HashMap)寫了連結串列與紅黑樹互轉,本文略微提及紅黑樹相關知識主要圍繞原始碼講述TreeMap的一些方法,下篇主要以TreeMap插入刪除後如何維持其特性。

參考

https://juejin.im/post/5828ef582f301e0058586fde
http://www.cnblogs.com/yangecnu/p/Introduce-Red-Black-Tree.html
https://blog.csdn.net/v_july_v/article/details/6105630

相關文章