作者:炸鸡可乐

原文出处:www.pzblog.cn

一、摘要

在集合系列的第一章,咱们了解到,Map的实现类有HashMap、LinkedHashMap、TreeMap、IdentityHashMap、WeakHashMap、Hashtable、Properties等等。

本文主要从数据结构和算法层面,探讨TreeMap的实现。

二、简介

Java TreeMap实现了SortedMap接口,也就是说会按照key的大小顺序对Map中的元素进行排序,key大小的评判可以通过其本身的自然顺序(natural ordering),也可以通过构造时传入的比较器(Comparator)。

TreeMap底层通过红黑树(Red-Black tree)实现,所以要了解TreeMap就必须对红黑树有一定的了解,在《集合系列》文章中,如果你已经读过红黑树的讲解,其实本文要讲解的TreeMap,跟其大同小异。

红黑树又称红-黑二叉树,它首先是一颗二叉树,它具有二叉树所有的特性。同时红黑树更是一颗自平衡的排序二叉树。

对于一棵有效的红黑树二叉树,主要有以下规则:

  • 1、每个节点要么是红色,要么是黑色,但根节点永远是黑色的;
  • 2、每个红色节点的两个子节点一定都是黑色;
  • 3、红色节点不能连续(也即是,红色节点的孩子和父亲都不能是红色);
  • 4、从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点;
  • 5、所有的叶节点都是是黑色的(注意这里说叶子节点其实是上图中的 NIL 节点);

这些约束强制了红黑树的关键性质:从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这棵树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。所以红黑树它是复杂而高效的,其检索效率O(log n)。下图为一颗典型的红黑二叉树。

在树的结构发生改变时(插入或者删除操作),往往会破坏上述条件3或条件4,需要通过调整使得查找树重新满足红黑树的条件。

调整方式主要有:左旋、右旋和颜色转换!

2.1、左旋

左旋的过程是将x的右子树绕x逆时针旋转,使得x的右子树成为x的父亲,同时修改相关节点的引用。旋转之后,二叉查找树的属性仍然满足。

2.2、右旋

右旋的过程是将x的左子树绕x顺时针旋转,使得x的左子树成为x的父亲,同时修改相关节点的引用。旋转之后,二叉查找树的属性仍然满足。

2.3、颜色转换

颜色转换的过程是将红色节点转换为黑色节点,或者将黑色节点转换为红色节点,以满足红黑树二叉树的规则!

三、常用方法介绍

3.1、get方法

get方法根据指定的key值返回对应的value,该方法调用了getEntry(Object key)得到相应的entry,然后返回entry.value

算法思想是根据key的自然顺序(或者比较器顺序)对二叉查找树进行查找,直到找到满足k.compareTo(p.key) == 0entry

源码如下:

final Entry<K,V> getEntry(Object key) {
//如果传入比较器,通过getEntryUsingComparator方法获取元素
if (comparator != null)
return getEntryUsingComparator(key);
//不允许key值为null
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;
}

如果传入比较器,通过getEntryUsingComparator方法获取元素

final Entry<K,V> getEntryUsingComparator(Object key) {
K k = (K) key;
Comparator<? super K> cpr = comparator;
if (cpr != null) {
Entry<K,V> p = root;
while (p != null) {
//通过比较器顺序,获取元素
int cmp = cpr.compare(k, p.key);
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p;
}
}
return null;
}

测试用例:

public static void main(String[] args) {
Map initMap = new TreeMap();
initMap.put("4", "d");
initMap.put("3", "c");
initMap.put("1", "a");
initMap.put("2", "b");
//默认自然排序,key为升序
System.out.println("默认 排序结果:" + initMap.toString()); //自定义排序,在TreeMap初始化阶段传入Comparator 内部对象
Map comparatorMap = new TreeMap<String, String>(new Comparator<String>() { @Override
public int compare(String o1, String o2){
//根据key比较大小,采用倒叙,以大到小排序
return o2.compareTo(o1);
}
});
comparatorMap.put("4", "d");
comparatorMap.put("3", "c");
comparatorMap.put("1", "a");
comparatorMap.put("2", "b"); System.out.println("自定义 排序结果:" + comparatorMap.toString());
}

输出结果:

默认 排序结果:{1=a, 2=b, 3=c, 4=d}
自定义 排序结果:{4=d, 3=c, 2=b, 1=a}

3.2、put方法

put方法是将指定的key, value对添加到map里。该方法首先会对map做一次查找,看是否包含该元组,如果已经包含则直接返回,查找过程类似于getEntry()方法;如果没有找到则会在红黑树中插入新的entry,如果插入之后破坏了红黑树的约束,还需要进行调整(旋转,改变某些节点的颜色)。

源码如下:

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();
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
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(Entry<K,V> x)

private void fixAfterInsertion(Entry<K,V> x) {
x.color = RED;
while (x != null && x != root && x.parent.color == RED) {
//判断x是否在树的左边,还是右边
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
//如果x的父亲的父亲的右子树是红色,违反了红色节点不能连续
if (colorOf(y) == RED) {
//进行颜色调整,以满足红色节点不能连续规则
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
//如果x的父亲的右子树等于自己,那么需要进行左旋转
if (x == rightOf(parentOf(x))) {
x = parentOf(x);
rotateLeft(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
} else {
//跟上面的流程正好相反
//获取x的父亲的父亲的左子树节点
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
//如果y是红色节点,违反了红色节点不能连续
if (colorOf(y) == RED) {
//进行颜色转换
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
//如果x的父亲的左子树等于自己,那么需要进行右旋转
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}
//根节点一定为黑色
root.color = BLACK;
}

上述代码的插入流程:

  • 1、首先在红黑树上找到合适的位置;
  • 2、然后创建新的entry并插入;
  • 3、通过函数fixAfterInsertion(),对某些节点进行旋转、改变某些节点的颜色,进行调整;

调整图解:

3.3、remove方法

remove的作用是删除key值对应的entry,该方法首先通过上文中提到的getEntry(Object key)方法找到 key 值对应的 entry,然后调用deleteEntry(Entry<K,V> entry)删除对应的 entry。由于删除操作会改变红黑树的结构,有可能破坏红黑树的约束,因此有可能要进行调整。

源码如下:

public V remove(Object key) {
Entry<K,V> p = getEntry(key);
if (p == null)
return null; V oldValue = p.value;
deleteEntry(p);
return oldValue;
}

删除函数 deleteEntry()

private void deleteEntry(Entry<K,V> p) {
modCount++;
size--;
if (p.left != null && p.right != null) {// 删除点p的左右子树都非空。
Entry<K,V> s = successor(p);// 后继
p.key = s.key;
p.value = s.value;
p = s;
}
Entry<K,V> replacement = (p.left != null ? p.left : p.right);
if (replacement != null) {// 删除点p只有一棵子树非空。
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.parent == null) {
root = null;
} else { //删除点p的左右子树都为空
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;
}
}
}

删除后调整函数fixAfterDeletion()的具体代码如下:

private void fixAfterDeletion(Entry<K,V> x) {
while (x != root && colorOf(x) == BLACK) {
//判断当前删除的元素,是在x父亲的左边还是右边
if (x == leftOf(parentOf(x))) {
Entry<K,V> sib = rightOf(parentOf(x));
//判断x的父亲的右子树,是红色还是黑色节点
if (colorOf(sib) == RED) {
//进行颜色转换
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateLeft(parentOf(x));
sib = rightOf(parentOf(x));
}
//x的父亲的右子树的左边是黑色节点,右边也是黑色节点
if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) {
//设置x的父亲的右子树为红色节点,将x的父亲赋值给x
setColor(sib, RED);
x = parentOf(x);
} else {
//x的父亲的右子树的左边是红色节点,右边也是黑色节点
if (colorOf(rightOf(sib)) == BLACK) {
//x的父亲的右子树的左边进行颜色调整,右旋调整
setColor(leftOf(sib), BLACK);
setColor(sib, RED);
rotateRight(sib);
sib = rightOf(parentOf(x));
}
//对x进行左旋,颜色调整
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(rightOf(sib), BLACK);
rotateLeft(parentOf(x));
x = root;
}
} else { // 跟前四种情况对称
Entry<K,V> sib = leftOf(parentOf(x));
if (colorOf(sib) == RED) {
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateRight(parentOf(x));
sib = leftOf(parentOf(x));
}
if (colorOf(rightOf(sib)) == BLACK &&
colorOf(leftOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
if (colorOf(leftOf(sib)) == BLACK) {
setColor(rightOf(sib), BLACK);
setColor(sib, RED);
rotateLeft(sib);
sib = leftOf(parentOf(x));
}
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(leftOf(sib), BLACK);
rotateRight(parentOf(x));
x = root;
}
}
}
setColor(x, BLACK);
}

上述代码的删除流程:

  • 1、首先在红黑树上找到合适的位置;
  • 2、然后删除entry;
  • 3、通过函数fixAfterDeletion(),对某些节点进行旋转、改变某些节点的颜色,进行调整;

四、总结

TreeMap 默认是按键值的升序排序,如果需要自定义排序,可以通过new Comparator构造参数,重写compare方法,进行自定义比较。

以上,主要是对 java 集合中的 TreeMap 做了写讲解,如果有理解不当之处,欢迎指正。

五、参考

1、JDK1.7&JDK1.8 源码

2、博客园 - chenssy - TreeMap分析

2、知乎 - CarpenterLee - TreeMap讲解

【集合系列】- 深入浅出的分析TreeMap的更多相关文章

  1. 【集合系列】- 深入浅出的分析 Hashtable

    一.摘要 在集合系列的第一章,咱们了解到,Map 的实现类有 HashMap.LinkedHashMap.TreeMap.IdentityHashMap.WeakHashMap.Hashtable.P ...

  2. 【集合系列】- 深入浅出分析HashMap

    一.摘要 在集合系列的第一章,咱们了解到,Map的实现类有HashMap.LinkedHashMap.TreeMap.IdentityHashMap.WeakHashMap.Hashtable.Pro ...

  3. 【集合系列】- 深入浅出的分析 Set集合

    一.摘要 关于 Set 接口,在实际开发中,其实很少用到,但是如果你出去面试,它可能依然是一个绕不开的话题. 言归正传,废话咱们也不多说了,相信使用过 Set 集合类的朋友都知道,Set集合的特点主要 ...

  4. 【集合系列】- 深入浅出分析LinkedHashMap

    一.摘要 在集合系列的第一章,咱们了解到,Map的实现类有HashMap.LinkedHashMap.TreeMap.IdentityHashMap.WeakHashMap.Hashtable.Pro ...

  5. 【集合系列】- 深入浅出的分析IdentityHashMap

    一.摘要 在集合系列的第一章,咱们了解到,Map 的实现类有 HashMap.LinkedHashMap.TreeMap.IdentityHashMap.WeakHashMap.Hashtable.P ...

  6. 【集合系列】- 深入浅出的分析 WeakHashMap

    一.摘要 在集合系列的第一章,咱们了解到,Map 的实现类有 HashMap.LinkedHashMap.TreeMap.IdentityHashMap.WeakHashMap.Hashtable.P ...

  7. 【集合系列】- 深入浅出的分析 Properties

    一.摘要 在集合系列的第一章,咱们了解到,Map 的实现类有 HashMap.LinkedHashMap.TreeMap.IdentityHashMap.WeakHashMap.Hashtable.P ...

  8. 【集合系列】- 深入浅出分析 ArrayDeque

    一.摘要 在 jdk1.5 中,新增了 Queue 接口,代表一种队列集合的实现,咱们继续来聊聊 java 集合体系中的 Queue 接口. Queue 接口是由大名鼎鼎的 Doug Lea 创建,中 ...

  9. Java 集合系列 12 TreeMap

    java 集合系列目录: Java 集合系列 01 总体框架 Java 集合系列 02 Collection架构 Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例 Java ...

随机推荐

  1. ‎Cocos2d-x 学习笔记(13) ActionEase

    ActionEase是ActionInterval的子类,能够变速执行动作.把内部动作进行了包装,通过传进update的time,加上不同的计算公式,算出新的time,作为内部动作的time,实现了内 ...

  2. java-i++的原理探究

    先看例子: package com.test; publick class Auto{ public static void main(String[] args){ Auto inc=new Aut ...

  3. 剑指Offer(十九)——顺时针打印矩阵

    题目描述 输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字. 例如,如果输入如下4 X 4矩阵: 1   2    3     4 5   6    7     8 9   10  11  ...

  4. 使用Jersey构建图片服务器

    使用Jersey构建图片服务器 前台页面代码 <form id="jvForm" action="add.do" method="post&qu ...

  5. Opentracing + Uber Jaeger 全链路灰度调用链,Nepxion Discovery

    当网关和服务在实施全链路分布式灰度发布和路由时候,我们需要一款追踪系统来监控网关和服务走的是哪个灰度组,哪个灰度版本,哪个灰度区域,甚至监控从Http Header头部全程传递的灰度规则和路由策略.这 ...

  6. Redis备忘(一)

    hash: 渐进式rehash:同时查询新旧两个hash,然后在后续定时任务以及hash的子指令中,循序渐进将旧的迁移到新的hash表 Redis应用: 1.分布式锁: 实现1:setnx+expir ...

  7. Kafka权威指南阅读笔记(第六章)

    Broker配置 Kafka可以同时拥有可靠的主题和非可靠的主题.非可靠的主题允许丢失. 复制系数 主题级别的配置参数是 replication.factor,在Broker级别则可以通过defaul ...

  8. 存储路径与文件目录操作ZT

    转自:https://www.cnblogs.com/zrr-notes/p/5953445.html (一)基本存储位置 我们的app在手机中存放的路径是:/var/mobile/Applicati ...

  9. Flask解析(二):Flask-Sqlalchemy与多线程、多进程

    Sqlalchemy flask-sqlalchemy的session是线程安全的,但在多进程环境下,要确保派生子进程时,父进程不存在任何的数据库连接,可以通过调用db.get_engine(app= ...

  10. 暑期集训20190730 取模(mod)

    [题目描述] 给定一个长度为n的非负整数序列a,你需要支持以下操作: 1:给定l,r,输出a[l]+a[l+1]+…+a[r]. 2:给定l,r,x,将a[l],a[l+1],…,a[r]对x取模. ...