Java程序員值得擁有的TreeMap指南

先看再點贊,給本身一點思考的時間,微信搜索【 沉默王二】關注這個有顏值卻僞裝靠才華苟且的程序員。
本文 GitHub github.com/itwanger 已收錄,裏面還有我精心爲你準備的一線大廠面試題。

吃飯間隙,迷上了《吐槽大會》,一集一集地刷啊,以爲這些嘉賓真的挺有勇氣的,勇於直面本身的慘淡槽點。因而,同窗們看到了,我做爲一個技術博主,也受到了「傳染」,不,受到了「薰陶」,原本這篇文章標題就想叫《TreeMap 指南》,是否是有點平淡無奇,沒有槽點?因而我就想,不妨蹭點吐槽大會的熱度吧,雖然吐槽大會如今也沒什麼熱度了哈。java

TreeMap,雖然也是個 Map,但存在感過低了。我作程序員這十多年裏,HashMap 用了超過十年,TreeMap 只用了多字裏那麼一小會兒一小會兒,真的是,太慘了。git

雖然 TreeMap 用得少,但仍是有用處的。程序員

以前 LinkedHashMap 那篇文章裏提到過了,HashMap 是無序的,全部有了 LinkedHashMap,加上了雙向鏈表後,就能夠保持元素的插入順序和訪問順序,那 TreeMap 呢?github

TreeMap 由紅黑樹實現,能夠保持元素的天然順序,或者實現了 Comparator 接口的自定義順序。面試

可能有些同窗不知道紅黑樹,理解起來 TreeMap 就有點難度,那我先來普及一下:數組

紅黑樹(英語:Red–black tree)是一種自平衡的二叉查找樹(Binary Search Tree),結構複雜,但卻有着良好的性能,完成查找、插入和刪除的時間複雜度均爲 log(n)。

二叉查找樹又是什麼呢?微信

圖片來源於網絡,侵刪

上圖中這棵樹,就是一顆典型的二叉查找樹:網絡

1)左子樹上全部節點的值均小於或等於它的根結點的值。性能

2)右子樹上全部節點的值均大於或等於它的根結點的值。學習

3)左、右子樹也分別爲二叉排序樹。

理解二叉查找樹了吧?不過,二叉查找樹有一個不足,就是容易變成瘸子,就是一側多,一側少,就像下圖這樣:

圖片來源於網絡,侵刪

查找的效率就要從 log(n) 變成 o(n) 了,對吧?必需要平衡一下,對吧?因而就有了平衡二叉樹,左右兩個子樹的高度差的絕對值不超過 1,就像下圖這樣:

圖片來源於網絡,侵刪

紅黑樹,顧名思義,就是節點是紅色或者黑色的平衡二叉樹,它經過顏色的約束來維持着二叉樹的平衡:

1)每一個節點都只能是紅色或者黑色

2)根節點是黑色

3)每一個葉節點(NIL 節點,空節點)是黑色的。

4)若是一個節點是紅色的,則它兩個子節點都是黑色的。也就是說在一條路徑上不能出現相鄰的兩個紅色節點。

5)從任一節點到其每一個葉子的全部路徑都包含相同數目的黑色節點。

圖片來源於網絡,侵刪

那,關於紅黑樹,同窗們就先了解到這,腦子裏有個大概的印象,知道 TreeMap 是個什麼玩意。

0一、天然順序

默認狀況下,TreeMap 是根據 key 的天然順序排列的。好比說整數,就是升序,一、二、三、四、5。

TreeMap<Integer,String> mapInt = new TreeMap<>();
mapInt.put(3, "沉默王二");
mapInt.put(2, "沉默王二");
mapInt.put(1, "沉默王二");
mapInt.put(5, "沉默王二");
mapInt.put(4, "沉默王二");

System.out.println(mapInt);

輸出結果以下所示:

{1=沉默王二, 2=沉默王二, 3=沉默王二, 4=沉默王二, 5=沉默王二}

TreeMap 是怎麼作到的呢?想一探究竟,就得上源碼了,來看 TreeMap 的 put() 方法(省去了一部分,版本爲 JDK 14):

public V put(K key, V value) {
    TreeMap.Entry<K,V> t = root;
    int cmp;
    TreeMap.Entry<K,V> parent;
    // split comparator and comparable paths
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
    }
    else {
        @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);
    }
    return null;
}

注意 cmp = k.compareTo(t.key) 這行代碼,就是用來進行 key 的比較的,因爲此時 key 是 int,因此就會調用 Integer 類的 compareTo() 方法進行比較。

public int compareTo(Integer anotherInteger) {
    return compare(this.value, anotherInteger.value);
}

public static int compare(int x, int y) {
    return (x < y) ? -1 : ((x == y) ? 0 : 1);
}

那相應的,若是 key 是字符串的話,也就會調用 String 類的 compareTo() 方法進行比較。

public int compareTo(String anotherString) {
    byte v1[] = value;
    byte v2[] = anotherString.value;
    byte coder = coder();
    if (coder == anotherString.coder()) {
        return coder == LATIN1 ? StringLatin1.compareTo(v1, v2)
                : StringUTF16.compareTo(v1, v2);
    }
    return coder == LATIN1 ? StringLatin1.compareToUTF16(v1, v2)
            : StringUTF16.compareToLatin1(v1, v2);
}

因爲內部是由字符串的字節數組的字符進行比較的,是否是聽起來很繞?對,就是很繞,因此使用中文字符串做爲 key 的話,看不出來效果。

TreeMap<String,String> mapString = new TreeMap<>();
mapString.put("c", "沉默王二");
mapString.put("b", "沉默王二");
mapString.put("a", "沉默王二");
mapString.put("e", "沉默王二");
mapString.put("d", "沉默王二");

System.out.println(mapString);

輸出結果以下所示:

{a=沉默王二, b=沉默王二, c=沉默王二, d=沉默王二, e=沉默王二}

字母的升序,對吧?

0二、自定義排序

若是天然順序不知足,那就能夠在聲明 TreeMap 對象的時候指定排序規則。

TreeMap<Integer,String> mapIntReverse = new TreeMap<>(Comparator.reverseOrder());
mapIntReverse.put(3, "沉默王二");
mapIntReverse.put(2, "沉默王二");
mapIntReverse.put(1, "沉默王二");
mapIntReverse.put(5, "沉默王二");
mapIntReverse.put(4, "沉默王二");

System.out.println(mapIntReverse);

TreeMap 提供了能夠指定排序規則的構造方法:

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

Comparator.reverseOrder() 返回的是 ReverseComparator 對象,就是用來反轉順序的,很是方便。

因此,輸出結果以下所示:

{5=沉默王二, 4=沉默王二, 3=沉默王二, 2=沉默王二, 1=沉默王二}

HashMap 是無序的,插入的順序隨着元素的增長會不停地變更。但 TreeMap 可以至始至終按照指定的順序排列,這對於須要自定義排序的場景,實在是太有用了!

0三、排序的好處

既然 TreeMap 的元素是通過排序的,那找出最大的那個,最小的那個,或者找出全部大於或者小於某個值的鍵來講,就方便多了。

Integer highestKey = mapInt.lastKey();
Integer lowestKey = mapInt.firstKey();
Set<Integer> keysLessThan3 = mapInt.headMap(3).keySet();
Set<Integer> keysGreaterThanEqTo3 = mapInt.tailMap(3).keySet();

System.out.println(highestKey);
System.out.println(lowestKey);

System.out.println(keysLessThan3);
System.out.println(keysGreaterThanEqTo3);

TreeMap 考慮得很周全,剛好就提供了 lastKey()firstKey() 這樣獲取最後一個 key 和第一個 key 的方法。

headMap() 獲取的是到指定 key 以前的 key;tailMap() 獲取的是指定 key 以後的 key(包括指定 key)。

來看一下輸出結果:

5
1
[1, 2]
[3, 4, 5]

0四、如何選擇 Map

在學習 TreeMap 以前,咱們已經學習了 HashMapLinkedHashMap ,那如何從它們三個中間選擇呢?

HashMap、LinkedHashMap、TreeMap 都實現了 Map 接口,並提供了幾乎相同的功能(增刪改查)。它們之間最大的區別就在於元素的順序:

HashMap 徹底不保證元素的順序,添加了新的元素,以前的順序可能徹底逆轉。

LinkedHashMap 默認會保持元素的插入順序。

TreeMap 默認會保持 key 的天然順序(根據 compareTo() 方法)。

來個表格吧,一目瞭然。

謝謝你們,下期見,同窗們。


我是沉默王二,一枚有顏值卻僞裝靠才華苟且的程序員。關注便可提高學習效率,別忘了三連啊,點贊、收藏、留言,我不挑,奧利給🌹

注:若是文章有任何問題,歡迎絕不留情地指正。

若是你以爲文章對你有些幫助,歡迎微信搜索「沉默王二」第一時間閱讀;本文 GitHub github.com/itwanger 已收錄,歡迎 star。

相關文章
相關標籤/搜索