Java經常使用數據結構之Map(3)-TreeMap

以前公衆號發佈的文章中,《Java經常使用數據結構系列》漏了一章,就直接在掘金髮布了。html

前言

TreeMap是一種帶有排序功能的key-value存儲結構,它是經過紅黑樹實現的。若是想學習TreeMap的內部細節操做(旋轉平衡處理等),就必須充分學習紅黑樹。本文不關注紅黑樹操做的具體細節(你們自行補課),只分析TreeMap自身的特色。java

總體結構

先來看看TreeMap的繼承關係:node

public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable 複製代碼
  • 繼承了AbstractMap抽象類,下降實現成本,會實現entrySet()方法;
  • 實現了NavigableMap接口,意味着支持一系列導航方法;
  • 實現了Cloneable接口,能夠被克隆;
  • 實現了Serializable接口,能夠進行系列化;

主要說一下NavigableMap接口:安全

public interface NavigableMap<K,V> extends SortedMap<K,V> 複製代碼

繼承自SortedMap接口:數據結構

public interface SortedMap<K,V> extends Map<K,V> 複製代碼

顧名思義,SortedMap的職責是排序,而NavigableMap的職責是在排好序的集合中進行各類導航搜索的。
看SortedMap中的關鍵方法:app

/** * Returns the comparator used to order the keys in this map, or * {@code null} if this map uses the {@linkplain Comparable * natural ordering} of its keys. * * @return the comparator used to order the keys in this map, * or {@code null} if this map uses the natural ordering * of its keys */
    Comparator<? super K> comparator();
複製代碼

comparator()方法就是返回比較器的。從註釋中能夠看出,有兩種排序方式:一種是天然排序(返回null),另外一種則是自定義排序(返回Comparator實例)。函數

  • 天然排序:要求Key必須實現Comparable接口,而且全部的Key都是同一個類的對象,不然會報ClassCastException異常。
  • 自定義排序:須要實現一個Comparator比較器,不要求Key實現Comparable接口。

瀏覽一下NavigableMap中的部分導航方法。源碼分析

// 返回小於key的第一個元素
Map.Entry<K,V> lowerEntry(K key);
... // 一系列相似方法

// 返回倒序集合
NavigableMap<K,V> descendingMap();
...

// 返回子集合,開閉區間
NavigableMap<K,V> subMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive);
... // 一系列相似方法
複製代碼

源碼分析

核心紅黑樹

首當其衝的固然是用來存儲key-value鍵值對的存儲結構了。學習

// 直接用布爾值來表示
private static final boolean RED   = false;
private static final boolean BLACK = true;

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是紅黑樹的樹結點結構,和HashMap中的TreeNode稍有區別。
而後就是紅黑樹的相關操做了,這裏僅簡單說明,不作展開。ui

// 左旋:左子樹不平衡時使用
private void rotateLeft(Entry<K,V> p) // 右旋:右子樹不平衡時使用 private void rotateRight(Entry<K,V> p) // 插入新結點 public V put(K key, V value) // 插入新結點後的調整,保證新樹仍是紅黑樹 private void fixAfterInsertion(Entry<K,V> x) // 刪除某個結點 private void deleteEntry(Entry<K,V> p) // 刪除某個結點後的調整,保證新樹仍是紅黑樹 private void fixAfterDeletion(Entry<K,V> x) 複製代碼

這裏僅分析put方法:

public V put(K key, V value) {
        Entry<K,V> t = root;
        if (t == 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;
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator; // 自定義比較器
        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);
        }
        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);
        }
        // 構建新結點
        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的構造函數

// 使用天然排序
public TreeMap() // 使用自定義排序 public TreeMap(Comparator<? super K> comparator) // 傳的map不必定是有序的,因此調用的是putAll方法來進行添加 public TreeMap(Map<? extends K, ? extends V> m) {
        comparator = null;
        putAll(m);
    }
    
// 傳的map是有序的,須要作一些調整
public TreeMap(SortedMap<K, ? extends V> m) {
        comparator = m.comparator();
        try {
            buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
        } catch (java.io.IOException | ClassNotFoundException cannotHappen) {
        }
    }
複製代碼

第三個和第四個構造方法的實現是不一樣的。在第三個構造方法中,不能保證傳入的Map是有序的,因此須要調用putAll方法將元素一個一個添加到Map中。而第四個構造方法中,傳入的就是一個有序的Map,因此直接將傳入的Map轉成紅黑樹了。

private void buildFromSorted(int size, Iterator<?> it, java.io.ObjectInputStream str, V defaultVal) throws java.io.IOException, ClassNotFoundException {
        this.size = size;
        // 將轉換後的樹的根結點賦值給TreeMap的根結點
        // computeRedLevel能夠理解爲計算樹的高度
        root = buildFromSorted(0, 0, size-1, computeRedLevel(size),
                               it, str, defaultVal);
    }
    
private final Entry<K,V> buildFromSorted(int level, int lo, int hi, int redLevel, Iterator<?> it, java.io.ObjectInputStream str, V defaultVal) throws java.io.IOException, ClassNotFoundException {

        if (hi < lo) return null;

        int mid = (lo + hi) >>> 1; // 取中間位置
        // 遞歸左子樹
        Entry<K,V> left  = null;
        if (lo < mid)
            left = buildFromSorted(level+1, lo, mid - 1, redLevel,
                                   it, str, defaultVal);

        // extract key and/or value from iterator or stream
        K key;
        V value;
        if (it != null) {
            if (defaultVal==null) {
                Map.Entry<?,?> entry = (Map.Entry<?,?>)it.next();
                key = (K)entry.getKey();
                value = (V)entry.getValue();
            } else {
                key = (K)it.next();
                value = defaultVal;
            }
        } else { // use stream
            key = (K) str.readObject();
            value = (defaultVal != null ? defaultVal : (V) str.readObject());
        }

        Entry<K,V> middle =  new Entry<>(key, value, null);

        // color nodes in non-full bottommost level red
        if (level == redLevel) // 最底層的結點設成紅色
            middle.color = RED;

        if (left != null) {
            middle.left = left;
            left.parent = middle;
        }
        // 遞歸右子樹
        if (mid < hi) {
            Entry<K,V> right = buildFromSorted(level+1, mid+1, hi, redLevel,
                                               it, str, defaultVal);
            middle.right = right;
            right.parent = middle;
        }

        return middle; // 返回根結點
    }
複製代碼

這個轉換方法是經過遞歸來將全部結點關聯成一個紅黑樹的,且會返回根結點(其實就是中間點)。有意思的是,它只將最底層的結點設置成了紅色,而其餘結點都是黑色。這樣是爲了方便後續結點的插入。

TreeMap的PrivateEntryIterator

TreeMap中全部迭代器子類都繼承自PrivateEntryIterator

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() {
            ...
            next = successor(e); // 二叉樹查找,主要查右子樹
            lastReturned = e;
            return e;
        }

        // 前一個結點
        final Entry<K,V> prevEntry() {
            ...
            next = predecessor(e); // 二叉樹查找,主要查左子樹
            lastReturned = e;
            return e;
        }

        public void remove() {
            ...
        }
    }
複製代碼

直接看successor方法,predecessor方法相似。

static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
        if (t == null)
            return null;
        else if (t.right != null) { // 右子樹不爲空,即存在比當前結點大的結點
            Entry<K,V> p = t.right;
            while (p.left != null) // 這裏就須要查左子樹了
                p = p.left;
            return p;
        } else { // 右子樹爲空
            Entry<K,V> p = t.parent;
            Entry<K,V> ch = t;
            while (p != null && ch == p.right) { // 針對葉結點
                ch = p;
                p = p.parent;
            }
            // 由於結點t多是其父節點的左子樹,也多是右子樹
            return p;
        }
    }
複製代碼

由於繼承了AbstractMap,因此必須實現entrySet()方法:

public Set<Map.Entry<K,V>> entrySet() {
        EntrySet es = entrySet;
        return (es != null) ? es : (entrySet = new EntrySet());
    }
    
class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        public Iterator<Map.Entry<K,V>> iterator() {
            // 把紅黑樹的最小結點做爲迭代器的第一個結點
            return new EntryIterator(getFirstEntry());
        }
        
        ...
    }

final class EntryIterator extends PrivateEntryIterator<Map.Entry<K,V>> {
        EntryIterator(Entry<K,V> first) {
            super(first);
        }
        public Map.Entry<K,V> next() {
            return nextEntry();
        }
    }
複製代碼

EntrySet繼承了AbstractSet,其中iterator()方法返回了EntryIterator,它直接就繼承了PrivateEntryIterator接口。相似的迭代器還有:ValueIteratorKeyIteratorDescendingKeyIterator

TreeMap中的導航方法

TreeMap中有不少導航方法,好比:lowerEntrylowerKeytailMap等等,方法自己實現沒有什麼要說的。若是你仔細閱讀源碼,你會發現有下面這兩種方法(還有相似的):

public Map.Entry<K,V> lowerEntry(K key) {
        return exportEntry(getLowerEntry(key));
    }
    
final Entry<K,V> getLowerEntry(K key) {
    ...
    }
複製代碼

爲何給出兩個方法?明明getLowerEntry就能夠拿到Entry了。其實,lowerEntry纔是對外接口,而getLowerEntry是內部接口。由於getLowerEntry拿到的Entry是可讀寫的,而TreeMap不但願開發人員修改返回的Entry,因此多作了一層處理,讓返回的Entry只能讀。關鍵在exportEntry方法:

static <K,V> Map.Entry<K,V> exportEntry(TreeMap.Entry<K,V> e) {
        return (e == null) ? null :
            new AbstractMap.SimpleImmutableEntry<>(e);
    }
複製代碼

能夠看到,直接強轉成了SimpleImmutableEntry,它是AbstractMap實現的一個不可變Entry,它的setValue方法會拋出UnsupportedOperationException異常。

反向TreeMap

TreeMap由紅黑樹實現,它是有序的,因此它能夠反向:

private transient NavigableMap<K,V> descendingMap;

public NavigableMap<K, V> descendingMap() {
        NavigableMap<K, V> km = descendingMap;
        return (km != null) ? km :
            (descendingMap = new DescendingSubMap<>(this,
                                                    true, null, true,
                                                    true, null, true));
    }
複製代碼

那如何反向呢?

static final class DescendingSubMap<K,V> extends NavigableSubMap<K,V> {
        ...
        
        // 直接反轉比較器
        private final Comparator<? super K> reverseComparator =
            Collections.reverseOrder(m.comparator);

        ...
    }
複製代碼

DescendingSubMap中,能夠發現,所謂反向其實只須要反轉比較器就能夠了。
既然能夠反向,那TreeMap就能夠進行逆序遍歷和迭代。

總結

  1. TreeMap由紅黑樹實現,能夠正序也能夠逆序;
  2. TreeMap不是線程安全的key使用: Collections.synchronizedSortedMap(new TreeMap(...))
  3. TreeMap中Key不能爲null,Value能夠爲null;
  4. TreeMap中有豐富的導航方法。

參考資料

  1. TreeMap
  2. Java 集合系列12之 TreeMap詳細介紹(源碼解析)和使用示例
相關文章
相關標籤/搜索