TreeMap(jdk1.7)瞭解一下

基本構造方法

// 默認構造方法,使用默認構造方法,要求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。

參考文章

java編程的邏輯基礎(馬俊昌)

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