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编程的逻辑基础(马俊昌)

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