文章目录
一、TreeMap概述
TreeMap是按照Key的排序结果组织内部结构的Map集合, 它改变了Map 类散乱无序的形象. 在 Key 有排序要求的场景下,使用TreeMap可以事半功倍. 在集合框架图中,ConcurrentHashMap、HashMap和TreeMap都继承于AbstractMap抽象类,他们的关系如图所示:
在TreeMap 的接口继承树中,有两个与众不同的接口: SortedMap 和 NavigableMap。SortedMap 接口表示它的Key 是有序不可重复的,支持获取头尾Key-Value 元素,或者根据Key指定范围获取子集合等。插入的Key必须实现Comparable 或提供额外的比较器Comparator,所以Key不允许为null,但是Value 可以。
二、TreeMap添加使用案例
NavigableMap 接口继承了 SortedMap 接口,根据指定的搜索条件返回最匹配的Key-Value 元素。不同于HashMap,TreeMap并非一定要覆写hashCode 和 equals 方法来达到key 去重的目的.
public class TreeMapRepeat {
public static void main(String[] args) {
// 如果仅把此处的TreeMap换成HashMap 的话,此处size=1;
TreeMap map = new TreeMap();
}
}
class Key implements Comparable<Key>{
@Override
// 返回负的常数,表示此对象永远小于输入的other对象,此处决定TreeMap 的 size=2
public int compareTo(Key other) {
return -1;
}
// hash是相等的
@Override
public int hashCode() {
return 1;
}
// equals 比较也是相等的
@Override
public boolean equals(Object obj) {
return true;
}
}
上述示例中的TreeMap 换成 HashMap, size 的结果则从2变成1. 注意HashMap 是使用hashCode 和 equals 实现去重的. 而TreeMap 依靠 Comparable 或Comparator 来实现Key的去重. 如果没有覆盖正确的方法,那么TreeMap 的最大特性将无法发挥出来, 甚至在运行时会出现异常. 如果要用TreeMap对Key进行排序,调用如下方法:
final int compare(Object k1, Object k2){
return comparator == null
? ((Comparable<? super K> k1).compareTo((K)k2))
: comparator.compara((K)k1, (K)k2);
}
如果Comparator 不为null,优先使用比较器 comparator 的compara 方法; 如果为null ,则使用Key 实现的自然排序 Comparable 接口的comparaTo 方法.如果两者都不满足,则抛出ClassCastException 异常.
三、红黑树实现TreeMap源码解析
基于红黑树实现的 TreeMap 提供了平均和最坏复杂度均为O(logn) 的增删改查操作,并且实现了NavigableMap 接口,该集合最大的特点是Key 的有序性. 先从类名和属性开始分析:
3.1 类名和属性源码解析
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
// 排序使用的比较器
private final Comparator<? super K> comparator;
// 根节点
private transient Entry<K,V> root;
...
// 定义成为有字面含义的常量
private static final boolean RED = false;
private static final boolean BLACK = true;
// TreeMap 的内部类,存储红黑树节点的载体类,在整个TreeMap 中高频出现
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;
...
}
...
}
TreeMap 通过put() 和deleteEntry() 实现红黑树的增加和删除节点操作,下面的源码分析以插入主流程为例,删除操作的主体流程与插入操作基本类似,在插入新节点之前,需要明确三个前提条件:
- 需要调整的新节点总是红色的.(插入元素的节点设置为红色)
- 如果插入新节点的父节点是黑色的,无须调整. 因为依然能符合红黑树的5个约束条件 .
- 如果插入新节点的父节点是红色的,因为红黑树规定不能出现相邻的两个红色节点,所以进入循环判断,或重新着色,或左右旋转,最终达到红黑树的五个约束条件,退出条件如下:
while(x != null && x != root && x.parent.color == RED){...}
如果是根节点,则直接退出,设置为黑色即可; 如果不是根节点,并且父节点为红色,会一直进行调整,直到退出循环 .TreeMap 的插入操作就是按Key 的对比往下遍历,大于比较节点值的向右走,小于比较节点值的向左走,先按照二叉查找树的特性进行操作,无须关心节点颜色与树的平衡,后续会重新着色和旋转,保持红黑树的特性.
3.2 put() 的源码解析
public V put(K key, V value){
// t表示当前节点,记住这个很重要! 先把TreeMap 的根节点root 引用赋值给当前节点
Entry<K,V> t = root;
// 如果当前节点为null,即是空树,新增的KV 形成的节点就是根节点
if(t == null){
// 看似多次一举,实际上预检了Key 是否可以比较
compara(key,key);
// 使用KV构造出新的Entry 对象,其中第三个参数是parent,根节点没有父节点
root = new Entry<>(key,value,null);
// 树的条目数
size = 1;
// 数结构修改的次数
modCount++;
return null;
}
// cmp 用来接收比较结果
int cmp;
Entry<K,V> parent;
// 构造方法中直入的外部比较器
Comparator<? super K> cpr = comparator;
// 重点步骤: 根据二叉查找树的特征,找到新节点插入的合适位置
if(cpr != null){
// 循环的目标: 根据参数key 与当前节点的key 不断地进行对比
do{
// 当前节点赋值给父节点,故从根节点开始遍历比较
parent = t;
// 比较输入的参数key 和当前节点key 的大小
cmp = cpr.compara(key, t.key);
// 参数的key更小,往左边走,把当前节点引用移动至它的左子节点上
if(cmp < 0)
t = t.left;
// 参数的key 更大,向右边走,把当前节点引用移动至它的右子节点上
else if(cmp > 0)
t = t.right;
// 如果相等,则会暴力的覆盖当前节点的value 值,并返回更新前的值
else
return t.setValue(value);
// 如果没有相等的key,一直会遍历到NIL 节点为止
} while(t !=null)
}else {
// 在没有置顶比较器的情况下,调用自然排序的Comparable 比较
if(key == null)
throw new NullPointerException();
Comparable<? super K> k = (Comparable<? super K>) key;
do{
parent = t;
cmp = k.comparaTo(t.key);
if(cmp <0)
t = t.left;
else if (cmp >0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
// 创建Entry对象,并把parent置入参数
Entry<K,V> e = new Entry<>(key, value, parent);
// 新节点找到自己的位置,原本以为可以安顿下来
if (cmp < 0)
// 如果比较结果小于0,则成为parent 的左孩子
parent.left = e;
else
// 如果比较结果大于0, 则成为parent 的右孩子
parent.right = e;
// 还需要对这个新节点进行重新着色和旋转操作,已达到平衡
fizAfterInsertion(e);
// 终于融入其中
size++;
modCount++;
// 成功插入新节点后,返回为null
return null;
}
如果一个新节点在插入时能够运行到fixAfterInsertion() 进行着色和旋转,说明: 第一,新节点加入之前是非空树; 第二, 新节点的Key 与任何节点都不相同. fixAfterInsertion()是插入节点后的动作,和删除节点操作中的fixAfterInsertion() 的原理基本相同,下例为新增节点为例分析.
3.3 fixAfterInsertion() 源码解析
private void fixAfterInsertion(Entry<K,V> x){
// 虽然内部类Entry 的属性color 默认为黑色,但新节点一律先赋值为红色
x.color = RED;
// 新节点是根节点或者父节点(简称为父亲) 为黑色,插入红色节点并不会破坏红黑树的性质,无须调整
// x值的改变已用红色高亮显示,改变的过程是在不断地向上游变量(或内部调整),直到父亲为黑色,或者到达根节点
while (x != null && x != root && x.parent.color == RED){
// 如果父亲是其父节点(简称为爷爷)的左子节点
if (parentOf(x) == leftOf(parentOf(parentOf(x)))){
// 这时,得看爷爷的右子节点(简称为右叔)的脸色
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
// 如果右叔是红色,此时通过局部颜色调整,就可以使子树继续满足红黑树的性质
if (colorOf(y) == RED){ // (第1处)
// 父亲设置为黑色
setColor(parentOf(x), BLACK);
// 右叔设置为黑色
setColor(y, BLACK);
// 爷爷设置为红色
setColor(parentOf(parentOf(x)), RED);
// 爷爷成为新的节点,进入到下一轮循环
x = parentOf(parentOf(x));
// 如果右叔是黑色,则需要加入旋转
}else {
// 如果x是父亲的右子节点,先对父亲做一次左旋转操作,转化x 是父亲的左子节点的情形
if(x == rightOf(parentOf(x))){
// 对父亲做一次左旋转操作,红色的父亲会沉入其左侧位置, 将父亲赋值给x
x = parentOf(x);
rotateLeft(x);
}
// 重新着色并对爷爷进行右旋操作
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
}else {
// 与上方代码相反,如果父亲是爷爷的右子节点, 则看左树的脸色,原理相同
...
}
}
root.color = BLACK;
}
在上方源码中,第1处出现的colorOf() 方法返回节点颜色. 调整后的根节点必然是黑色的; 叶子节点可能是黑色的,也可能使红色的;叶子节点下挂的两个虚节点即NIL 节点必然是黑色的,下方源码中的p==null 时,返回为BLACK ,这些都是红黑树的重要性质.
private static <K,V> boolean colorOf(Entry<K,V> p){
return (p == null ? BLACK : p.color);
}
3.4 rotateLift()左旋源码解析
左旋和右旋的代码基本类似,结合后面的旋转示意图,输入参数为失去平衡的那棵子树的根节点
private void rotateLeft(Entry<K,V> p){
// 如果参数节点不是NIL 节点
if(p != null){
// 获取p的右子节点 r
Entry<K,V> r = p.right;
// 将r的左子树设置为p 的右子树
p.right = r.left;
// 若r的左子树不为空,则将p设置为r左子树的父亲
if(r.left != null)
r.left.parent = p;
// 将p的父亲设置r的父亲
r.parent = p.parent;
// 无论如何,r 都要在p父亲心目中替代p的位置
if (p.parent == null)
root = t;
else if(p.parent.left == p)
p.parent.left = r;
else
p.parent.right = r;
// 将p设置为r的左子树,将r设置为p的父亲
r.left = p;
p.parent = r;
}
}
3.5 红黑树的平衡策略的实例
构造一个自然排序的Treemap 对象,插入, 删除数据的示例代码如下:
TreeMap<Integer, String> treeMap = new TreeMap<Integer, String>();
treeMap.put(55, "fifty-five");
treeMap.put(56,"fifty-six");
treeMap.put(57, "fifty-seven");
treeMap.put(58, "fifty-eight");
treeMap.remove(57);
treeMap.put(59, "fifty-nine");
为什么在 58 和 59 之间插入 83 和删除 57 呢? 是因为需要构造这样的场景: 旋转两次(先右旋,再左旋) .
- 第一步:
如上图所示,先分析55、56、57三个数的插入操作。图中55在插入时是空树,它就是根节点,根据红黑树约束条件,根节点必须是黑色的,将节点55涂黑。继续插入节点56与57,新节点的颜色设置为红色。当插入56时,由于父亲是黑色节点,不做任何调整;当插入57时,由于父节点是红色的,出现两个连续红色节点,需要重新着色,并却旋转。完成之后如上图(e)所示. - 第二步:
如图2所示,再分析节点58 的插入操作,父亲57 是爷爷56 的右节点,左叔55 为红色.这时把父亲和左叔同时涂黑,把爷爷56 设置为红色. 因为爷爷56 是根节点,退出循环,最后一句代码时root.color=BLACK, 重新把56 涂黑.完成之后如图2 ©所示
-
第三步:
如图3 所示,再分析节点83 的插入操作.根据自然排序结果,从根节点56 开始比较,比56 大,比57 大,比58大,所以放置在58的右子节点上,在重新调整平衡时,父亲58是爷爷57的右子节点,左叔不存在,认为是黑色NIL。这时把父亲颜色涂黑,把爷爷设置为红色。此时,爷爷57为失去平衡的那棵小树(57/58/83)的根节点,将它作为输入参数,进行左旋操作. 完成之后如图© 所示. -
第四步
删除57,因为节点57 没有任何子节点,也非根节点,本身又是红色,不影响红黑树性质,直接删除即可. -
第五步
如图4所示,删除57之后,在分析59的插入操作.根据自然排序结果,从根节点56 开始比较,比56大、比58大、比83小,放置在83 的左子节点上。对于59,只有满足如下条件,才会进入右旋转操作: -
父亲是爷爷的右子节点;
-
当前节点是父亲的左子节点;
-
左叔是黑色的 (删除57的原因所在)。
右旋之后,把59涂黑,把58置为红色,然后以58为输入参数,进入左旋操作,完成之后如图4 (d)所示。
在树的演化过程中,插入节点的过程中,如果需要重新着色或旋转,存在三种情形,如图2和图4所示:
- 节点的父亲是红色,叔叔是红色的,则
重新着色
。 - 节点的父亲是红色,叔叔是黑色的,而新节点是父亲的左节点:
进行右旋
。 - 节点的父亲是红色,叔叔是黑色的,而新节点是父亲的右节点:
进行左旋
。
如图4,在旋转时,箭头方向的引出端均为红色。插入55、56、58,删除57均并没有引起树的旋转调整。红黑树相比AVL 树,任何不平衡都能在3 次旋转之内调整完成。每次向上回溯的步长是2,对于频繁插入和删除的场景,红黑树的优势是非常明显的。
总结
总体来说,TreeMap的时间复杂度比HashMap 要高一些,但是合理利用好TreeMap集合的有序性和稳定性,以及支持范围查找的特性,往往在数据排序的场景中特别高效。另外,TreeMap是线程不安全的集合,不能在多线程之间进行共享数据的写操作。在多线程进行写操作时,需要添加互斥机制,或者把对象放在Collections.synchronizedMap(treeMap) 中实现同步 .
在JDK7之后的HashMap、TreeSet、ConcurrentHashMap,也使用红黑树的方式管理节点。如果只是对单个元素进行排序,使用TreeSet即可。TreeSet底层其实就是TreeMap,Value共享使用一个静态Object对象,如下源码所示:
private static final Object PRESENT = new Object();
public boolean add(E e){
return treeMap.put(e, PRESENT) == null;
}
点击了解HashMap详细分析
数据结构与集合(八) — Map类集合之(HashMap)
数据结构与集合(八) — Map类集合之(ConcurrentHashMap)