TreeMap是使用红黑树实现的,他是按键有序的。
红黑树:从根到叶子节点的路径,没有任何一条路径的长度会比其他路径长过两倍。红黑树把每个节点进行重色,对节点颜色有一些约束。它确保树是大致平衡的。
基本构造方法:
/**
* 该方法要求Map中的键必须实现Comparable接口,TreeMap进行各种个比较时会调用键的Comparable接口中的compareTo方法
*/
public TreeMap()
/**
* 这个构造方法要传入一个comparator比较器对象,如果comparator不为null,在TreeMap内部进行比较时会调用这个comparator的comapre方法,而不再调用键
* 的compareTo方法,也不要求键实现Comparable接口
*/
public TreeMap(Comparator<? super K> comparator)
内部组成
/**
* The comparator used to maintain order in this tree map, or
* null if it uses the natural ordering of its keys.
*这个 comparator用于维护树的排序,如果使用key的默认排序则comparator为null
* @serial
*/
private final Comparator<? super K> comparator;
private transient Entry<K,V> root; // 指向树的根节点
/**
* The number of entries in the tree
* 树中键值对的个数
*/
private transient int size = 0;
Entry(节点)为树的内部类:
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left;
Entry<K,V> right;
Entry<K,V> parent;
boolean color = BLACK;
/**
* Make a new cell with given key, value, and parent, and with
* {@code null} child links, and BLACK color.
*/
Entry(K key, V value, Entry<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
}
每个节点除了键和值之外,还有三个引用,分别为左、右孩子,以及父节点。
color表示颜色,默认为黑。
方法详解
1.put
/**
* Associates the specified value with the specified key in this map.
* 将制定的值与指定的键在map中做关联
* If the map previously contained a mapping for the key, the old
* 如果以前这个key在map中存在,那么旧值会被取代
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
*
* @return the previous value associated with {@code key}, or
* {@code null} if there was no mapping for {@code key}.
* (A {@code null} return can also indicate that the map
* previously associated {@code null} with {@code key}.)
* @throws ClassCastException if the specified key cannot be compared
* with the keys currently in the map
* @throws NullPointerException if the specified key is null
* and this map uses natural ordering, or its comparator
* does not permit null keys
*/
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
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);
}
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
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);
}
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
代码比较长,我们分段来看:
- 第一次添加的判断:
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check : 类型和null检查
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
首先判断根节点是否为null,如果为空,说明是第一次添加,则把新传入的键值节点设为根节点。然后size+1,modCount
用于记录修改次数
使用modCount属性的全是线程不安全的,关于modCount的解释可以参考这篇博文modCount到底是干什么的呢
其中比较奇怪的是一段类型和null检查的代码:compare(key, key),比较key和key有什么意思呢?
/**
* Compares two keys using the correct comparison method for this TreeMap.
* 使用这个TreeMap的比较方法比较两个key
*/
@SuppressWarnings("unchecked")
final int compare(Object k1, Object k2) {
// 如果comparator对象为null则使用key的compareTo方法进行比较,不为null则使用comparator的compare比较哦
return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
: comparator.compare((K)k1, (K)k2);
}
还是不太懂这段代码是什么意思,注意put方法中调用compare方法时的注释:类型和null检查;
这里主要是为了检查key是否实现Comparable接口或者是否为null——即检查key是否符合TreeMap的要求
- 寻找新put键值对的父节点
寻找键值对父节点的时候分两种情况,使用key自身的比较器或者使用初始化TreeMap时传入的比较器
2.1. 设置了comparator比较器:
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t; // 第一次执行这一步时,t是根节点
cmp = cpr.compare(key, t.key);
if (cmp < 0) // key小于根节点的
t = t.left; // 把t改为当前节点的左孩子,因为左孩子小于父节点
else if (cmp > 0) // key大于根节点
t = t.right; // 把t改为当前节点的右孩子
else // 说明跟当前节点key值,相等
return t.setValue(value);
} while (t != null); // 当退出循环时parent指向待插入节点的父节点
}
2.2. 没有设置comparator比较器
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key; // 如果key
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0) // key小于当前节点
t = t.left;
else if (cmp > 0) // key大于当前节点
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
Comparable可以认为是一个内比较器,实现了Comparable接口的类有一个特点,就是这些类是可以和自己比较的,至于具体和另一个实现了Comparable接口的类如何比较,则依赖compareTo方法的实现,compareTo方法也被称为自然比较方法。
Comparator可以认为是是一个外比较器,个人认为有两种情况可以使用实现Comparator接口的方式:
1、一个对象不支持自己和自己比较(没有实现Comparable接口),但是又想对两个对象进行比较
2、一个对象实现了Comparable接口,但是开发者认为compareTo方法中的比较方式并不是自己想要的那种比较方式
引用自:Comparable和Comparator的区别
- 新建节点
找到父节点后,新建一个节点挂在父节点上,挂之前判断是左孩子还是右孩子,并增加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++;
return null;
}
其中fixAfterInsertion方法,调整树的结构,使之符合红黑树约束,保持大致平衡,这里就不深入探讨了。
2. get
上代码:
public V get(Object key) {
Entry<K,V> p = getEntry(key);
return (p==null ? null : p.value);
}
get方法就不多说了,是老铁都明白。
getEntry方法:
final Entry<K,V> getEntry(Object key) {
// Offload comparator-based version for sake of performance
// 同样分是否传入了comparator对象两种情况
if (comparator != null)
return getEntryUsingComparator(key);
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
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;
}
跟put方法很类似,如果comparator不为空,则调用单独的方法getEntryUsingComparator,否则假定key实现了Comparable接口,使用compareTo进行比较
找的逻辑跟put一样,从根开始找,比根小,往左找;比根大,往右找,找到为止,如果没有找到,返回null;。getEntryUsingComparator方法的逻辑类似,就不多说了。
3. containsValue
TreeMap可以根据键高效的进行查找,但是如果根据值进行查找,则需要遍历,上代码:
public boolean containsValue(Object value) {
// getFirstEntry获取第一个点(key最小)
// successor获取给定节点的后继节点
for (Entry<K,V> e = getFirstEntry(); e != null; e = successor(e))
if (valEquals(value, e.value)) // valEquals比较值是否相等
return true;
return false;
}
getFirstEntry:
final Entry<K,V> getFirstEntry() {
Entry<K,V> p = root;
if (p != null)
while (p.left != null)
p = p.left;
return p;
}
successor:
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
if (t == null)
return null;
else if (t.right != null) { // 右子节点不为null则找右子节点下的最左侧节点
Entry<K,V> p = t.right;
while (p.left != null)
p = p.left;
return p;
} else { // 右子节点为null
Entry<K,V> p = t.parent;
Entry<K,V> ch = t;
while (p != null && ch == p.right) { // 父节点不为null且当前节点是父节点的右孩子
ch = p;
p = p.parent;
}
return p; // 如果当前节点父节点是左孩子或者为null,则返回父节点
}
}
其实只要知道二叉树是如何找后继节点,这段代码就很好懂;
这里画蛇添足增加一下文案描述:
- 如果当前节点有右节点则找到右子树中最小的节点(最左下的节点)
- 如果当前节点没有右节点,那么其后继节有三种情况
2.1 :当前节点是父节点的右孩子,那么后继节点一定是当前节点的某一个祖先节点;一直往上找父,直到父是爷爷的左子树,那么返回爷爷,比较抽象,建议大家画个图,一目了然。
2.2 :当前节点是父节点的左孩子,那么后继节点就是父节点
2.3 :当前节点无后继节点,返回null
4. remove
public V remove(Object key) {
Entry<K,V> p = getEntry(key);
if (p == null)
return null;
V oldValue = p.value;
deleteEntry(p);
return oldValue;
}
调用getEntry获取要删除的节点;
如果存在那么调用deleteEntry方法删除,返回节点值。
deleteEntry:
/**
* Delete node p, and then rebalance the tree.
* 删掉p,然后平衡树
*/
private void deleteEntry(Entry<K,V> p) {
modCount++; // 变更记录+1
size--; // 大小-1
// If strictly internal, copy successor's element to p and then make p
// point to successor.
if (p.left != null && p.right != null) { // 删除的为叶子节点
Entry<K,V> s = successor(p); // 找到叶子节点的后继节点
p.key = s.key;
p.value = s.value;
p = s; // p指向后继节点
} // p has 2 children
// Start fixup at replacement node, if it exists.
Entry<K,V> replacement = (p.left != null ? p.left : p.right);
if (replacement != null) {
// Link replacement to parent
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;
// Null out links so they are OK to use by fixAfterDeletion.
p.left = p.right = p.parent = null;
// Fix replacement
if (p.color == BLACK)
fixAfterDeletion(replacement);
} else if (p.parent == null) { // return if we are the only node.
root = null;
} else { // No children. Use self as phantom replacement and unlink.
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;
}
}
}
删除这部分不太好理解,建议我们先在纸上自己画一下二叉树删除的几种情况:
- 叶子节点,这个容易处理,直接修改父节点的对应引用位置为null即可
- 只有一个孩子:在父节点和孩子节点之间建立连接。
- 有两个孩子:这个就复杂一点了,首先,找到后继节点,找到后替换当前节点为后继节点,然后再删除后继节点,因为后继节点肯定没有左孩子,这样就把两个孩子的情况转换为了前面两种情况(可以理解为把要删除的节点替换为后继节点,然后再处理后继节点下的树)
我们拆开来看:画图更好理解!!
- 两个子节点的情况, 把要删除的节点替换为后继节点,然后删除后继节点,转换为前两种情况
private void deleteEntry(Entry<K,V> p) {
modCount++; // 变更记录+1
size--; // 大小-1
// If strictly internal, copy successor's element to p and then make p
// point to successor.
if (p.left != null && p.right != null) { // 先处理有两个孩子节点的情况
Entry<K,V> s = successor(p); // 找到叶子节点的后继节点
p.key = s.key;
p.value = s.value;
p = s; // 替换当前节点为后继节点
} // p has 2 children
- 一个子节点的情况:
// Start fixup at replacement node, if it exists.
Entry<K,V> replacement = (p.left != null ? p.left : p.right); // p为待删除节点,找到替换p节点的孩子节点,找不到为null
if (replacement != null) { // 孩子节点不为null
// Link replacement to parent
replacement.parent = p.parent; // 把孩子节点的父节点改为要删除节点的父节点
if (p.parent == null) // 如果要删除节点的parent为null说明是根节点,那么把唯一的孩子设为根节点
root = replacement;
else if (p == p.parent.left) // 如果要删除节点是其父节点的左孩子,那么修改其父的左孩子为要删除节点的唯一子节点。
p.parent.left = replacement;
else // 要删除节点是右孩子
p.parent.right = replacement;
// Null out links so they are OK to use by fixAfterDeletion.
// 将要删除节点的左右和父都职位null,然后使用平衡方法fixAfterDeletion平衡该树
p.left = p.right = p.parent = null;
// Fix replacement
if (p.color == BLACK)
fixAfterDeletion(replacement);
} else if (p.parent == null) { // return if we are the only node.——没有孩子节点也没有父节点
- 最后一段: 无子节点——叶子节点的情况
} else if (p.parent == null) { // return if we are the only node.
// 无子节点,且父节点也为空,说明树只有一个节点,把根节点置空即可
root = null;
} else { // No children. Use self as phantom replacement and unlink.
// 没有子节点但是有父节点
if (p.color == BLACK)
fixAfterDeletion(p); // 重新平衡 树
if (p.parent != null) { // 重新平衡 树 后,要删除节点的父节点不为空
if (p == p.parent.left) // 如果要删除的节点为父的左节点,设置父的左孩子为null
p.parent.left = null;
else if (p == p.parent.right) 如果要删除的节点为父的右节点,设置父的右孩子为null
p.parent.right = null;
p.parent = null; // 因为是叶子节点,只把要删除节点的父置为null;
}
}
}
小结
与HashMap相比,TreeMap同样实现了Map接口,但内部使用红黑树实现。红黑树是统计效率比较高的大致平衡二叉树,这决定了它有如下特点:
- 按键有序,TreeMap同样实现了NavigableMap接口(该接口实现了SortedMap接口)可以方便地根据键的顺序进行查找,如第一个、最后一个、某一范围的键、邻近键等。
- 为了按键有序,TreeMap要求键实现Comparable接口或通过构造方法提供一个Comparator对象。
- 根据键保存、查找、删除的效率比较高,为O(h),h为树的高度,在树平衡的情况下,h为log(N),N为节点数。
应该使用TreeMap还是HashMap呢?不要求排序优先考虑HashMap,要求排序,考虑TreeMap。