TreeMap的深入剖析

TreeMap的深入剖析

一、簡介

TreeMap最早出現在JDK 1.2中,是 Java 集合框架中比較重要一個的實現。TreeMap 底層基於紅黑樹實現,可保證在log(n)時間複雜度內完成 containsKey、get、put 和 remove 操作,效率很高。另一方面,由於 TreeMap 基於紅黑樹實現,這爲 TreeMap 保持鍵的有序性打下了基礎。總的來說,TreeMap 的核心是紅黑樹,其很多方法也是對紅黑樹增刪查基礎操作的一個包裝。所以只要弄懂了紅黑樹,TreeMap 就沒什麼祕密了。

二、概覽

TreeMap繼承自AbstractMap,並實現了 NavigableMap接口。NavigableMap 接口繼承了SortedMap接口,SortedMap 最終繼承自Map接口,同時 AbstractMap 類也實現了 Map 接口。以上就是 TreeMap 的繼承體系,描述起來有點亂,不如看圖了:

mark

  • TreeMap實現了NavigableMap接口,而NavigableMap接口繼承着SortedMap接口,致使我們的TreeMap是有序的
  • TreeMap底層是紅黑樹,它方法的時間複雜度都不會太高:log(n)~
  • 非同步
  • 使用Comparator或者Comparable來比較key是否相等與排序的問題~

上圖就是 TreeMap 的繼承體系圖,比較直觀。這裏來簡單說一下繼承體系中不常見的接口NavigableMapSortedMap,這兩個接口見名知意。先說 NavigableMap 接口,NavigableMap 接口聲明瞭一些列具有導航功能的方法,比如:

/**
 * 返回紅黑樹中最小鍵所對應的 Entry
 */
Map.Entry<K,V> firstEntry();

/**
 * 返回最大的鍵 maxKey,且 maxKey 僅小於參數 key
 */
K lowerKey(K key);

/**
 * 返回最小的鍵 minKey,且 minKey 僅大於參數 key
 */
K higherKey(K key);

// 其他略

通過這些導航方法,我們可以快速定位到目標的 key 或 Entry。

對於SortedMap來說,該類是TreeMap體系中的父接口,也是區別於HashMap體系最關鍵的一個接口。

主要原因就是SortedMap接口中定義的第一個方法—Comparator

public interface SortedMap<K,V> extends Map<K,V> {

    //返回元素比較器。如果是自然順序,則返回null;
    Comparator<? super K> comparator();

    //返回從fromKey到toKey的集合:含頭不含尾
    java.util.SortedMap<K,V> subMap(K fromKey, K toKey);

    //返回從頭到toKey的集合:不包含toKey
    java.util.SortedMap<K,V> headMap(K toKey);

    //返回從fromKey到結尾的集合:包含fromKey
    java.util.SortedMap<K,V> tailMap(K fromKey);

    //返回集合中的第一個元素:
    K firstKey();

    //返回集合中的最後一個元素:
    K lastKey();

    //返回集合中所有key的集合:
    Set<K> keySet();

    //返回集合中所有value的集合:
    Collection<V> values();

    //返回集合中的元素映射:
    Set<Map.Entry<K, V>> entrySet();
}

TreeMap具有如下特點:

  • 不允許出現重複的key;
  • 可以插入null鍵,null值;
  • 可以對元素進行排序;
  • 無序集合(插入和遍歷順序不一致);

2.1、屬性

mark

三、源碼分析

JDK 1.8中的TreeMap源碼有兩千多行,還是比較多的。本文並不打算逐句分析所有的源碼,而是挑選幾個常用的方法進行分析。這些方法實現的功能分別是查找、遍歷、插入、刪除等,其他的方法小夥伴們有興趣可以自己分析。TreeMap實現的核心部分是關於紅黑樹的實現,其絕大部分的方法基本都是對底層紅黑樹增、刪、查操作的一個封裝。如簡介一節所說,只要弄懂了紅黑樹原理,TreeMap 就沒什麼祕密了。關於紅黑樹的原理,請參考文章-紅黑樹詳細分析,本篇文章不會對此展開討論。

3.1構造函數

TreeMap() 
          使用鍵的自然順序構造一個新的、空的樹映射。
TreeMap(Comparator<? super K> comparator) 
          構造一個新的、空的樹映射,該映射根據給定比較器進行排序。
TreeMap(Map<? extends K,? extends V> m) 
          構造一個與給定映射具有相同映射關係的新的樹映射,該映射根據其鍵的自然順序 進行排序。
TreeMap(SortedMap<K,? extends V> m) 
          構造一個與指定有序映射具有相同映射關係和相同排序順序的新的樹映射。

這裏我們簡單的看看TreeMap的使用方式

(1)使用元素自然排序

public class SortedTest implements Comparable<SortedTest> {
    private int age;
    public SortedTest(int age){
        this.age = age;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    //自定義對象,實現compareTo(T o)方法:
    public int compareTo(SortedTest sortedTest) {
        int num = this.age - sortedTest.getAge();
        //爲0時候,兩者相同:
        if(num==0){
            return 0;
            //大於0時,傳入的參數小:
        }else if(num>0){
            return 1;
            //小於0時,傳入的參數大:
        }else{
            return -1;
        }
    }

    @Override
    public String toString() {
        return "SortedTest{" +
                "age=" + age +
                '}';
    }
}
  • 在使用自然排序的時候分爲兩種情況,第一種是JDK定義的對象,第二種是 我們自己定義的對象
  • 在自然順序的比較中,需要讓被比較的 元素實現Comparable接口,否則在向集合中他添加元素時報”java.lang.ClassCastException: com.jiaboyan.collection.map.SortedTest cannot be cast to java.lang.Comparable”異常; 這是因爲在調用put()方法時,會將傳入的元素轉化成Comparable類型對象,所以當你傳入的元素沒有實現Comparable接口時,就無法轉換,遍會報錯;
  • 使用JDK定義的對象的時候,JDK是幫我們實現了Compareble接口的自然排序;在使用自定義對象的時候是需要我們自己實現Comparable接口的
public class TreeMapTest {
    public static void main(String[] agrs){
        //自然順序比較
        naturalSort();
    }
    //自然排序順序:
    public static void naturalSort(){
        //第一種情況:Integer對象,沒有實現Comparator使用的是自然排序
        TreeMap<Integer,String> treeMapFirst = new TreeMap<Integer, String>();
        treeMapFirst.put(1,"huanghe");
        treeMapFirst.put(6,"huanghe");
        treeMapFirst.put(3,"huanghe");
        treeMapFirst.put(10,"huanghe");
        treeMapFirst.put(7,"huanghe");
        treeMapFirst.put(13,"huanghe");
        System.out.println(treeMapFirst.toString());

        //第二種情況:SortedTest對象
        TreeMap<SortedTest,String> treeMapSecond = new TreeMap<SortedTest, String>();
        treeMapSecond.put(new SortedTest(10),"huanghe");
        treeMapSecond.put(new SortedTest(1),"huanghe");
        treeMapSecond.put(new SortedTest(13),"huanghe");
        treeMapSecond.put(new SortedTest(4),"huanghe");
        treeMapSecond.put(new SortedTest(0),"huanghe");
        treeMapSecond.put(new SortedTest(9),"huanghe");
        System.out.println(treeMapSecond.toString());
    }

(2)使用自定義比較器排序

使用自定義比較器排序,需要在創建TreeMap對象時,將自定義比較器對象傳入到TreeMap構造方法中;

自定義比較器對象,需要實現Comparator接口,並實現比較方法compare(T o1,T o2);

值得一提的是,使用自定義比較器排序的話,被比較的對象無需再實現Comparable接口了;

public class SortedTest {
    private int age;
    public SortedTest(int age){
        this.age = age;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}
public class SortedTestComparator implements Comparator<SortedTest> {
    //自定義比較器:實現compare(T o1,T o2)方法:
    public int compare(SortedTest sortedTest1, SortedTest sortedTest2) {
        int num = sortedTest1.getAge() - sortedTest2.getAge();
        if(num==0){//爲0時候,兩者相同:
            return 0;
        }else if(num>0){//大於0時,後面的參數小:
            return 1;
        }else{//小於0時,前面的參數小:
            return -1;
        }
    }
}

public class TreeMapTest {
    public static void main(String[] agrs){
        //自定義順序比較
        customSort();
    }
    //自定義排序順序:
    public static void customSort(){
        TreeMap<SortedTest,String> treeMap = new TreeMap<SortedTest, String>(new SortedTestComparator());
        treeMap.put(new SortedTest(10),"hello");
        treeMap.put(new SortedTest(21),"my");
        treeMap.put(new SortedTest(15),"name");
        treeMap.put(new SortedTest(2),"is");
        treeMap.put(new SortedTest(1),"jiaboyan");
        treeMap.put(new SortedTest(7),"world");
        System.out.println(treeMap.toString());
    }
}

3.2 查找

TreeMap基於紅黑樹實現,而紅黑樹是一種自平衡二叉查找樹,所以 TreeMap 的查找操作流程和二叉查找樹一致。二叉樹的查找流程是這樣的,先將目標值和根節點的值進行比較,如果目標值小於根節點的值,則再和根節點的左孩子進行比較。如果目標值大於根節點的值,則繼續和根節點的右孩子比較。在查找過程中,如果目標值和二叉樹中的某個節點值相等,則返回 true,否則返回 false。TreeMap 查找和此類似,只不過在 TreeMap 中,節點(Entry)存儲的是鍵值對<k,v>。在查找過程中,比較的是鍵的大小,返回的是值,如果沒找到,則返回null。TreeMap 中的查找方法是get,具體實現在getEntry方法中,相關源碼如下:

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

final Entry<K,V> getEntry(Object key) {
    // Offload comparator-based version for sake of performance
    //如果comparator不爲null,調的就是
    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;

    // 查找操作的核心邏輯就在這個 while 循環裏
    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;
}

如果Comparator不爲null,接下來我們進去看看getEntryUsingComparator(Object 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) {
                //調用的是Comparator自己實現的方法來回去對應位置,總體的邏輯和外面的
                //沒有什麼區別
                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;
    }

3.3 遍歷

遍歷操作也是大家使用頻率較高的一個操作,對於TreeMap,使用方式一般如下:

for(Object key : map.keySet()) {
    // do something
}

或者

for(Map.Entry entry : map.entrySet()) {
    // do something
}

從上面代碼片段中可以看出,大家一般都是對 TreeMap 的 key 集合或 Entry 集合進行遍歷。上面代碼片段中用 foreach 遍歷keySet 方法產生的集合,在編譯時會轉換成用迭代器遍歷,等價於:

Set keys = map.keySet();
Iterator ite = keys.iterator();
while (ite.hasNext()) {
    Object key = ite.next();
    // do something
}

另一方面,TreeMap 有一個特性,即可以保證鍵的有序性,默認是正序。所以在遍歷過程中,大家會發現 TreeMap 會從小到大輸出鍵的值。那麼,接下來就來分析一下keySet方法,以及在遍歷 keySet 方法產生的集合時,TreeMap 是如何保證鍵的有序性的。相關代碼如下:

public Set<K> keySet() {
    return navigableKeySet();
}

public NavigableSet<K> navigableKeySet() {
    KeySet<K> nks = navigableKeySet;
    return (nks != null) ? nks : (navigableKeySet = new KeySet<>(this));
}

static final class KeySet<E> extends AbstractSet<E> implements NavigableSet<E> {
    private final NavigableMap<E, ?> m;
    KeySet(NavigableMap<E,?> map) { m = map; }

    public Iterator<E> iterator() {
        if (m instanceof TreeMap)
            return ((TreeMap<E,?>)m).keyIterator();
        else
            return ((TreeMap.NavigableSubMap<E,?>)m).keyIterator();
    }

    // 省略非關鍵代碼
}

Iterator<K> keyIterator() {
    return new KeyIterator(getFirstEntry());
}

final class KeyIterator extends PrivateEntryIterator<K> {
    KeyIterator(Entry<K,V> first) {
        super(first);
    }
    public K next() {
        return nextEntry().key;
    }
}

abstract class PrivateEntryIterator<T> implements Iterator<T> {
    Entry<K,V> next;
    Entry<K,V> lastReturned;
    int expectedModCount;

    PrivateEntryIterator(Entry<K,V> first) {
        expectedModCount = modCount;
        lastReturned = null;
        next = first;
    }

    public final boolean hasNext() {
        return next != null;
    }

    final Entry<K,V> nextEntry() {
        Entry<K,V> e = next;
        if (e == null)
            throw new NoSu chElementException();
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        // 尋找節點 e 的後繼節點
        next = successor(e);
        lastReturned = e;
        return e;
    }

    // 其他方法省略
}

上面的代碼比較多,keySet 涉及的代碼還是比較多的,大家可以從上往下看。從上面源碼可以看出 keySet 方法返回的是KeySet類的對象。這個類實現了Iterable接口,可以返回一個迭代器。該迭代器的具體實現是KeyIterator,而 KeyIterator 類的核心邏輯是在PrivateEntryIterator中實現的。上面的代碼雖多,但核心代碼還是 KeySet 類和 PrivateEntryIterator 類的 nextEntry方法。KeySet 類就是一個集合,這裏不分析了。而 nextEntry 方法比較重要,下面簡單分析一下。

在初始化 KeyIterator 時,會將 TreeMap 中包含最小鍵的 Entry 傳給 PrivateEntryIterator。當調用 nextEntry 方法時,通過調用 successor 方法找到當前 entry 的後繼,並讓 next 指向後繼,最後返回當前的 entry。通過這種方式即可實現按正序返回鍵值的的邏輯。

3.3、插入

相對於前兩個操作,插入操作明顯要複雜一些。當往 TreeMap 中放入新的鍵值對後,可能會破壞紅黑樹的性質。這裏爲了描述方便,把 Entry 稱爲節點。並把新插入的節點稱爲N,N 的父節點爲P。P 的父節點爲G,且 P 是 G 的左孩子。P 的兄弟節點爲U。在往紅黑樹中插入新的節點 N 後(新節點爲紅色),會產生下面5種情況:

  1. N 是根節點
  2. N 的父節點是黑色
  3. N 的父節點是紅色,叔叔節點也是紅色
  4. N 的父節點是紅色,叔叔節點是黑色,且 N 是 P 的右孩子
  5. N 的父節點是紅色,叔叔節點是黑色,且 N 是 P 的左孩子

① 情況說明:被插入的節點是根節點。 

​ 處理方法:直接把此節點塗爲黑色。

② 情況說明:被插入的節點的父節點是黑色。 

​ 處理方法:什麼也不需要做。節點被插入後,仍然是紅黑樹。

 ③ 情況說明:被插入的節點的父節點是紅色。 

​ 處理方法:這時就需要分多種情況進行考慮了

上面5中情況中,情況2不會破壞紅黑樹性質,所以無需處理。情況1 會破壞紅黑樹性質2(根是黑色),情況3、4、和5會破壞紅黑樹性質4(每個紅色節點必須有兩個黑色的子節點)。這個時候就需要進行調整,以使紅黑樹重新恢復平衡。至於怎麼調整,可以參考我另一篇關於紅黑樹的文章(紅黑樹詳細分析),這裏不再重複說明。接下來分析一下插入操作相關源碼:

public V put(K key, V value) {
    Entry<K,V> t = root;
    // 1.如果根節點爲 null,將新節點設爲根節點
    if (t == null) {
        compare(key, key);
        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    int cmp;
    Entry<K,V> parent;
    // split comparator and comparable paths
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        // 2.爲 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 {
        // 與上面代碼邏輯類似,省略
    }
    Entry<K,V> e = new Entry<>(key, value, parent);
    // 3.將新節點鏈入紅黑樹中
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    // 4.插入新節點可能會破壞紅黑樹性質,這裏修正一下
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}

put 方法代碼如上,邏輯和二叉查找樹插入節點邏輯一致。重要的步驟我已經寫了註釋,並不難理解。插入邏輯的複雜之處在於插入後的修復操作,對應的方法fixAfterInsertion,該方法的源碼和說明如下:

mark

到這裏,插入操作就講完了。接下來,來說說 TreeMap 中最複雜的部分,也就是刪除操作了。

3.4 刪除

刪除操作是紅黑樹最複雜的部分,原因是該操作可能會破壞紅黑樹性質5(從任一節點到其每個葉子的所有簡單路徑都包含相同數目的黑色節點),修復性質5要比修復其他性質(性質2和4需修復,性質1和3不用修復)複雜的多。當刪除操作導致性質5被破壞時,會出現8種情況。爲了方便表述,這裏還是先做一些假設。我們把最終被刪除的節點稱爲 X,X 的替換節點稱爲 N。N 的父節點爲P,且 N 是 P 的左孩子。N 的兄弟節點爲S,S 的左孩子爲 SL,右孩子爲 SR。這裏特地強調 X 是 最終被刪除 的節點,是原因二叉查找樹會把要刪除有兩個孩子的節點的情況轉化爲刪除只有一個孩子的節點的情況,該節點是欲被刪除節點的前驅和後繼。

接下來,簡單列舉一下刪除節點時可能會出現的情況,先列舉較爲簡單的情況:

  1. 最終被刪除的節點 X 是紅色節點
  2. X 是黑色節點,但該節點的孩子節點是紅色

比較複雜的情況:

  1. 替換節點 N 是新的根
  2. N 爲黑色,N 的兄弟節點 S 爲紅色,其他節點爲黑色。
  3. N 爲黑色,N 的父節點 P,兄弟節點 S 和 S 的孩子節點均爲黑色。
  4. N 爲黑色,P 是紅色,S 和 S 孩子均爲黑色。
  5. N 爲黑色,P 可紅可黑,S 爲黑色,S 的左孩子 SL 爲紅色,右孩子 SR 爲黑色
  6. N 爲黑色,P 可紅可黑,S 爲黑色,SR 爲紅色,SL 可紅可黑

上面列舉的8種情況中,前兩種處理起來比較簡單,後6種情況中情況26較爲複雜。接下來我將會對情況26展開分析,刪除相關的源碼如下:

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

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

private void deleteEntry(Entry<K,V> p) {
    modCount++;
    size--;

    /* 
     * 1. 如果 p 有兩個孩子節點,則找到後繼節點,
     * 並把後繼節點的值複製到節點 P 中,並讓 p 指向其後繼節點
     */
    if (p.left != null && p.right != null) {
        Entry<K,V> s = successor(p);
        p.key = s.key;
        p.value = s.value;
        p = s;
    } // p has 2 children

    // Start fixup at replacement node, if it exists.
    Entry<K,V> replacement = (p.left != null ? p.left : p.right);

    if (replacement != null) {
        /*
         * 2. 將 replacement parent 引用指向新的父節點,
         * 同時讓新的父節點指向 replacement。
         */ 
        replacement.parent = p.parent;
        if (p.parent == null)
            root = replacement;
        else if (p == p.parent.left)
            p.parent.left  = replacement;
        else
            p.parent.right = replacement;

        // Null out links so they are OK to use by fixAfterDeletion.
        p.left = p.right = p.parent = null;

        // 3. 如果刪除的節點 p 是黑色節點,則需要進行調整
        if (p.color == BLACK)
            fixAfterDeletion(replacement);
    } else if (p.parent == null) { // 刪除的是根節點,且樹中當前只有一個節點
        root = null;
    } else { // 刪除的節點沒有孩子節點
        // p 是黑色,則需要進行調整
        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;
        }
    }
}

從源碼中可以看出,remove方法只是一個簡單的保證,核心實現在deleteEntry方法中。deleteEntry 主要做了這麼幾件事:

  1. 如果待刪除節點 P 有兩個孩子,則先找到 P 的後繼 S,然後將 S 中的值拷貝到 P 中,並讓 P 指向 S
  2. 如果最終被刪除節點 P(P 現在指向最終被刪除節點)的孩子不爲空,則用其孩子節點替換掉
  3. 如果最終被刪除的節點是黑色的話,調用 fixAfterDeletion 方法進行修復

上面說了 replacement 不爲空時,deleteEntry 的執行邏輯。上面說的略微囉嗦,如果簡單說的話,7個字即可總結:找後繼 -> 替換 -> 修復。這三步中,最複雜的是修復操作。修復操作要重新使紅黑樹恢復平衡,修復操作的源碼分析如下:

fixAfterDeletion 方法分析如下:

mark

上面對 fixAfterDeletion 部分代碼邏輯就行了分析,通過配圖的形式解析了每段代碼邏輯所處理的情況。通過圖解,應該還是比較好理解的。好了,TreeMap 源碼先分析到這裏。

總結

對於TreeMap的理解,最重要的是對紅黑樹要進行深刻的認識

參考文獻

TreeMap源碼分析

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