基本構造方法
// 默認構造方法,使用默認構造方法,要求Map中的鍵實現Comparable接口,TreeMap內部進行各種比較時會調用鍵的Comparable 接口中的compareTo方法
public TreeMap()
//一個比較器對象comparator,如果compartor不爲null,在TreeMap內部進行比較時會調用這個comparator的compare方法,而不再調用鍵的compareTo方法,也不要求實現Comparable接口
public TreeMap(Comparator<? super K> comparator)
需要強調的是,TreeMap 是按鍵而不是按值有序,無論哪一種,都是對鍵而非值進行比較
Map<String, String> map = new TreeMap<>();
map.put("a", "abstract");
map.put("c", "call");
map.put("b", "basic");
map.put("T", "tree");
for(Entry<String,String> kv : map.entrySet()){
System.out.println(kv.getKey()+"="+kv.getValue()+" ");
}
輸出結果爲: 是按鍵排序的,T排在最前面,是因爲大寫字母的ASCLL碼都小於小寫字母
T=tree a=abstract b=basic c=call
如果忽略大小寫,可以傳遞一個比較器,String類有一個靜態成員CASE_INSENSITIVE_ORDER ,它就是一個忽略大小寫的Comparator對象,替換第一行代碼:
Map<String, String> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
輸出結果爲:
a=abstract b=basic c=call T=tree
正常排序是從大到小,如果希望逆序排序,可以傳遞一個不同的Comparator對象
Map<String, String> map = new TreeMap<>(new Comparator<String>(){
@Override
public int compare(String o1, String o2){
return 02.compareTo(o1);
}
})
如果既希望逆序且忽略大小寫呢?
Map<String, String> map = new TreeMap<>(Collections.reverseOrder(String.CASE_INSENSITIVE_ORDER));
需要說明的是,TreeMap 使用鍵的比較結果對鍵進行重排,即使鍵實際上不同,但只要比較結果相同,他們就會被認爲相同,鍵只會保存一份。
比如:
Map<String, String> map = new TreeMap<>(String.Case_INSENSITIVE_ORDER);
map.put("T", tree);
map.put("t", "try");
for(Entry<String, String> kv : map.entrySet()){
System.out.println(kv.getKey()+"="+kv.getValue()+" ");
}
輸出結果爲
T = tree
如果對日期2020-7-10 格式排序呢?可以自定義一個比較器,將字符串轉化爲日期,按日期進行比較
Map<String, Sting> map = new TreeMap<>(new Compartor<String>(){
SimplateDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
@Override
public int compare(String o1, String o2){
try{
return sdf.parse(o1).compareTo(sdf.parse(o2));
}
}catch(ParseException e){
e.printStackTrace();
return 0;
}
}
TreeMap 按鍵有序,它還實現了SortedMap 和NavigableMap接口,通過兩個接口,可以方便地根據鍵的順序進行查找,如第一個,最後一個,某一範圍的鍵,鄰近鍵等。
實現原理
TreeMap 內部是用紅黑樹實現的,紅黑樹是一種大致平衡的排序二叉樹,先看看Treemap的內部組成。
1、內部組成
private final Comparator<? super K> comparator;
private transient Entry<K, V> root = null;
private transient int size = 0;
comparator 就是比較器,在構造方法中傳遞,如果沒傳,就是null。 size爲當前鍵值對個數。root指向樹的根節點,從根節點可以訪問到每個節點,節點的類型爲Entry。Entry 是TreeMap的一個內部類。
static final class Entry<K, V> implements Map.Entry<K, V> {
K key;
V value;
Entry<K, V> letf = null ;
Entry<K, V> right = null;
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;
}
}
每個節點除了鍵(key) 和 值(value) 之外,還有三個引用,分別指向其左孩子,右孩子和父節點,對於根節點,父節點爲null,對於葉子節點,孩子節點都爲null,還有一個color表示顏色。TreeMap是用紅黑樹實現的,每個節點都有一個顏色,非黑即紅。
2、保存鍵值對
public V put(K key,V value){
Entry<K, V> t =root;
//當添加第一個節點時,root爲null,執行的就是這段代碼
if(t == null){
compare(key,key);
root = new Entry<>(key,value,null);
size = 1;
modCount++;
return null;
}
}
//這裏不是爲了比較,而是爲了檢查key的類型,如果類型不匹配或爲null,那麼compare 方法會拋出異常
final int compare(Object k1, object k2){
return comparator == null ? ((Comparable<? super k>) k1).compareTo((k)k2) :comparator.compare((K)k1,(K)k2);
}
// 當不是第一次添加,會執行以下代碼
int cmp;
Entry<K, V> parent;
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);
}
尋找一個從根節點開始循環的過程,在循環中,cmp保存比較結果,t指向當前比較節點,parent爲t 的父節點,循環結束後parent就是要找的父節點。從根節點開始比較鍵,如果小於根節點,就將t設爲左孩子,與左孩子比較,大於就與右孩子比較,就這樣一直比,直到t爲null或比較結果爲0.如果比較結果爲0,表示已經有這個鍵了,設置值,然後返回。如果 t爲null,則當退出循環時,parent就是指向待插入節點的父節點。
找到父節點後,就是新建一個節點,根據新的鍵與父節點鍵的比較結果,插入作爲左孩子或右孩子,並增加size和modCount,代碼如下:
Entry<K, V> e = new Entry<>(key,value,parent);
if(cmp <0)
parent.left = e;
else
parent.right = e;
//調整數的結構,使之符合紅黑樹的約束,保持大致平衡
fixAfterInsertion(e);
size++;
modCount++
小結一下,基本思路就是比較找到父節點,並插入作爲其左孩子或右孩子,然後調整保持樹的大致平衡。
3、根據鍵獲取值
public V get(Object key){
Entry<K,V> p = getEntry(key);
return(p == null? null : p.value);
}
final Entry<K,V> getEntry(Object key){
if(comparator != null)
return getEntryUsingComparator();
if(key == null)
throw new nullPointerException();
Comparable<? super k> K = (Comparable<? super k>) key;
Entry<K, V> p =root;
while(p != null){
int cmp = k.compareTo(p.key);
if(cmp < 0)
p = p.left;
else if(cmp > 0)
p=p.right;
else
return p;
}
return null;
}
4、查看是否包含某個值
TreeMap 可以高效地按鍵進行查找,但如果要根據值進行查找,則需要遍歷。
public boolean containsValue(Object value){
for(Entry<K, V> e = getFirstEntry(); e != null; e=successor(e))
if(valEquals(value, e.value))
return true;
return false;
}
//返回第一個節點,第一個節點就是最左邊的節點
final Entry<K, V> getFirstEntry(){
Entry<K, V> p = root;
if(p != null)
while(p.left != null)
p=p.left;
return p;
}
// 返回給定節點的後繼節點
static Entry<K, V> TreeMap。Entry<K, V> successor(Entry<K, V> t){
if(t == null){
return null;
}else if(t.right != null){
Entry<K, V> p = t.right;
while(p.left != null)
p = p.left;
return p;
}else {
Entry<K, V> p = t.parent;
Entry<K, V> ch =t;
while(p != null && ch == p.right){
ch = p;
p =p.parent;
}
return p;
}
}
後繼算法:
(1) 如果有右孩子,則後繼節點爲右子樹中最小的節點。
(2)如果沒有右孩子,後繼節點爲父節點或某個祖先節點,從當前節點往上找,如果它是父節點的右孩子,則繼續找父節點,直到它不是右孩子或父節點爲空,則第一個非右孩子節點的父節點就是後繼節點,如果父節點爲空,則後繼節點爲null。
具體實例可以查看排序二叉樹(概念性)瞭解一下
5、根據鍵刪除鍵值對
public V remove(Object key){
Entry<K, V> p = getEntry(key);
if(p == null)
return null;
V oldValue = p.value;
deleteEntry(p);
return oldValue;
}
private void deleteEntry(Entry<K, V> p){
modCount++;
size--;
//這裏處理的就是兩個孩子的情況,s爲後繼,當前節點p的key 和 value設置s的key和value,然後將待刪節點p指向了s,這樣就轉換了成一個孩子或葉子節點的情況
if(p.left != null && p.right != null){
Entry<K, V> s = successor(p);
p.key = s.key;
p.value = s.value;
p=s
}
//p 爲待刪節點,replacement爲要替換p的孩子節點,主體代碼就是在p的父節點p.parent和replacement之間建立鏈接,以替換p.parent和p原來的鏈接,如果p.parent爲 null,則修改root以指向新的根。fixDeletion重新平衡二叉樹。
Entry<K, V> replacement = (p.left != null ? p.left:p.right);
if(replacement != null){
replacement.parent = p.parent;
if(p.parent == null)
root =replacement;
else if(p == p.parent.left)
p.parent.left = replacement;
else
p.parent.right = replacement;
p.left=p.right=p.parent=null;
if(p.color ==BLACK)
fixAfterDeletion(replacement)
} else if(p.parnet == null){ //葉子節點情況,分爲兩種情況,一種是刪除最後一個節點,,修改root 爲null,另一種是根據待刪節點是父節點的左孩子還是右孩子,相應的設置孩子節點爲null。
root = null;
}else{
if(p.color == BLACK)
fixAfterDeletion(p)
if(p.parent != null){
if(p == p.parent.left)
p.parent.left =null;
else if(p == p.parent.right)
p.parent.right = null;
p.parent=null;
}
}
}
刪除算法
(1)葉子節點:直接修改父節點對應引用置null即可。
(2)只有一個孩子:就是在父親節點和孩子節點直接建立鏈接。
(3)有兩個孩子:先找到後繼節點,找到後,替換當前節點的內容爲後繼節點,然後刪除後繼節點,因爲這個後繼節點一定沒左孩子,所以就將兩個漢字情況轉化爲前面兩種情況。
具體實例可以查看排序二叉樹(概念性)瞭解一下
小結
與HashMap相比,TreeMap同樣實現了Map接口,但內部使用紅黑樹實現,紅黑樹是統計效率比較高的大致平衡的排序二叉樹,這決定了它有如下特點:
(1) 按鍵有序,TreeMap 同樣實現了SortedMap 和 NavigableMap接口,可以方便地根據鍵的順序進行查找,如第一個,最後一個,某一範圍的鍵、鄰近鍵。
(2) 爲了按鍵有序,TreeMap 要求鍵實現Comparable接口或通過構造方法提供一個comparator對象。
(3) 根據鍵保存,查找,刪除的效率比較高,爲O(h) ,h爲樹的高度,在樹平衡的情況下,h爲log2(N),N爲節點數。
應該用HashMap 還是TreeMap呢? 不要求排序,優先考慮HashMap 要求排序,考慮TreeMap。