Java基礎系列:瞭解TreeMap

來,進來的小夥伴們,咱們認識一下。html

我是俗世遊子,在外流浪多年的Java程序猿java

前面咱們已經介紹了HashMap,今天咱們來看看Map的另一個子類:TreeMapapi

前置知識

首先在介紹TreeMap以前,咱們先了解一些前置知識,往下看安全

排序方式

在瞭解排序方式以前,咱們先來聊一聊什麼是:有序,無序,排序數據結構

有序oracle

保證插入的順序和在容器中存儲的順序是一致的,典型表明:ide

  • List

無序源碼分析

插入的順序和在容器中存儲的順序不一致的,典型表明:性能

  • Set
  • Map

排序優化

基於某種規則在迭代的時候輸出符合規則的元素順序, 好比:

  • TreeMap
  • TreeSet

那麼咱們來看具體的排序方式

那麼,如今有一種需求,就是咱們按照必定順序將集合中的元素進行輸出,那麼咱們該怎麼作呢?基於這種方式,Java爲咱們提供了兩種實現方式:

Comparable

實現該接口須要一個實體對象,而後重寫其compareTo(),咱們來看例子:

// 定義一個Student對象
class Student implements Comparable<Student> {

    public int id;
    public String name;
    public int age;

    public Student(int id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    /**
     * 對比方法
     */
    @Override
    public int compareTo(Student o) {
        // 按照年齡從小到大的排序方式
        return age - o.age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

// 小案例
ArrayList<Student> students = new ArrayList<Student>(6) {{
    add(new Student(1, "張三", 20));
    add(new Student(2, "里斯", 18));
    add(new Student(3, "王五", 38));
    add(new Student(4, "趙柳", 10));
    add(new Student(5, "天氣", 77));
}};

// 排序前的輸出
System.out.println("排序前的輸出:");
System.out.println(students);

System.out.println("================");
// 排序操做
Collections.sort(students);
System.out.println("排序後的輸出:");
System.out.println(students);

/**
排序前的輸出
[Student{id=1, name='張三', age=20}, Student{id=2, name='里斯', age=18}, Student{id=3, name='王五', age=38}, Student{id=4, name='趙柳', age=10}, Student{id=5, name='天氣', age=77}]
================
[Student{id=4, name='趙柳', age=10}, Student{id=2, name='里斯', age=18}, Student{id=1, name='張三', age=20}, Student{id=3, name='王五', age=38}, Student{id=5, name='天氣', age=77}]
**/

能夠看到咱們已經實現了按照年齡從小到大的順序進行排序的

這裏須要注意一點,在compareTo()中,是傳入的對象和當前對象進行對比:

  • 若是對比大於0,說明按照降序排序
  • 若是對比小於0,說明按照升序排序
  • 若是對比等於0,當前不變

Comparator

這種方式是經過外部類的方式進行編寫,仍是上面的代碼,咱們改一些地方:

Collections.sort(students, new Comparator<Student>() {
    @Override
    public int compare(Student o1, Student o2) {
        // 按照ID降序排序
        return (int) (o2.id - o1.id);
    }
});

/**
排序前的輸出:
[Student{id=1, name='張三', age=20}, Student{id=2, name='里斯', age=20}, Student{id=3, name='王五', age=38}, Student{id=4, name='趙柳', age=10}, Student{id=5, name='天氣', age=77}]
================
排序後的輸出:
[Student{id=5, name='天氣', age=77}, Student{id=4, name='趙柳', age=10}, Student{id=3, name='王五', age=38}, Student{id=2, name='里斯', age=20}, Student{id=1, name='張三', age=20}]
**/

查看,已經實現了需求

compare()中返回值的對比和第一種方式是同樣的

你們按照實際的需求選擇合理的排序方式吧

樹介紹

是一種數據結構,它是由n(n>=1)個有限結點組成一個具備層次關係的集合。把它叫作「樹」是由於它看起來像一棵倒掛的樹,也就是說它是根朝上,而葉朝下的。它具備如下的特色:

  • 每一個結點有零個或多個子結點;
  • 沒有父結點的結點稱爲根結點;
  • 每個非根結點有且只有一個父結點;
  • 除了根結點外,每一個子結點能夠分爲多個不相交的子樹

摘抄自:百度百科:Tree

二叉樹

二叉樹是樹形結構中的一種重要類型,是咱們在數據結構中最經常使用的樹結構之一,每一個節點下最多隻有兩個子節點

二叉樹結構

二叉搜索樹

顧名思義,二叉搜索樹是以二叉樹來組織的,對比二叉樹,擁有如下特性:

  • 每一個節點下最多隻擁有兩個節點
  • 採用左小右大的規則插入元素節點,若是相等,那麼插入到右邊
  • 因此在遍歷節點的時候能夠採用 二分法 來減小元素的查詢

二叉樹插入過程

二叉搜索樹的插入過程

平衡樹

也成AVL樹,是基於二叉搜索樹的一種擴展,也就是說擁有二叉搜索樹的所有特性。二叉搜索樹存在缺點:

  • 數據插入方式,容易形成兩邊節點,一邊長一邊短的問題,這樣在經過 二分法來遍歷元素的時候也存在性能問題

AVL樹針對這一狀況進行了改進:

  • AVL樹會對不平衡的樹進行一個旋轉,優化整個數據結構,保證整個樹的平衡,保證整個二分查找的效率
  • 旋轉規則:每一個節點的左右子節點的高度之差的絕對值最多爲1, 即平衡因子爲範圍[-1,1]

AVL樹插入過程

紅黑樹

基於平衡樹的一種演進,也存在旋轉操做保持二叉樹的平衡,同時在此基礎上添加變色操做,擁有以下特性:

  • 節點是紅色或者黑色
  • 根節點是黑色,每一個葉子節點(NUIL節點)是黑色的
  • 若是一個節點是紅色的,那麼其子節點就是黑色的(也就是說不能存在連續的紅色節點)
  • 從任意節點到其每一個葉子的全部路徑都包含相同數目的黑色節點
  • 最長路徑不超過最短路徑的2倍

紅黑樹插入過程

這裏沒有講解的很詳細,簡單的說一下有個概念

源碼分析TreeMap

下面咱們來看一下TreeMap

TreeMap結構圖

以前我說的有些問題:咱們遇到一個類,它最重要的是類中的註釋,咱們先來看TreeMap的註釋是如何介紹TreeMap的:

  • 基於紅黑樹方式實現的Map
  • 按照天然排序或者是指定的方式排序,這取決於咱們所使用的的構造方法,因此說,TreeMap是有序的,咱們能夠指定其排序方式
  • TreeMap是線程不安全的,若是想要實現,須要對TreeMap進行包裝
SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...));

構造方法

下面咱們來具體看看咱們如何使用TreeMap

TreeMap<String, String> treeMap = new TreeMap<>();
new TreeMap<String, Long>(new Comparator<String>() {
    @Override
    public int compare(String o1, String o2) {
        return 0;
    }
});
public TreeMap() {
    comparator = null;
}

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

第二個構造方法傳入一個比較器,這個咱們在 排序方式 中就已經說到,也明白了返回的含義

可是這裏要注意一點:若是咱們沒有傳入 比較器,默認爲null,那麼咱們須要明白:

  • 傳入的Key必需要實現Comparable接口的類型,這一點咱們在put()方法中會跟源碼說明

put()

在瞭解該方法以前,咱們先來了解一個類:

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

咱們已經知道,TreeMap底層是採用紅黑樹的結構來存儲數據,那麼對應到代碼中的實現就是上面的樣子。

下面咱們來看具體是如何添加元素的

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

總結一下,能夠分爲四步來進行操做:

  • 判斷若是當前根節點爲null,那麼當前插入的第一個元素就爲根節點元素

  • 若是存在根節點,那麼再添加元素的時候根據排序器進行對比,驗證當前元素應該在左側仍是在右側,若是對比爲0,那麼說明當前元素存在於TreeMap中,直接將其進行覆蓋。這裏也就說明了一個問題:在TreeMap中,不會存在重複元素
  • 找到本身所對應的位置,而後進行指針引用
  • 節點變色操做和旋轉操做

前面3點都很簡單,無非就是經過do..while循環經過排序器進行對比,這裏有一點,也就是在構造方法裏我提到的一點:

class A {
    public Long id;
}

TreeMap<A, String> map = new TreeMap<>();
map.put(new A(), "11");
map.put(new A(), "11");
System.out.println(map);

// java.lang.ClassCastException: zopx.top.study.jav.maps.A cannot be cast to java.lang.Comparable

在採用默認構造方法的時候,這樣的方式出現錯誤:類型轉換異常,這也就是爲何TreeMap:Key必需要實現Comparable的緣由

下來咱們重點看看節點變色和旋轉操做

private void fixAfterInsertion(Entry<K,V> x) {
    // 將當前節點標記爲紅色
    x.color = RED;

    // 當插入元素後出現不平衡,則進行調整
    while (x != null && x != root && x.parent.color == RED) {
        // 判斷是不是左邊節點
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateLeft(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateRight(parentOf(parentOf(x)));
            }
        } else {
            // 不然就是右邊節點
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                if (x == leftOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateRight(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateLeft(parentOf(parentOf(x)));
            }
        }
    }
    // 根節點永遠是黑色
    root.color = BLACK;
}

下面我經過畫圖來進行代碼分析吧,這樣更容易理解:

TreeMap變色過程

這是一個最簡單的例子,節點也很是少,你們能夠本身按照上面的方式過一下代碼,理解下代碼的邏輯

右旋操做

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

一樣,在紅黑樹中還包含左旋的操做,你們能夠本身看下源代碼: rotateLeft(),和右旋很相似

最好是可以邊分析源代碼,邊經過畫圖的方式加深理解

上面也就是TreeMap基於紅黑樹的實現方式,你們能夠結合上面介紹的紅黑樹的特性好好理解下

remove(Object key) 方法和put()方法相差很少,你們能夠本身看看源碼

get()

下面簡單來講一下get()方法:

Entry<K,V> p = getEntry(key);

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

    // 默認構造器
    @SuppressWarnings("unchecked")
    Comparable<? super K> k = (Comparable<? super K>) key;
    Entry<K,V> p = root;
    while (p != null) {
        // 不斷對比,若是==0,那麼就是當前須要的Entry
        int cmp = k.compareTo(p.key);
        if (cmp < 0)
            p = p.left;
        else if (cmp > 0)
            p = p.right;
        else
            return p;
    }
    return null;
}

// 自定義排序方式
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) {
            // 不斷對比,若是==0,那麼就是當前須要的Entry
            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;
}

該方法仍是比較簡單的,也就是在while()循環中經過比較器進行對比

TreeMap更多api方法

更多關於TreeMap使用方法推薦查看其文檔:

TreeMap API文檔

數據結構可視化網站

相關文章
相關標籤/搜索