Java集合容器系列05-TreeMap

1、TreeMap的介紹

    TreeMap繼承自AbstractMap,實現了NavigableMap,基於紅黑樹實現,它內部的鍵值對映射是按照類內部指定的比較器Comparator進行排序,若是內部的排序器爲空則根據鍵值對映射中Key的天然順序進行排序,取決於實例化對象時使用的構造函數。Treemap的底層紅黑樹結構保證了containKey、get、put、remove方法定位Key操做的時間複雜度爲O(logN)。TreeMap是非線程安全的,在建立容器迭代器Iterator以後若是有其餘線程對容器作出結構性修改(添加、插入、刪除鍵值對均屬於結構性修改,修改已存在鍵值對的value值不算結構性修改)會拋出一個ConcurrentModificationException異常。java

2、TreeMap的數據結構

1 - 繼承結構

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

    從源碼中能夠看出TreeMap繼承AbstractMap,AbstractMap實現了Map的一些基本操做,此外是實現了NavigableMap、Cloneable、java.io.Serializable接口。NavigableMap接口定義了一系列基於排序的導航方法例如返回lowerEntry(K key)返回key小於key且排序最接近key的對應鍵值對,由於間接實現了SortedMap故TreeMap的鍵值對按序存儲。安全

2 - 內部成員變量和底層數據結構

public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
    //key比較器
    private final Comparator<? super K> comparator;
    
    //根節點
    private transient Entry<K,V> root;

    //容器存儲的節點個數
    private transient int size = 0;

    //結構化修改的次數
    private transient int modCount = 0;
}

    其餘成員變量直接看源碼註釋就能夠了,下面我看來看下根節點Entry類的源碼:數據結構

static final class Entry<K,V> implements Map.Entry<K,V> {
        //鍵值對節點的鍵
        K key;
        //鍵值對節點的值
        V value;
        //左孩子節點
        Entry<K,V> left;
        //右孩子節點
        Entry<K,V> right;
        //雙親節點
        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;
        }

        //獲取節點key
        public K getKey() {
            return key;
        }

        //獲取鍵值對映射節點的value值
        public V getValue() {
            return value;
        }
        //設置鍵值對映射節點的value值
        public V setValue(V value) {
            V oldValue = this.value;
            this.value = value;
            return oldValue;
        }

        //節點比較
        public boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;

            return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
        }
        
       
        public int hashCode() {
            int keyHash = (key==null ? 0 : key.hashCode());
            int valueHash = (value==null ? 0 : value.hashCode());
            return keyHash ^ valueHash;
        }

        public String toString() {
            return key + "=" + value;
        }
    }

    查看Entry類的內部屬性咱們知道TreeMap底層是基於紅黑樹實現的,這裏對於紅黑樹這種數據結構暫時不作詳細分析,只要知道這是一種大體平衡的二叉樹,且查詢所需的時間複雜度爲O(logN)app

3、TreeMap的源碼解析

1 - 構造函數

public TreeMap() {
        comparator = null;
    }

    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }

    public TreeMap(Map<? extends K, ? extends V> m) {
        comparator = null;
        putAll(m);
    }

    public TreeMap(SortedMap<K, ? extends V> m) {
        comparator = m.comparator();
        try {
            buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
        } catch (java.io.IOException cannotHappen) {
        } catch (ClassNotFoundException cannotHappen) {
        }
    }

    咱們依次分析一下TreeMap提供的四種構造函數。函數

1)TreeMap()ui

沒作啥,把排序器comparator設置爲空,comparator被final修飾且以後鍵值對映射排序以前須要判斷comparator是否爲空。this

2)TreeMap(Comparator<? super K> comparator)spa

將內部的key比較器設置爲方法指定的比較器comparator線程

3)TreeMap(Map<? extends K, ? extends V> m)指針

方法入參是一個Map對象由於Map內部鍵值對存儲不是有序的,不包含一個比較器comparator,將內部的比較器設置爲null,把map對象包含的全部鍵值對按照Key實現的天然順序插入TreeMap

4)TreeMap(SortedMap<K, ? extends V> m) 

設置TreeMap內部比較器爲SortedMap對象m內部指定的的比較器。而後按序遍歷m中的全部鍵值對節點插入到TreeMap

2 - V get(Object key)根據key獲取鍵值對映射value

public V get(Object key) {
        Entry<K,V> p = getEntry(key);
        return (p==null ? null : p.value);
    }

    get方法內部調用getEntry方法根據key獲取TreeMap容器中保存的鍵值對節點p,而後判斷節點p是否爲空,爲空返回null,不爲空返回節點的value屬性。咱們繼續跟進getEntry(key)方法源碼看下該方法內部作了什麼。

final Entry<K,V> getEntry(Object key) {
        //內部比較器comparator不爲空,基於它指定的比較器遍歷紅黑樹獲取key對應節點
        if (comparator != null)
            return getEntryUsingComparator(key);
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        Entry<K,V> p = root;
        //遍歷TreeMap內部紅黑樹,基於指定對象key定義的比較函數比較指定key與節點key,獲取指定key對應的節點信息
        while (p != null) {
            int cmp = k.compareTo(p.key);
            if (cmp < 0)
                //指定key小於當前節點,繼續遍歷當前節點的左孩子節點
                p = p.left;
            else if (cmp > 0)
                //指定key大於當前節點,繼續遍歷當前節點的右孩子節點
                p = p.right;
            else
                //相等直接返回當前節點
                return p;
        }
        return null;
    }

    方法內部首先判斷內部比較器comparator是否爲null,不爲null直接調用getEntryUsingComparator(key)使用內部指定的比較器遍歷比較TreeMap內部紅黑樹中的鍵值對節點key與指定key獲取指定key匹配的節點信息,咱們跟進該方法看下源碼層面如何實現

final Entry<K,V> getEntryUsingComparator(Object key) {
        @SuppressWarnings("unchecked")
            K k = (K) key;
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            Entry<K,V> 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;
    }

    該方法內部實現邏輯很簡單,就是從根節點開始遍歷內部紅黑樹,基於內部比較器比較指定key與當前節點的key,相等直接返回當前節點,若是指定key小於當前節點key繼續遍歷當前節點的左節點,不然繼續遍歷當前節點的右節點。遍歷完成還沒找到指定key對應節點信息或者內部比較器comparator爲null則方法返回null。

    返回上一個方法getEntry繼續分析。接下來對方法入參key進行判空,若是對象爲null拋出空指針異常。接下來遍歷TreeMap內部紅黑樹從根節點開始使用指定key類內部實現的compare方法(即類天然順序)對兩個對象key和紅黑樹當前節點key比較,若等於表示匹配上返回當前節點,小於繼續遍歷左孩子節點,大於則遍歷右孩子節點。

 

3 - V put(K key, V value)插入鍵值對

public V put(K key, V value) {
        Entry<K,V> t = root;
        //若根節點爲空
        if (t == null) {
            //類型校驗可能爲null
            compare(key, key); // type (and possibly null) check
            //基於鍵值對建立新的根節點
            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        //根節點不爲空繼續執行後續代碼
        int cmp;
        Entry<K,V> parent;
        Comparator<? super K> cpr = comparator;
        //若容器內部存在比較器,則遍歷紅黑樹基於比較器comparator比較節點key和指定key獲取指定鍵值對的插入節點,若該 
        //位置已經存在,則用新值value取代舊值
        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中定義的天然順序遍歷容器內部紅黑樹,比較節點key和指定key
        //獲取指定鍵值對的插入節點,若指定key已存在則用新值value覆蓋舊值
        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);
        }
        //建立新節點,設置雙親節點parent
        Entry<K,V> e = new Entry<>(key, value, parent);
        //若新插入節點的邏輯順序小於雙親節點,則它被設置爲雙親節點的左孩子
        if (cmp < 0)
            parent.left = e;
        //不然設置爲雙親節點的右孩子
        else
            parent.right = e;
        //執行插入修正操做,保證在插入新節點以後仍然是紅黑樹,這裏的調整操做主要包括兩種1修改節點顏色;2對某些節點進 
        //行旋轉進行旋轉
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

4 - V remove(Object key)刪除key對應的鍵值對

public V remove(Object key) {
        Entry<K,V> p = getEntry(key);
        if (p == null)
            return null;

        V oldValue = p.value;
        deleteEntry(p);
        return oldValue;
    }

    方法內部首先調用getEntry方法獲取key對應的鍵值對節點,接着進行判空,若是返回的節點爲空方法結束返回null,不然調用deleteEntry方法刪除節點,返回刪除節點的value。咱們進入deleteEntry方法源碼看下它是如何實現的。

private void deleteEntry(Entry<K,V> p) {
        //爲何結構修改計數器和鍵值對個數size放在方法開頭更新?
        modCount++;
        size--;
        //待刪除節點左右孩子節點均不爲空
        if (p.left != null && p.right != null) {
            //獲取指定節點的後繼節點
            Entry<K,V> s = successor(p);
            p.key = s.key;
            p.value = s.value;
            p = s;
        } 

    
        Entry<K,V> replacement = (p.left != null ? p.left : p.right);
        //若刪除節點左節點或者右節點不爲空不爲空
        if (replacement != null) {
            //替換節點的雙親節點指向被刪除節點的雙親節點
            replacement.parent = p.parent;
            //被刪除節點是根節點則重置根節點爲替換節點
            if (p.parent == null)
                root = replacement;
            //若被刪除節點是他雙親節點的左孩子則讓被刪除節點的左孩子節點指向當前替換節點replacement
            else if (p == p.parent.left)
                p.parent.left  = replacement;
            //若被刪除節點爲雙親節點的右孩子則讓被刪除節點的右孩子節點指向當前節點replacement
            else
                p.parent.right = replacement;

            // Null out links so they are OK to use by fixAfterDeletion.
            p.left = p.right = p.parent = null;
            //若是被刪除的節點是黑色則須要執行刪除後修復,保證刪除節點後底層存儲結構仍然是紅黑樹
            if (p.color == BLACK)
                fixAfterDeletion(replacement);
        } else if (p.parent == null) {//若被刪除節點是根節點,且左右孩子節點均爲空,則根節點置空
            root = null;
        } else { //被刪除節點不是根節點且左右子樹均爲空,由於紅黑樹須要知足一個節點到它的子孫節點的全部路徑都包含相 
        //同數目的黑節點則,所以在刪除以後須要判斷當前節點是不是黑節點若是是則可能違反紅黑樹的規定,須要調用 
         //fixAfterDeletion執行刪除後處理保證刪除節點後鍵值對的存儲結構仍然是紅黑樹
            if (p.color == BLACK)
                fixAfterDeletion(p);
            //若是若被刪除節點的雙親節點不爲空,斷開被刪除節點與雙親節點的鏈接
            if (p.parent != null) {
                //若被刪除節點是雙親節點的左孩子,讓被刪除節點的左孩子指向null
                if (p == p.parent.left)
                    p.parent.left = null;
                //若被刪除節點是雙親節點的右孩子則讓被刪除節點的右孩子指向null
                else if (p == p.parent.right)
                    p.parent.right = null;
                //被刪除節點的雙親節點指向null
                p.parent = null;
            }
        }
    }

    分析當前方法結合以前的調用方方法源碼咱們能夠大概整理出remove方法的基本邏輯以下:

1)根據key遍歷紅黑樹獲取待刪除節點,若節點爲空返回null;

2)若待刪除節點不爲空,調用deleteEntry刪除節點,若待刪除節點的左右孩子節點都存在則讓待刪除節點的後繼節點替代待刪除節點,對替換以後的待刪除節點分狀況進行處理:

   選擇待刪除節點的左孩子節點或右孩子節點爲替代節點replacement,優先選擇左孩子若左孩子不存在選擇右孩子節點。

   1.若替代節點replacement不爲null。若待刪除節點爲根節點時,重置根節點爲替換節點replacement;若待刪除節點爲雙親節點的左孩子,則讓待刪除節點的左孩子指向替代節點replacement,不然讓它的雙親節點的右孩子指向替代節點replacement;

以後斷開待刪除節點與其餘節點的鏈接釋放該節點,若是刪除的節點是黑色BLACK則還須要調用fixAfterDeletion方法進行節點刪除後調整,保證紅黑樹結構不被破壞;

    2.若待刪除節點p是根節點,根節點置空;

    3.若待刪除節點不是根節點且左右子樹均爲空。若被刪除節點顏色是黑色BLACK,由於紅黑樹必須保證,任意一個葉子節點到根節點的路徑中包含黑色節點的個數相同,因此若被刪除節點是根節點還須要調用fixAfterDeletion進行節點刪除調整,斷開與雙親節點的鏈接。

相關文章
相關標籤/搜索