基本构造方法
// 默认构造方法,使用默认构造方法,要求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。