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樹)出現了,它用左右子樹的高度差來保持着樹的平衡。而我們本節要介紹的紅黑樹,用的是節點的顏色來維持樹的平衡。
那麼,紅黑樹是怎麼利用顏色來維持平衡的呢?接下來,讓我們慢慢道來。
紅黑樹,即紅顏色和黑顏色並存的二叉樹,插入的元素節點會被賦爲紅色或者黑色,待所有元素插入完成後就形成了一顆完整的紅黑樹。如下圖所示:
不過,你不要想當然的以爲只是給二叉樹的階段隨意賦爲黑色或者紅色,就可保證樹的平衡。事情遠沒有你想象的那麼簡單。
一顆紅黑樹必須滿足一下幾點要求:
-
樹中每個節點必須是有顏色的,要麼紅色,要麼黑色;
-
樹中的根節點必須是黑色的;
-
樹中的葉節點必須是黑色的,也就是樹尾的NIL節點或者爲null的節點;
-
樹中任意一個節點如果是紅色的,那麼它的兩個子節點一點是黑色的;
-
任意節點到葉節點(樹最下面一個節點)的每一條路徑所包含的黑色節點數目一定相同;
科普: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點要求,也就不是一顆紅黑樹了。於是我們需要通過對節點的旋轉,使其重新成爲一顆紅黑樹。
- 左旋
左旋,顧名思義就是對節點進行向左旋轉處理;
對節點X進行向左進行旋轉,將其變成了一個左子節點;
這個左旋中的左,就是將被旋轉的節點變成一個左節點;
其中,如果Y的的左子節點不爲null,則將其賦值X的右子節點;
- 右旋
右旋,同理,也就是對節點進行向右旋轉處理;
對節點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元素刪除
暫忽略;