Java集合之TreeMap

Map的單元是對鍵值對的處理,以前分析過的兩種Map,HashMap和LinkedHashMap都是用哈希值去尋找咱們想要的鍵值對,優勢是由O(1)的查找速度。java

那若是咱們在一個對查找性能要求不那麼高,反而對有序性要求比較高的應用場景呢?node

這個時候HashMap就再也不適用了,咱們須要一種新的Map,在JDK中提供了一個接口:SortedMap,我想分析一下具體的實現中的一種:TreeMap.算法

HahMap是Key無序的,而TreeMap是Key有序的。編程

1.看一下基本成員:數據結構

public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
    private final Comparator<? super K> comparator;
    private transient Entry<K,V> root = null;
    private transient int size = 0;
    private transient int modCount = 0;
    public TreeMap() {
        comparator = null;
    }    
    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }
    //後面省略
}

TreeMap繼承了NavigableMap,而NavigableMap繼承自SortedMap,爲SortedMap添加了搜索選項,NavigableMap有幾種方法,分別是不一樣的比較要求:floorKey是小於等於,ceilingKey是大於等於,lowerKey是小於,higherKey是大於。ide

注意初始化的時候,有一個Comparator成員,這是用於維持有序的比較器,當咱們想作一個自定義數據結構的TreeMap時,能夠重寫這個比較器性能

2.咱們看一下Entry的成員:this

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;
    //後續省略
}

咦?木有了熟悉了哈希值,多了left,right,parent,這是咱們的樹結構,最後看到color,明白了:TreeMap是基於紅黑樹實現的!並且默認的節點顏色是黑色。spa

至於紅黑樹,想必多多少少都聽過,這是一種平衡的二叉查找樹,是2-3樹的一種變體,即擁有二叉查找樹的高效查找,擁有2-3樹的高效平衡插入能力。指針

紅黑樹巧妙的增長了顏色這個維度,對2-3樹的樹自己進行了降維成了二叉樹,這樣樹的調整不會再如2-3樹那麼繁瑣。

有的同窗看到這裏會質疑我,你這個胡說八道,和算法導論裏講的不同!

對,CLRS中確實沒有這段,這段選自《Algorithms》,我以爲提供了一種有趣的理解思路,因此若是以前只看了CLRS,建議去看一下這本書,互相驗證。

不過爲了尊重JDK的做者,後面的仍是按照CLRS中的講解來吧,畢竟在JDK源碼的註釋中寫着:From CLR。

咱們在紅黑樹中的一切插入和刪除後,爲了維護樹的有序性的動做看起來繁複,但都是爲了維護下面幾個紅黑樹的基本性質

(1)樹的節點只有紅與黑兩種顏色
(2)根節點爲黑色的
(3)葉子節點爲黑色的
(4)紅色節點的字節點一定是黑色的
(5)從任意一節點出發,到其後繼的葉子節點的路徑中,黑色節點的數目相同

紅黑樹的第4條性質保證了這些路徑中的任意一條都不存在連續的紅節點,而紅黑樹的第5條性質又保證了全部的這些路徑上的黑色節點的數目相同。於是最短路徑一定是隻包含黑色節點的路徑,而最長路徑爲紅黑節點互相交叉的路徑,因爲全部的路徑的起點必須是黑色的,而紅色節點又不能連續存在,於是最長路徑的長度爲全爲黑色節點路徑長度的二倍。

回到TreeMap自己,看看它的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();
        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;
}
View Code

此處就是二叉樹的比較查找到合適的位置,而後插入,須要注意的是

(1)先檢測root節點是否是null,若是爲null,則新插入的節點爲root節點。

(2)最好自定義本身的Comparator,不然將會繼承原始的比較方法,可能會出現問題

(3)插入的鍵值不能爲null,不然會拋出空指針的異常。

(4)插入新節點後,調用fixAfterInsertion(e)方法來修復紅黑樹。

看一下get方法,這裏會調用getEntry方法,就是二叉查找樹的查找:

final Entry<K,V> getEntry(Object key) {
    // Offload comparator-based version for sake of performance
    if (comparator != null)
        return getEntryUsingComparator(key);
    if (key == null)
        throw new NullPointerException();
    Comparable<? super K> k = (Comparable<? super K>) key;
    Entry<K,V> 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;
}
View Code

還有一個remove方法,這裏最後調用的是deleteEntry()方法,在deleteEntry()方法中最後調用fixAfterDeletion方法來修復樹的順序。

紅黑樹的刪除操做複雜的讓人髮指,對着CLRS慢慢看吧:

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--;

    // If strictly internal, copy successor's element to p and then make p
    // point to successor.
    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) {
        // Link replacement to parent
        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;

        // Fix replacement
        if (p.color == BLACK)
            fixAfterDeletion(replacement);
    } else if (p.parent == null) { // return if we are the only node.
        root = null;
    } else { //  No children. Use self as phantom replacement and unlink.
        if (p.color == BLACK)
            fixAfterDeletion(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;
        }
    }
}
View Code

上面所作的一切繁瑣操做都是爲了紅黑樹的基本性質,而修復順序的操做中最基本的就是左旋和右旋了,下面是左旋和右選的源碼。

/** From CLR */
private void rotateLeft(Entry<K,V> p) {
    if (p != null) {
        Entry<K,V> r = p.right;
        p.right = r.left;
        if (r.left != null)
            r.left.parent = p;
        r.parent = p.parent;
        if (p.parent == null)
            root = r;
        else if (p.parent.left == p)
            p.parent.left = r;
        else
            p.parent.right = r;
        r.left = p;
        p.parent = r;
    }
}

/** From CLR */
private void rotateRight(Entry<K,V> p) {
    if (p != null) {
        Entry<K,V> l = p.left;
        p.left = l.right;
        if (l.right != null) l.right.parent = p;
        l.parent = p.parent;
        if (p.parent == null)
            root = l;
        else if (p.parent.right == p)
            p.parent.right = l;
        else p.parent.left = l;
        l.right = p;
        p.parent = l;
    }
}

其實全部的操做都是關於紅黑樹的操做,

決定了TreeMap的有序性,對於TreeMap的增刪改查的效率都是O(Log(n))的。

 到這裏,TreeMap其實就差很少了,最關鍵的仍是對紅黑樹的操做,但願這種數據結構的知識能掌握的比較紮實吧,多看書,多編程,夯實基礎,與諸君共勉。

相關文章
相關標籤/搜索