Java集合--TreeMap完全解析

https://www.jianshu.com/p/2dcff3634326

4 TreeMap

上一篇,介紹了集合框架中的HashMap對象,主要講述了HashMap的底層實現和基本操作。本篇,讓我們繼續來學習Map集合,今天的主角是TreeMap。

相比於HashMap來說,TreeMap理解起來更爲複雜,你做好準備了嗎?

4.1 TreeMap

在Map集合框架中,除了HashMap以外,TreeMap也是我們工作中常用到的集合對象之一。

與HashMap相比,TreeMap是一個能比較元素大小的Map集合,會對傳入的key進行了大小排序。其中,可以使用元素的自然順序,也可以使用集合中自定義的比較器來進行排序;

不同於HashMap的哈希映射,TreeMap底層實現了樹形結構,至於具體形態,你可以簡單的理解爲一顆倒過來的樹---根在上--葉在下。如果用計算機術語來說的話,TreeMap實現了紅黑樹的結構,形成了一顆二叉樹。

至於什麼是二叉樹,什麼是紅黑樹,我們後面再談,你現在只需要記住它是一顆倒過來的樹,就OK了。

TreeMap繼承於AbstractMap,實現了Map, Cloneable, NavigableMap, Serializable接口。

(1)TreeMap 繼承於AbstractMap,而AbstractMap實現了Map接口,並實現了Map接口中定義的方法,減少了其子類繼承的複雜度;

(2)TreeMap 實現了Map接口,成爲Map框架中的一員,可以包含着key--value形式的元素;

(3)TreeMap 實現了NavigableMap接口,意味着擁有了更強的元素搜索能力;

(4)TreeMap 實現了Cloneable接口,實現了clone()方法,可以被克隆;

(5)TreeMap 實現了Java.io.Serializable接口,支持序列化操作,可通過Hessian協議進行傳輸;

對於Cloneable, Serializable來說,我們再熟悉不過,基本上Java集合框架中的每一個類都會實現這2個接口,而NavigableMap接口是幹什麼的,它定義了什麼樣的功能?接下來,我們就通過NavigableMap的源碼來看下!

根據上面的截圖,我們首先介紹下NavigableMap體系中的SortedMap接口:

對於SortedMap來說,該類是TreeMap體系中的父接口,也是區別於HashMap體系最關鍵的一個接口。

主要原因就是SortedMap接口中定義的第一個方法---Comparator<? super K> comparator();

該方法決定了TreeMap體系的走向,有了比較器,就可以對插入的元素進行排序了;

public interface SortedMap<K,V> extends Map<K,V> {
    
    //返回元素比較器。如果是自然順序,則返回null;
    Comparator<? super K> comparator();
    
    //返回從fromKey到toKey的集合:含頭不含尾
    java.util.SortedMap<K,V> subMap(K fromKey, K toKey);

    //返回從頭到toKey的集合:不包含toKey
    java.util.SortedMap<K,V> headMap(K toKey);

    //返回從fromKey到結尾的集合:包含fromKey
    java.util.SortedMap<K,V> tailMap(K fromKey);
    
    //返回集合中的第一個元素:
    K firstKey();
   
    //返回集合中的最後一個元素:
    K lastKey();
    
    //返回集合中所有key的集合:
    Set<K> keySet();
    
    //返回集合中所有value的集合:
    Collection<V> values();
    
    //返回集合中的元素映射:
    Set<Map.Entry<K, V>> entrySet();
}

上面介紹了SortedMap接口,而NavigableMap接口又是對SortedMap進一步的擴展:主要增加了對集合內元素的搜索獲取操作,例如:返回集合中某一區間內的元素、返回小於大於某一值的元素等類似操作。

public interface NavigableMap<K,V> extends SortedMap<K,V> {

    //返回小於key的第一個元素:
    Map.Entry<K,V> lowerEntry(K key);

    //返回小於key的第一個鍵:
    K lowerKey(K key);

    //返回小於等於key的第一個元素:
    Map.Entry<K,V> floorEntry(K key);

    //返回小於等於key的第一個鍵:
    K floorKey(K key);

    //返回大於或者等於key的第一個元素:
    Map.Entry<K,V> ceilingEntry(K key);

    //返回大於或者等於key的第一個鍵:
    K ceilingKey(K key);

    //返回大於key的第一個元素:
    Map.Entry<K,V> higherEntry(K key);

    //返回大於key的第一個鍵:
    K higherKey(K key);

    //返回集合中第一個元素:
    Map.Entry<K,V> firstEntry();

    //返回集合中最後一個元素:
    Map.Entry<K,V> lastEntry();

    //返回集合中第一個元素,並從集合中刪除:
    Map.Entry<K,V> pollFirstEntry();

    //返回集合中最後一個元素,並從集合中刪除:
    Map.Entry<K,V> pollLastEntry();

    //返回倒序的Map集合:
    java.util.NavigableMap<K,V> descendingMap();

    NavigableSet<K> navigableKeySet();

    //返回Map集合中倒序的Key組成的Set集合:
    NavigableSet<K> descendingKeySet();

    java.util.NavigableMap<K,V> subMap(K fromKey, boolean fromInclusive,
                                       K toKey, boolean toInclusive);

    java.util.NavigableMap<K,V> headMap(K toKey, boolean inclusive);

    java.util.NavigableMap<K,V> tailMap(K fromKey, boolean inclusive);

    SortedMap<K,V> subMap(K fromKey, K toKey);

    SortedMap<K,V> headMap(K toKey);

    SortedMap<K,V> tailMap(K fromKey);
}

其實,NavigableMap的目的很簡單、很直接,就是增強了對集合內元素的搜索、獲取的功能,當子類TreeMap實現時,自然獲取以上的功能;

TreeMap具有如下特點:

  • 不允許出現重複的key;

  • 可以插入null鍵,null值;

  • 可以對元素進行排序;

  • 無序集合(插入和遍歷順序不一致);

4.2 TreeMap基本操作

public class TreeMapTest {
    public static void main(String[] agrs){
        //創建TreeMap對象:
        TreeMap<String,Integer> treeMap = new TreeMap<String,Integer>();
        System.out.println("初始化後,TreeMap元素個數爲:" + treeMap.size());

        //新增元素:
        treeMap.put("hello",1);
        treeMap.put("world",2);
        treeMap.put("my",3);
        treeMap.put("name",4);
        treeMap.put("is",5);
        treeMap.put("jiaboyan",6);
        treeMap.put("i",6);
        treeMap.put("am",6);
        treeMap.put("a",6);
        treeMap.put("developer",6);
        System.out.println("添加元素後,TreeMap元素個數爲:" + treeMap.size());

        //遍歷元素:
        Set<Map.Entry<String,Integer>> entrySet = treeMap.entrySet();
        for(Map.Entry<String,Integer> entry : entrySet){
            String key = entry.getKey();
            Integer value = entry.getValue();
            System.out.println("TreeMap元素的key:"+key+",value:"+value);
        }

        //獲取所有的key:
        Set<String> keySet = treeMap.keySet();
        for(String strKey:keySet){
            System.out.println("TreeMap集合中的key:"+strKey);
        }

        //獲取所有的value:
        Collection<Integer> valueList = treeMap.values();
        for(Integer intValue:valueList){
            System.out.println("TreeMap集合中的value:" + intValue);
        }

        //獲取元素:
        Integer getValue = treeMap.get("jiaboyan");//獲取集合內元素key爲"jiaboyan"的值
        String firstKey = treeMap.firstKey();//獲取集合內第一個元素
        String lastKey =treeMap.lastKey();//獲取集合內最後一個元素
        String lowerKey =treeMap.lowerKey("jiaboyan");//獲取集合內的key小於"jiaboyan"的key
        String ceilingKey =treeMap.ceilingKey("jiaboyan");//獲取集合內的key大於等於"jiaboyan"的key
        SortedMap<String,Integer> sortedMap =treeMap.subMap("a","my");//獲取集合的key從"a"到"jiaboyan"的元素

        //刪除元素:
        Integer removeValue = treeMap.remove("jiaboyan");//刪除集合中key爲"jiaboyan"的元素
        treeMap.clear(); //清空集合元素:

        //判斷方法:
        boolean isEmpty = treeMap.isEmpty();//判斷集合是否爲空
        boolean isContain = treeMap.containsKey("jiaboyan");//判斷集合的key中是否包含"jiaboyan"
    }
}

4.3 TreeMap排序

上一節,通過代碼展示出TreeMap簡單使用。而早在第一小節,筆者就說過TreeMap是一個可以對元素進行排序的集合,那麼究竟怎麼排序呢?

(1)使用元素自然排序

在使用自然順序排序時候,需要區分兩種情況:一種是Jdk定義的對象,一種是我們應用自己定義的對象;

public class SortedTest implements Comparable<SortedTest> {
    private int age;
    public SortedTest(int age){
        this.age = age;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    //自定義對象,實現compareTo(T o)方法:
    public int compareTo(SortedTest sortedTest) {
        int num = this.age - sortedTest.getAge();
        //爲0時候,兩者相同:
        if(num==0){
            return 0;
        //大於0時,傳入的參數小:
        }else if(num>0){
            return 1;
        //小於0時,傳入的參數大:
        }else{
            return -1;
        }
    }
}

public class TreeMapTest {
    public static void main(String[] agrs){
        //自然順序比較
        naturalSort();
    }
     //自然排序順序:
    public static void naturalSort(){
        //第一種情況:Integer對象
        TreeMap<Integer,String> treeMapFirst = new TreeMap<Integer, String>();
        treeMapFirst.put(1,"jiaboyan");
        treeMapFirst.put(6,"jiaboyan");
        treeMapFirst.put(3,"jiaboyan");
        treeMapFirst.put(10,"jiaboyan");
        treeMapFirst.put(7,"jiaboyan");
        treeMapFirst.put(13,"jiaboyan");
        System.out.println(treeMapFirst.toString());

        //第二種情況:SortedTest對象
        TreeMap<SortedTest,String> treeMapSecond = new TreeMap<SortedTest, String>();
        treeMapSecond.put(new SortedTest(10),"jiaboyan");
        treeMapSecond.put(new SortedTest(1),"jiaboyan");
        treeMapSecond.put(new SortedTest(13),"jiaboyan");
        treeMapSecond.put(new SortedTest(4),"jiaboyan");
        treeMapSecond.put(new SortedTest(0),"jiaboyan");
        treeMapSecond.put(new SortedTest(9),"jiaboyan");
        System.out.println(treeMapSecond.toString());
    }
}

在自然順序比較中,需要讓被比較的元素實現Comparable接口,否則在向集合裏添加元素時報:"java.lang.ClassCastException: com.jiaboyan.collection.map.SortedTest cannot be cast to java.lang.Comparable"異常;

這是因爲在調用put()方法時,會將傳入的元素轉化成Comparable類型對象,所以當你傳入的元素沒有實現Comparable接口時,就無法轉換,遍會報錯;

(2)使用自定義比較器排序

使用自定義比較器排序,需要在創建TreeMap對象時,將自定義比較器對象傳入到TreeMap構造方法中;

自定義比較器對象,需要實現Comparator接口,並實現比較方法compare(T o1,T o2);

值得一提的是,使用自定義比較器排序的話,被比較的對象無需再實現Comparable接口了;

public class SortedTest {
    private int age;
    public SortedTest(int age){
        this.age = age;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}
public class SortedTestComparator implements Comparator<SortedTest> {
    //自定義比較器:實現compare(T o1,T o2)方法:
    public int compare(SortedTest sortedTest1, SortedTest sortedTest2) {
        int num = sortedTest1.getAge() - sortedTest2.getAge();
        if(num==0){//爲0時候,兩者相同:
            return 0;
        }else if(num>0){//大於0時,後面的參數小:
            return 1;
        }else{//小於0時,前面的參數小:
            return -1;
        }
    }
}

public class TreeMapTest {
    public static void main(String[] agrs){
        //自定義順序比較
        customSort();
    }
    //自定義排序順序:
    public static void customSort(){
        TreeMap<SortedTest,String> treeMap = new TreeMap<SortedTest, String>(new SortedTestComparator());
        treeMap.put(new SortedTest(10),"hello");
        treeMap.put(new SortedTest(21),"my");
        treeMap.put(new SortedTest(15),"name");
        treeMap.put(new SortedTest(2),"is");
        treeMap.put(new SortedTest(1),"jiaboyan");
        treeMap.put(new SortedTest(7),"world");
        System.out.println(treeMap.toString());
    }
}

4.4 樹的自我介紹

在具體講解TreeMap底層結構之前,我們有必要先來了解下

那麼,到底什麼是呢?

--是計算機中的一種數據結構,是一個由n(>=1)個元素組成具有層次關係的集合。因該數據結構呈現出的形狀像一顆,所以稱這種數據結構叫做,只不過這顆樹是倒過來的。

在上面的截圖中,簡單抽象出數據結構中的。在生活中,這樣的結構比比皆是,例如:公司的組織架構、家族的族譜、計算機中的文件結構等等。只要符合上面的結構,均可以稱爲

在計算機領域中,只是一種簡稱,具體的實現還是交由其子樹來完成;這其中就包括:二叉樹、平衡二叉樹、紅黑樹、B樹、哈夫曼樹等。在本章節中,我們主要介紹其中的兩種數據結構---二叉樹、紅黑樹;

在介紹二叉樹之前,還需要對的相關術語進行理解:

舉個圖片來說,更加直觀:(只爲講解樹的概念)

節點(Nood)中的每一個元素都叫做節點,又或者稱爲結點;圖中A、B、C、D等都稱之爲節點;

根節點(樹根-Root):在樹中,最頂端的節點稱之爲根節點(樹根-Root);A節點就是整個樹的根節點;

子樹:除了根節點之外,其餘節點自由組合成的樹,稱之爲子樹,子樹可以是一個節點,也可以是多個節點;其中,Q節點可稱之爲子樹,B、D、G三個節點結合也可以稱之爲子樹;

葉子節點(葉節點):沒有孩子節點的節點,稱之爲葉子節點(葉節點),也就是該節點下面沒有子節點了;圖中D、G、Q、V稱之爲葉子節點;

父節點:簡單來說,就是一個節點上面的節點,就是該節點的父節點;B節點是D節點的父節點,C節點是Q節點的父節點;

樹的高度:從葉子節點(此時高度爲1)開始自底向上逐層增加,得到的值稱之爲樹的高度;截圖中樹的高度爲4(V、H、C、A);

樹的深度:從根節點(此時深度爲1)開始自上而下逐層增加,最終得到的值稱之爲樹的深度;截圖中樹的深度爲4(A、C、H、V);

4.5 二叉樹

二叉樹是最基礎的結構,也是樹結構中的根基;

二叉樹可以有多個元素,也可以只有一個元素,當然也可以一個元素也沒有,它是一個元素的集合;二叉樹規定了在每個節點下最多隻能擁有兩個子節點,一左一右;其中,左節點可以稱爲左子樹,右節點稱爲右子樹;但沒有要求左子樹中任意節點的值要小於右子樹中任意節點的值。

說到這,暫時我們可以簡單地將二叉樹理解成鏈表,只不過這個鏈表被從中間進行了分隔,一邊朝右,一邊朝左,就形成了一顆二叉樹。

二叉樹節點實現:

public class TreeNode {

    //節點的值:
    private int data;
    //指向左孩子的指針:
    private TreeNode leftTreeNode;
    //指向右孩子的指針:
    private TreeNode rightTreeNode;
    //指向父節點的指針
    private TreeNode parentNode;

    public TreeNode(int data, TreeNode leftTreeNode,
                    TreeNode rightTreeNode, TreeNode parentNode) {
        this.data = data;
        this.leftTreeNode = leftTreeNode;
        this.rightTreeNode = rightTreeNode;
        this.parentNode = parentNode;
    }
    public int getData() {
        return data;
    }
    public void setData(int data) {
        this.data = data;
    }
    public TreeNode getLeftTreeNode() {
        return leftTreeNode;
    }
    public void setLeftTreeNode(TreeNode leftTreeNode) {
        this.leftTreeNode = leftTreeNode;
    }
    public TreeNode getRightTreeNode() {
        return rightTreeNode;
    }
    public void setRightTreeNode(TreeNode rightTreeNode) {
        this.rightTreeNode = rightTreeNode;
    }
    public TreeNode getParentNode() {return parentNode;}
    public void setParentNode(TreeNode parentNode) {this.parentNode = parentNode;}
}

4.6 二叉查找樹

說完了二叉樹,下面繼續說二叉查找樹;相比於二叉樹來說,二叉查找樹的特點更加明確。

二叉查找樹規定:

如果二叉查找樹的左子樹不爲空,那麼它的左子樹上的任意節點的值都小於根節點的值;

如果二叉查找樹的右子樹不爲空,那麼它的右子樹上的任意節點的值都大於根節點的值;

也就是說,二叉查找樹的左子樹中任意節點的值都小於右子樹中任意節點的值。而且,在左子樹和右子樹中也同樣形成了二叉查找樹;所以說二叉查找樹的特點比二叉樹更加明確。

此外,根據上面的特點,我們還可以使用二分查找在樹中進行元素搜索:如果查找的元素大於根節點,則在樹的右邊進行搜索;如果小於根節點,則在樹的左邊進行搜索。如果等於根節點,則直接返回;所以二叉查找樹的查詢效率遠遠高於二叉樹。

4.8 平衡二叉樹(AVL樹)

前幾節中,筆者說過:可以將二叉樹簡單地理解爲一個鏈表結構。當時,是爲了能讓各位能對二叉樹有一個最直觀的理解。

其實,在具體的實現中二叉樹確實會形成一個鏈表結構。

試想下,如果一個二叉樹的左子樹爲空,只有右子樹,且右子樹中又只有右節點(左節點),那麼這個二叉樹就形成了一個不折不扣的鏈表。對於二叉查找樹來說,二分查找也就失去了原有的性能,變成了順序查找。即元素的插入順序是1、2、3、4、5、6的話,即可實現。

爲了針對這一情況,平衡二叉樹出現了(又稱爲AVL樹),它要求左右子樹的高度差的絕對值不能大於1,也就是說左右子樹的高度差只能爲-1、0、1。

4.7 紅黑樹

終於說到了今天的重中之重--紅黑樹,相比於之前講過的數據結構,紅黑樹的難度有所增加,你要做好準備!

只要帶有字,它就遠離不了二叉樹的結構。

紅黑樹,本質上依舊一顆二叉查找樹,它滿足了二叉查找樹的特點,即左子樹任意節點的值永遠小於右子樹任意節點的值。

不過,二叉查找樹還有一個致命的弱點,即左子樹(右子樹)可以爲空,而插入的節點全部集中到了樹的另一端,致使二叉查找樹失去了平衡,二叉查找樹搜索性能下降,從而失去了使用二分查找的意義。

爲了維護樹的平衡性,平衡二叉樹(AVL樹)出現了,它用左右子樹的高度差來保持着樹的平衡。而我們本節要介紹的紅黑樹,用的是節點的顏色來維持樹的平衡。

那麼,紅黑樹是怎麼利用顏色來維持平衡的呢?接下來,讓我們慢慢道來。

紅黑樹,即紅顏色和黑顏色並存的二叉樹,插入的元素節點會被賦爲紅色或者黑色,待所有元素插入完成後就形成了一顆完整的紅黑樹。如下圖所示:

不過,你不要想當然的以爲只是給二叉樹的階段隨意賦爲黑色或者紅色,就可保證樹的平衡。事情遠沒有你想象的那麼簡單。

一顆紅黑樹必須滿足一下幾點要求:

  1. 樹中每個節點必須是有顏色的,要麼紅色,要麼黑色;

  2. 樹中的根節點必須是黑色的;

  3. 樹中的葉節點必須是黑色的,也就是樹尾的NIL節點或者爲null的節點;

  4. 樹中任意一個節點如果是紅色的,那麼它的兩個子節點一點是黑色的;

  5. 任意節點到葉節點(樹最下面一個節點)的每一條路徑所包含的黑色節點數目一定相同;

科普:NIL節點是就是一個假想的或是無實在意義的節點,所有應該指向NULL的指針,都看成指向了NIL節點。包括葉節點的子節點指針或是根節點的父指針(均是指向null的)

除了給節點賦顏色之外,還會對節點進行左旋、右旋操作,以來維持樹的平衡。那麼,左旋、右旋又什麼?

首先,我們先來看下在紅黑樹中,每一個節點的數據結構是什麼樣子的?

public class TreeNode {
    //節點的值:
    private int data;
    //節點的顏色:
    private String color;
    //指向左孩子的指針:
    private TreeNode leftTreeNode;
    //指向右孩子的指針:
    private TreeNode rightTreeNode;
    //指向父節點的指針
    private TreeNode parentNode;

    public TreeNode(int data, String color, TreeNode leftTreeNode,
                    TreeNode rightTreeNode, TreeNode parentNode) {
        this.data = data;
        this.color = color;
        this.leftTreeNode = leftTreeNode;
        this.rightTreeNode = rightTreeNode;
        this.parentNode = parentNode;
    }
    public int getData() {
        return data;
    }
    public void setData(int data) {
        this.data = data;
    }
    public String getColor() {return color;}
    public void setColor(String color) {this.color = color;}
    public TreeNode getLeftTreeNode() {
        return leftTreeNode;
    }
    public void setLeftTreeNode(TreeNode leftTreeNode) {
        this.leftTreeNode = leftTreeNode;
    }
    public TreeNode getRightTreeNode() {
        return rightTreeNode;
    }
    public void setRightTreeNode(TreeNode rightTreeNode) {
        this.rightTreeNode = rightTreeNode;
    }
    public TreeNode getParentNode() {return parentNode;}
    public void setParentNode(TreeNode parentNode) {this.parentNode = parentNode;}
}

如果用圖片來形容的話:

4.7.1 旋轉

上面說過,爲了維護紅黑樹的平衡,不只是給節點着色那麼簡單,還有更復雜的處理邏輯。

而這更復雜的處理邏輯就是對節點進行旋轉操作,其中旋轉操作又分爲左旋、右旋兩種;

對於樹的操作,最常見的就是添加、刪除而已。不過,在添加或者刪除之後,紅黑樹發生了改變,可能就不滿足以上的5點要求,也就不是一顆紅黑樹了。於是我們需要通過對節點的旋轉,使其重新成爲一顆紅黑樹。

  1. 左旋

左旋,顧名思義就是對節點進行向左旋轉處理;

對節點X進行向左進行旋轉,將其變成了一個左子節點;

這個左旋中的左,就是將被旋轉的節點變成一個左節點;

其中,如果Y的的左子節點不爲null,則將其賦值X的右子節點;

  1. 右旋

右旋,同理,也就是對節點進行向右旋轉處理;

對節點Y進行向右進行旋轉,將其變成了一個右子節點;

這個右旋中的右,就是將被旋轉的節點變成一個右節點;

其中,如果X節點的右子節點不爲null,則賦值給Y的左子節點;

補充:在舉個例子,讓大家更直觀的理解:(暫時先忽略紅黑樹,只關注旋轉)

4.7.2 紅黑樹節點添加

本節中,筆者就來介紹下紅黑樹節點的添加。

在將一個節點插入到紅黑樹中,首選需要將紅黑樹當做一顆二叉樹查找樹來對待,將節點進行插入。然後,爲新插入的節點進行着色;最後,通過旋轉和重新着色的方式來修正樹的平衡,使其成爲一顆紅黑樹;

(1)對於紅黑樹來說,其底層依舊是一顆二叉查找樹。當我們插入新的元素時,它依舊會對該元素進行排序比較,將其送入到左子樹或者是右子樹中。

(2)在將新節點安置好以後,在對其進行着色處理,必須着成紅色。

你此時可能會問,爲什麼要着成紅色,而不是黑色呢?

這就需要我們重新回顧下紅黑樹的規範了:

1. 樹中每個節點必須是有顏色的,要麼紅色,要麼黑色;

2. 樹中的根節點必須是黑色的;

3. 樹中的葉節點必須是黑色的,也就是樹尾的NIL節點或者爲null的節點;

4. 樹中任意一個節點如果是紅色的,那麼它的兩個子節點一點是黑色的;

5. 任意節點到葉節點(樹最下面一個節點)的每一條路徑所包含的黑色節點數目一定相同;

通過以上的規範,我們可以分析出,當將新增節點着成紅色後,我們違背以上規範的條數最少,恢復平衡起來最省事,當新增節點爲紅色時:

1.不會違背第五條--黑色節點數目相同;

2.不會違背第一條--節點必須是紅色,或者黑色;

3.不會違背第二條--根節點是黑色--當樹中已有元素時候,我們新增節點並不會影響根節點的顏色(若樹中沒有元素,新增節點會被當成根節點,此時雖被着爲紅色,但在最後一步中還會對其重新着色,變成黑色);

4.不會違背第三條--此處所指的葉節點指的是葉節點的子節點,也就是爲null的元素;

5.可能會違背第四條--任意節點爲紅色,其子節點一定是黑色;

如果新插入的節點的父節點是紅色的話,那麼第四條一定會違背,所以說我們需要對其進行旋轉處理;

(3)衝突來了,解決衝突(對節點進行旋轉)

  • 當被插入節點是根節點:

    新插入的節點後,如果爲根節點,則直接將其變成黑色,並返回;

     

  • 當被插入節點的父節點是黑色:

    新插入的節點被着成紅色後,發現其父節點是黑色,並不影響紅黑樹結構,所以並不需要對平衡性做處理,直接返回;

     

  • 當被插入節點的父節點是紅色(最複雜):

    新插入的節點被着成紅色後,發現其父節點是紅色,此時紅黑樹的平衡被打破,需要特別處理。

    說之前,簡單的闡述下什麼是該節點的祖父節點,什麼是該節點的叔叔節點(畫圖一目瞭然);

case1:叔叔節點爲黑色(空節點默認爲黑色)

A.2是3的左子節點,3是5的左子節點;

B.17是16的右子節點,16是15的右子節點;

C.17是15的右子節點,15是23的左子節點 

D.43是56的左子節點,46是41的右子節點;

其中,A、B稱之爲外側插入,C、D稱之爲內測插入。

首先,來看A的解決方案:

將3設置爲黑色,5設置爲紅色,再對5進行右旋,結果如下:

 

來看B的解決方案:

將16設置爲黑色,15設置爲紅色,再對15進行左旋,結果如下:

 

來看C的解決方案:

將15進行左旋,成爲17的左子節點,此時形成了A情況,再將17置爲黑色,23置爲紅色,再對23進行右旋,結果如下:

 

來看D的解決方案:
將46進行右旋,成爲41的右子節點,此時形成了B情況,再將43置爲黑色,41置爲紅色,再對41進行左旋,結果如下:

 

case2:叔叔節點爲紅色

當叔叔節點爲紅色時,處理的邏輯較爲簡單,主要對元素節點的顏色進行處理,無需左旋、右旋的操作。

在A情況中,無論新插入的元素是7還是12,處理過程都是一樣的;

將10和27置爲黑色,23置爲紅色,再將23置爲黑色,新插入元素7(12)保持紅色不變,結束返回;

同理,在B情況中,處理26或者35的邏輯也是相同的;

將15和30置爲黑色,18置爲紅色,再將18置爲黑色,新插入元素26(35)保持紅色不變,結束返回;

同上,暫不畫圖了;

以上,就是紅黑樹添加節點的處理邏輯,希望以上的圖片可以讓你對着色、旋轉有一個清晰的認識!

有興趣的朋友,可以參考紅黑樹網站進行實際操作!

4.7.3 紅黑樹節點刪除

暫省略;

4.8 TreeMap源碼講解(基於JDK1.7.0_75)

上面,我們通過圖片的形式,對紅黑樹進行了全面的講解。接下來,就讓我們學習下紅黑樹在Java中的實現--TreeMap;

  • TreeMap節點結構

TreeMap底層存儲結構與HashMap基本相同,依舊是Entry對象,存儲key--value鍵值對,子節點的引用和父節點的引用,以及默認的節點顏色(黑色);

static final class Entry<K,V> implements Map.Entry<K,V> {
    //插入節點的key:
    K key;

    //插入節點的value:
    V value;

    //插入節點的左子節點:
    java.util.TreeMap.Entry<K,V> left = null;

    //插入節點的右子節點:
    java.util.TreeMap.Entry<K,V> right = null;

    //插入節點的父節點:
    java.util.TreeMap.Entry<K,V> parent;

    //插入節點默認的顏色:
    boolean color = BLACK;

    //Entry對象的構造函數:
    Entry(K key, V value, java.util.TreeMap.Entry<K,V> parent) {
        this.key = key;
        this.value = value;
        this.parent = parent;
    }
    public K getKey() {
        return key;
    }
    public V getValue() {
        return value;
    }
    public V setValue(V value) {
        V oldValue = this.value;
        this.value = value;
        return oldValue;
    }
    public boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry<?,?>)o;
        return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
    }
    public int hashCode() {
        int keyHash = (key==null ? 0 : key.hashCode());
        int valueHash = (value==null ? 0 : value.hashCode());
        return keyHash ^ valueHash;
    }
    public String toString() {
        return key + "=" + value;
    }
}
  • TreeMap構造函數

與HashMap不同,TreeMap底層不是數組結構,成員變量中並沒有數組,而是用根節點root來替代,所有的操作都是通過根節點來進行的。

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 java.util.TreeMap.Entry<K,V> root = null;

    //集合元素數量:
    private transient int size = 0;

    //對TreeMap操作的數量:
    private transient int modCount = 0;

    //無參構造方法:comparator屬性置爲null
    //代表使用key的自然順序來維持TreeMap的順序,這裏要求key必須實現Comparable接口
    public TreeMap() {
        comparator = null;
    }

    //帶有比較器的構造方法:初始化comparator屬性;
    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }

    //帶有map的構造方法:
    //同樣比較器comparator爲空,使用key的自然順序排序
    public TreeMap(Map<? extends K, ? extends V> m) {
        comparator = null;
        putAll(m);
    }

    //帶有SortedMap的構造方法:
    //根據SortedMap的比較器來來維持TreeMap的順序
    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) {
        }
    }
}
  • TreeMap元素添加

將新增節點的key--value插入到TreeMap當中,在上文中我們已經通過圖文的形式介紹了新增的流程,如果你還不明白,可以結合源碼進行理解;

首先找到新增節點的位置,其次在判斷TreeMap是否處於平衡狀態,若不平衡,則對節點進行着色、旋轉操作;

//插入key-value:
public V put(K key, V value) {
    //根節點賦值給t:
    java.util.TreeMap.Entry<K,V> t = root;
    //如果根節點爲null,則創建第一個節點,根節點
    if (t == null) {
        //對傳入的元素進行比較,若TreeMap沒定義了Comparator,則驗證傳入的元素是否實現了Comparable接口;
        compare(key, key);
        //根節點賦值:
        root = new java.util.TreeMap.Entry<>(key, value, null);
        //長度爲1:
        size = 1;
        //修改次數+1
        modCount++;
        //直接返回:此時根節點默認爲黑色
        return null;
    }

    //如果根節點不爲null:
    int cmp;
    java.util.TreeMap.Entry<K,V> parent;
    Comparator<? super K> cpr = comparator;

    //判斷TreeMap中自定義比較器comparator是否爲null:
    if (cpr != null) {
        // do while循環,查找新插入節點的父節點:
        do {
            // 記錄上次循環的節點t,首先將根節點賦值給parent:
            parent = t;
            //利用自定義比較器的比較方法:傳入的key跟當前遍歷節點比較:
            cmp = cpr.compare(key, t.key);
            //判斷結果小於0,處於父節點的左邊 
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
            //判斷結果大於0,處於父節點的右邊 
                t = t.right;
            else
            //判斷結果等於0,爲當前節點,覆蓋原有節點處的value:
                return t.setValue(value);
        // 只有當t爲null,也就是找到了新節點的parent了
        } while (t != null);
    } else {
        //沒有自定義比較器:
        if (key == null)
            //TreeMap不允許插入key爲null,拋異常:
            throw new NullPointerException();
        //將key轉換爲Comparable對象:若key沒有實現Comparable接口,此處會報錯
        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);
    }

    //構造新增節點對象:
    java.util.TreeMap.Entry<K,V> e = new java.util.TreeMap.Entry<>(key, value, parent);

    //根據之前的比較結果,判斷新增節點是在父節點的左邊還是右邊
    if (cmp < 0)
        // 如果新節點key的值小於父節點key的值,則插在父節點的左側
        parent.left = e;
    else
        // 如果新節點key的值大於父節點key的值,則插在父節點的右側
        parent.right = e;
    //核心方法:插入新的節點後,爲了保持紅黑樹平衡,對紅黑樹進行調整
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}

//對插入的元素比較:若TreeMap沒有自定義比較器,則調用調用默認自然順序比較,要求元素必須實現Comparable接口;
//若自定義比較器,則用自定義比較器對元素進行比較;
final int compare(Object k1, Object k2) {
    return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
            : comparator.compare((K)k1, (K)k2);
}

//核心方法:維護TreeMap平衡的處理邏輯;(回顧上面的圖文描述)
private void fixAfterInsertion(java.util.TreeMap.Entry<K,V> x) {
    //首先將新插入節點的顏色設置爲紅色
    x.color = RED;
    //TreeMap是否平衡的重要判斷,當不在滿足循環條件時,代表樹已經平衡;
    //x不爲null,不是根節點,父節點是紅色(三者均滿足才進行維護):
    while (x != null && x != root && x.parent.color == RED) {
        //節點x的父節點 是 x祖父節點的左孩子:
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            //獲取x節點的叔叔節點y:
            java.util.TreeMap.Entry<K,V> y = rightOf(parentOf(parentOf(x)));
            
            //叔叔節點y是紅色:
            if (colorOf(y) == RED) {
                //將x的父節點設置黑色:
                setColor(parentOf(x), BLACK);  
                //將x的叔叔節點y設置成黑色:
                setColor(y, BLACK);
                //將x的祖父節點設置成紅色:
                setColor(parentOf(parentOf(x)), RED);
                //將x節點的祖父節點設置成x(進入了下一次判斷):
                x = parentOf(parentOf(x));
            } else {
                 //叔叔節點y不爲紅色:
                //x爲其父節點的右孩子:
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateLeft(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                //右旋:
                rotateRight(parentOf(parentOf(x)));
            }
        } else {
            //節點x的父節點 是x祖父節點的右孩子:

            //獲取x節點的叔叔節點y:
            java.util.TreeMap.Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            //判斷叔叔節點y是否爲紅色:
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);12
                setColor(y, BLACK);5
                setColor(parentOf(parentOf(x)), RED);10
                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;
}

//獲取節點的顏色:
private static <K,V> boolean colorOf(java.util.TreeMap.Entry<K,V> p) {
    //節點爲null,則默認爲黑色;
    return (p == null ? BLACK : p.color);
}

//獲取p節點的父節點:
private static <K,V> java.util.TreeMap.Entry<K,V> parentOf(java.util.TreeMap.Entry<K,V> p) {
    return (p == null ? null: p.parent);
}

//對節點進行着色,TreeMap使用了boolean類型來代表顏色(true爲紅色,false爲黑色)
private static <K,V> void setColor(java.util.TreeMap.Entry<K,V> p, boolean c){
    if (p != null)
        p.color = c;
}

//獲取左子節點:
private static <K,V> java.util.TreeMap.Entry<K,V> leftOf(java.util.TreeMap.Entry<K,V> p) {
    return (p == null) ? null: p.left;
}

//獲取右子節點:
private static <K,V> java.util.TreeMap.Entry<K,V> rightOf(java.util.TreeMap.Entry<K,V> p) {
    return (p == null) ? null: p.right;
}

//左旋:
private void rotateLeft(java.util.TreeMap.Entry<K,V> p) {
    if (p != null) {
        java.util.TreeMap.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;
    }
}

//右旋:
private void rotateRight(java.util.TreeMap.Entry<K,V> p) {
    if (p != null) {
        java.util.TreeMap.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底層是紅黑樹結構,而紅黑樹本質是一顆二叉查找樹,所以在獲取節點方面,使用二分查找算法性能最高;

//通過key獲取對應的value:
public V get(Object key) {
    //獲取TreeMap中對應的節點:
    java.util.TreeMap.Entry<K,V> p = getEntry(key);
    //獲取節點的值:
    return (p==null ? null : p.value);
}

//通過key獲取Entry對象:
final java.util.TreeMap.Entry<K,V> getEntry(Object key) {
    //TreeMap自定義比較器不爲空,使用自定義比較器對象來獲取節點:
    if (comparator != null)
        //獲取節點:
        return getEntryUsingComparator(key);

    //如果key爲null,則拋出異常,TreeMap中不允許存在爲null的key:
    if (key == null)
        throw new NullPointerException();

    //將傳入的key轉換成Comparable類型,傳入的key必須實現Comparable接口
    Comparable<? super K> k = (Comparable<? super K>) key;
    //獲取根節點:
    java.util.TreeMap.Entry<K,V> p = root;

    // 使用二分查找方式,首先判斷傳入的key與根節點的key哪個大:
    while (p != null) {
        //傳入的key與p節點的key進行大小比較:
        int cmp = k.compareTo(p.key);
        //傳入的key小於p節點的key,則從根的左子樹中搜索:
        if (cmp < 0)
            //左邊
            p = p.left;
        else if (cmp > 0)
            //傳入的key大於p節點的key,則從根的右邊子樹中搜索:
            //右邊
            p = p.right;
        else
            //傳入的key等於p節點的key,則直接返回當前節點:
            return p;
    }
    //以上循環沒有找對對應的節點,則返回null:
    return null;
}

//使用自定義比較器進行元素比較,獲取對節點:
final java.util.TreeMap.Entry<K,V> getEntryUsingComparator(Object key) {
    K k = (K) key;
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        java.util.TreeMap.Entry<K,V> p = root;
        while (p != null) {
            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;
}
  • TreeMap元素刪除

暫忽略;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章