JDK學習---深刻理解Comparator、TreeSet、TreeMap爲何能夠排序

  我原本打算仔細的去分析分析TreeSet和TreeMap排序規則,而且從底層實現和數據結構入手。當我去讀完底層源碼之後,我感受我就的目標定的太大了,單單就是數據結構就夠我本身寫好久了,所以我決定先易後難,先把底層源碼以及最直接的數據結構分析一下,至於底層的平衡二叉樹以及紅黑二叉樹,我就不過多去介紹,由於這是底層源碼優化用的,與直接實現代碼沒有多大關係,感興趣的也能夠去仔細研究。java

  樹: 樹是n ( n >=0)個節點的有限集。n = 0時稱爲空樹。在任意一顆非空樹種中: (1)有且僅有一個特定的稱爲根(Root)的節點;(2)當n > 1時,其他節點可分爲m(m > 0)個互不相交的有限集T一、T二、T3......Tm,其中集合自己又是一顆樹,而且稱爲根的子樹。以下圖:數組

   節點的子樹的根稱爲該節點的孩子,相應地,該節點稱爲孩子的雙親。爲何叫雙親,而不是父母呢?由於對於節點來講其父母同體,惟一的一個,因此只能把它稱爲雙親。同一個雙親的孩子之間稱爲兄弟。以下圖:數據結構

 

 

 

樹的其餘相關概念:app

   層:節點的層次是從根開始定義的,根稱爲第一層,根的孩子稱爲第二層。樹中節點的最大層次稱爲樹的高度或深度。以下圖:優化

    

  若是將樹中節點的各個子樹當作從左到右是有次序的,不能互換的,則稱該樹爲有序樹,不然稱爲無序樹。ui

  

 二叉樹this

   二叉樹二叉樹是n (n >= 0)個節點的有序集合,該集合或者爲空集(稱爲空二叉樹),或者由一個根節點和兩顆互不相交的、分別稱爲根節點的左子樹和右子樹的二叉樹組成。spa

   二叉樹的特色:
設計

    一、每一個節點最多有兩顆子樹,因此二叉樹中不存在度大於2的節點。注意不是隻有兩顆子樹,而是最多有。沒有子樹或者有一顆子樹都是能夠的。3d

    二、左子樹和右子樹是有順序的,次序不能任意顛倒。

    三、即便樹種某節點只有一顆子樹,也要區分它是左子樹仍是右子樹。由於左子樹和右子樹是徹底不一樣的概念,區別特別重要。

        二叉樹的形態:

     一、空二叉樹

     二、只有一個根節點

     三、根節點只有左子樹

     四、根節點只有右子樹

    五、根節點既有左子樹,又有右子樹。對應下面5附圖:

  

  

   二叉樹的存儲結構:

     一、二叉樹的順序存儲結構

     二、二叉樹的連式存儲結構(二叉鏈表)

    

     順序存儲結構:順序存儲結構就是用一維數組存儲二叉樹中的節點,而且節點的存儲,也就是數組的下標要能體現節點之間的邏輯關係,好比雙親與孩子的關係,左右兄弟的關係等。  

      存儲前:

 

    存儲後:

      

 

 

    二叉鏈表:二叉樹每一個節點最多有2個孩子,因此爲它設計一個數據域和兩個指針域。結構圖以下:

  二叉樹的遍歷:前序遍歷、中序遍歷、後序遍歷、層序遍歷。具體遍歷我就不累贅了。

      

二叉排序樹:  

    二叉排序樹:二叉排序樹,又稱爲二叉查找樹。它或者是一顆空樹,或者是具備下列性質的二叉樹。

        一、若它的左子樹不空,則左子樹上全部節點的值均小於它的根結構的值。

     二、若它的右子樹不空,則右子樹上全部節點的值均大於它的根節點的值。

       三、它的左、右子樹也分別爲二叉排序樹。

      

    典型案例就是數字遊戲:我在紙上寫好了一個100之內的正整數數字,你們來猜我寫的是哪個數字。注意,大家在纔對過程當中我只會回答「大了」 或 「 小了 」。

    其實,這是一個很典型的折半查找法,就是對二叉排序樹的典型應用。以下圖:

    

 

源碼解讀:

首先,咱們看看TreeMap中須要用到的二叉樹的類結構:

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; /** * Make a new cell with given key, value, and parent, and with * {@code null} child links, and BLACK color. */ Entry(K key, V value, Entry<K,V> parent) { this.key = key; this.value = value; this.parent = parent; }
//省略不少具體的方法

TreeMap成員變量和構造方法:

   //排序規則輔助類
    private final Comparator<? super K> comparator;
  //記錄根節點
private transient Entry<K,V> root = null; /** * The number of entries in the tree */ private transient int size = 0; /** * The number of structural modifications to the tree. */ private transient int modCount = 0; public TreeMap() { comparator = null; }   //本文重點分析的構造方法 public TreeMap(Comparator<? super K> comparator) { this.comparator = comparator; } public TreeMap(Map<? extends K, ? extends V> m) { comparator = null; putAll(m); } public TreeMap(SortedMap<K, ? extends V> m) { comparator = m.comparator(); try { buildFromSorted(m.size(), m.entrySet().iterator(), null, null); } catch (java.io.IOException cannotHappen) { } catch (ClassNotFoundException cannotHappen) { } }

構造方法比較多,我本篇穩重重點說排序功能,所以我就選  public TreeMap(Comparator<? super K> comparator) 方法進行突破。而Comparator就是JDK自帶的排序輔助類,這個咱們後面講。

分析put方法:public V put(K key, V value) {        Entry<K,V> t = root;

    
    //若是根節點爲null將傳入的鍵值對構形成根節點
if (t == null) { compare(key, key); // type (and possibly null) check        //根節點沒有父節點,因此傳入null 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...else很是重要,主要是定位具體的節點(這個節點是做爲父節點的,咱們將新傳入的key/value插入到這個具體的節點下)
    //有比較器的狀況
if (cpr != null) {
       //dowhile實如今root爲根節點移動尋找傳入鍵值對須要插入的位置
do {
          //記錄將要被插入新的鍵值對的節點 parent
= t;
          //比較器,按照自定義的規則返回結果 cmp
= cpr.compare(key, t.key);
          //插入的key較大
if (cmp < 0) t = t.left;
         //插入的key較小
else if (cmp > 0) t = t.right;
          //若是key相等,則直接替換value
else return t.setValue(value); } while (t != null); }
     //沒有傳入比較器
else { if (key == null) throw new NullPointerException(); Comparable<? super K> k = (Comparable<? super K>) key;
        //與上方的do..while同樣,知識比較的規則不一樣
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); }
     //沒有找到相同的key,纔會有此如下的方法操做。不然直接在上方就直接t.setValue(value)了
     //根據key、value以及雙親節點,建立一個新的節點 Entry
<K,V> e = new Entry<>(key, value, parent);
     //若是最後一次判斷的結果,確認新節點是父節點的左孩子,仍是右孩子;爲何說是最後一次判斷的結果呢?由於上面的if...else...中都有while方法,而這個while就是爲了找這個最後的一次比較的結果
if (cmp < 0) parent.left = e; else parent.right = e;

     //此方法我就不介紹了,涉及到紅黑二叉樹以及二叉樹的搖擺,對二叉樹進行優化操做 fixAfterInsertion(e); size
++; modCount++; return null; }

 至此,咱們發現,二叉樹的插入式根據cmp的值進行操做的,小於0就放在左子樹,大於0就放在右子樹。這不就是典型的二叉排序樹啊?還記得以前說的猜數字遊戲麼?

由此可知:

  一、TreeMap底層的二叉樹是按照二叉排序樹的結構進行存儲的,左側小於根節點,右側大於根節點

  二、至因而大於父節點,仍是小於父節點,那就是咱們本身定義的Comparator比較器的事情了。正常的狀況下,咱們知道1小於2;可是若是是自定義比較器,那麼咱們徹底能夠自定義1大於2;這種狀況下也就出現了所謂的升序和降序了。

 

說了這麼多,也許好多人還不是很明白。那麼接下來,我就舉幾個例子進行說明吧:

案例1:根據key的長度升序

    public <T> void test1()
    {
        Map<String, String> map = new TreeMap<String, String>(
                new Comparator<String>()
                {
                    public int compare(String o1, String o2)
                    {
                        return o1.length() - o2.length();
                    }
                });

        map.put("hello", "我是hello");
        map.put("jk", "咱們認識嗎?");
        map.put("oooooo", "我要去香山看紅葉");

        Set<Entry<String, String>> set = map.entrySet();
        System.out.println("-----------------test1 : ");
        for (Iterator iter = set.iterator(); iter.hasNext();)
        {
            Entry<String, String> entry = (Entry<String, String>) iter.next();
            System.out.println(entry.getKey() + " : " + entry.getValue());
        }
    }

這個案例是升序,由於TreeMap調用compare(T o1, T o2)傳入的是能夠的值,所以,此處o1是新插入的key,而o2則是咱們源碼提到的do...while...中說道的找到的最後一次排序的key。而若是咱們想降序,只要將compare(T o1, T o2)實現方法中的 return o1.length() - o2.length();改爲 return o2.length() - o1.length();便可。容許結果以下圖:

-----------------test1 : 
jk : 咱們認識嗎?
hello : 我是hello
oooooo : 我要去香山看紅葉

 

那麼假如咱們按照value進行排序,那又該怎麼辦呢?咱們看過底層的源碼實現,TreeMap沒有提供說put的時候,能夠進行對value的操做,所以要想直接經過TreeMap對value的值進行排序,那是不現實的。那若是咱們的業務非要對value進行排序又該怎麼辦呢?以下:

//根據value排序
    public void sortByValue()
    {
        Map<String, String> map = new HashMap<String, String>();
        map.put("a3", "dddd");
        map.put("d", "aaaa");
        map.put("b435", "cccc");
        map.put("c6323", "bbbb");

        List<Entry<String, String>> list = new ArrayList<Entry<String, String>>(
                map.entrySet());

        Collections.sort(list, new Comparator<Map.Entry<String, String>>()
        {
            // 升序排序
            public int compare(Entry<String, String> o1,Entry<String, String> o2)
            {
                return o1.getValue().compareTo(o2.getValue());
            }
        });

        System.out.println("sortByValue =" + list);
    }

看了這個實現,其實咱們並無對HashMap進行排序,而是在遍歷的時候對存放二叉樹Entry的list進行排序的,運行結果以下:

sortByValue =[d=aaaa, c6323=bbbb, b435=cccc, a3=dddd]

 

 

TreeSet源碼:

TreeSet的構造方法:  

 TreeSet(NavigableMap<E,Object> m) {
        this.m = m;
    }

    public TreeSet() {
        this(new TreeMap<E,Object>());
    }

    public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }

    public TreeSet(Collection<? extends E> c) {
        this();
        addAll(c);
    }

    public TreeSet(SortedSet<E> s) {
        this(s.comparator());
        addAll(s);
    }

接下來,我將會圍繞 public TreeSet(Comparator<? super E> comparator) 進行拓展:

 public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }

而TreeMap上面已經分析過了,咱們知道TreeMap默認的是對key進行排序的,而TreeMap的構造方法竟然在構建一個TreeMap方法,接下來接續分析

add方法:

 public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }

remove方法:

 public boolean remove(Object o) {
        return m.remove(o)==PRESENT;
    }

first方法、last方法:

 public E first() {
        return m.firstKey();
    }

    /**
     * @throws NoSuchElementException {@inheritDoc}
     */
    public E last() {
        return m.lastKey();
    }

iterator方法:

  public Iterator<E> iterator() {
        return m.navigableKeySet().iterator();
    }

看完實現方法,所有是對m進行操做,而這個m是什麼呢?就是咱們以前的TreeMap。TreeMap已經分析過了,而TreeSet只是在調用TreeMap而已,所以廢話就很少說了。

 

總結:

 一、TreeMap只能經過對key進行排序操做,沒法直接對value進行排序操做;而TreeSet的底層實現則是TreeMap,所以TreeSet也value也就是TreeMap的key,所以TreeSet是能夠對value進行各類排序的;

 二、Comparator根本不能排序,它只是自定義的一種規則;而這個規則,TreeMap已經在底層對它進行封裝和調用了;

 三、若是咱們想要對TreeMap的value進行操做的話,能夠藉助集合輔助類Collections進行操做

相關文章
相關標籤/搜索