TreeMap实现了SotredMap接口,它是有序的集合。而且是一个红黑树结构,每个key-value都作为一个红黑树的节点。如果在调用TreeMap的构造函数时没有指定比较器,则根据key执行自然排序,如果指定了比较器则按照比较器来进行排序。

一、数据结构

  1、继承关系

public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable {}

  2、实现接口

Serializable, Cloneable, Map<K,V>, NavigableMap<K,V>, SortedMap<K,V> 

  3、基本属性

private final Comparator<? super K> comparator;  //比较器,是自然排序,还是定制排序 ,使用final修饰,表明一旦赋值便不允许改变
private transient Entry<K,V> root = null; //红黑树的根节点
private transient int size = 0; //TreeMap中存放的键值对的数量
private transient int modCount = 0; //修改的次数

二、源码解析

  由于TreeMap中源码较长,接下来将分段解析部分源码。既然是红黑树存储,肯定要有数据结构(Node)节点的。看一下TreeMap中关于节点的定义部分。

  1、数据结构

static final class Entry<K,V> implements Map.Entry<K,V> {
K key; //键
V value; //值
Entry<K,V> left = null; //左孩子节点
Entry<K,V> right = null; //右孩子节点
Entry<K,V> parent; //父节点
boolean color = BLACK; //节点的颜色,在红黑树种,只有两种颜色,红色和黑色 //构造方法,用指定的key,value ,parent初始化,color默认为黑色
Entry(K key, V value, Entry<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
} //返回key
public K getKey() {
return key;
} //返回该节点对应的value
public V getValue() {
return value;
} //替换节点的值,并返回旧值
public V setValue(V value) {
V oldValue = this.value;
this.value = value;
return oldValue;
}
//重写equals()方法
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
//两个节点的key相等,value相等,这两个节点才相等
return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
}
//重写hashCode()方法
public int hashCode() {
int keyHash = (key==null ? 0 : key.hashCode());
int valueHash = (value==null ? 0 : value.hashCode());
//key和vale hash值得异或运算,相同则为零,不同则为1
return keyHash ^ valueHash;
}
//重写toString()方法
public String toString() {
return key + "=" + value;
}
}

  2、构造方法

//构造方法,comparator用键的顺序做比较
public TreeMap() {
comparator = null;
} //构造方法,提供比较器,用指定比较器排序
public TreeMap(Comparator<? super K> comparator) {
his.comparator = comparator;
} //将m中的元素转化daoTreeMap中,按照键的顺序做比较排序
public TreeMap(Map<? extends K, ? extends V> m) {
comparator = null;
putAll(m);
} //构造方法,指定的参数为SortedMap
//采用m的比较器排序
public TreeMap(SortedMap<K, ? extends V> m) {
comparator = m.comparator();
try {
buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
} catch (java.io.IOException cannotHappen) {
} catch (ClassNotFoundException cannotHappen) {
}
}

  TreeMap提供了四个构造方法,实现了方法的重载。无参构造方法中比较器的值为null,采用自然排序的方法,如果指定了比较器则称之为定制排序.

  • 自然排序:TreeMap的所有key必须实现Comparable接口,所有的key都是同一个类的对象
  • 定制排序:创建TreeMap对象传入了一个Comparator对象,该对象负责对TreeMap中所有的key进行排序,采用定制排序不要求Map的key实现Comparable接口。等下面分析到比较方法的时候在分析这两种比较有何不同。

  3、核心方法 

  对于Map来说,使用的最多的就是put()/get()/remove()等方法,下面依次进行分析

  3.1、put()

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; //size值加1
modCount++; //改变修改的次数
return null; //返回null
}
int cmp;
Entry<K,V> parent; //定义节点
// split comparator and comparable paths
Comparator<? super K> cpr = comparator; //获取比较器
if (cpr != null) { //如果定义了比较器,采用自定义比较器进行比较
do {
parent = t; //将红黑树根节点赋值给parent
cmp = cpr.compare(key, t.key); //比较key, 与根节点的大小
if (cmp < 0) //如果key < t.key , 指向左子树
t = t.left; //t = t.left , t == 它的做孩子节点
else if (cmp > 0)
t = t.right; //如果key > t.key , 指向它的右孩子节点
else
return t.setValue(value); //如果它们相等,替换key的值
} 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) // key < t.key
t = t.left; //左孩子
else if (cmp > 0) // key > t.key
t = t.right; //右孩子
else
return t.setValue(value); //t == t.key , 替换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;
}
//比较方法,如果comparator==null ,采用comparable.compartTo进行比较,否则采用指定比较器比较大小
final int compare(Object k1, Object k2) {
return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
: comparator.compare((K)k1, (K)k2);
} private void fixAfterInsertion(Entry<K,V> x) {
//插入的节点默认的颜色为红色
x.color = RED; //
//情形1: 新节点x 是树的根节点,没有父节点不需要任何操作
//情形2: 新节点x 的父节点颜色是黑色的,也不需要任何操作 while (x != null && x != root && x.parent.color == RED) {
//情形3:新节点x的父节点颜色是红色的
//判断x的节点的父节点位置,是否属于左孩子
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
//获取x节点的父节点的兄弟节点,上面语句已经判断出x节点的父节点为左孩子,所以直接取右孩子
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
//判断是否x节点的父节点的兄弟节点为红色。
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK); // x节点的父节点设置为黑色
setColor(y, BLACK); // y节点的颜色设置为黑色
setColor(parentOf(parentOf(x)), RED); // x.parent.parent设置为红色
x = parentOf(parentOf(x)); // x == x.parent.parent ,进行遍历。
} else {
//x的父节点的兄弟节点是黑色或者缺少的
if (x == rightOf(parentOf(x))) { //判断x节点是否为父节点的右孩子
x = parentOf(x); //x == 父节点
rotateLeft(x); //左旋转操作
}
//x节点是其父的左孩子
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED); //上面两句将x.parent 和x.parent.parent的颜色做调换
rotateRight(parentOf(parentOf(x))); //进行右旋转
}
} else {
Entry<K,V> y = leftOf(parentOf(parentOf(x))); //y 是x 节点的祖父节点的左孩子
if (colorOf(y) == RED) { //判断颜色
setColor(parentOf(x), BLACK); //父节点设置为黑色
setColor(y, BLACK); //父节点的兄弟节点设置为黑色
setColor(parentOf(parentOf(x)), RED); //祖父节点设置为红色
x = parentOf(parentOf(x)); //将祖父节点作为新插入的节点,遍历调整
} else {
if (x == leftOf(parentOf(x))) { //x 是其父亲的左孩子
x = parentOf(x);
rotateRight(x); //以父节点为旋转点,进行右旋操作
}
setColor(parentOf(x), BLACK); //父节点为设置为黑色
setColor(parentOf(parentOf(x)), RED); //祖父节点设置为红色
rotateLeft(parentOf(parentOf(x))); //以父节点为旋转点,进行左旋操作
}
}
}
root.color = BLACK; //通过节点位置的调整,最终将红色的节点条调换到了根节点的位置,根节点重新设置为黑色
}

  红黑树是一个更高效的检索二叉树,有如下特点:

  1. 每个节点只能是红色或者黑色
  2. 根节点永远是黑色的
  3. 所有的叶子的子节点都是空节点,并且都是黑色的
  4. 每个红色节点的两个子节点都是黑色的(不会有两个连续的红色节点)
  5. 从任一个节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点(叶子节点到根节点的黑色节点数量每条路径都相同)

  上面的代码,详细的标注了每条语句的作用,但是我相信,如果你没有一定的功力,即使注释已经很详细了,你也会是一脸懵逼 ,二脸懵逼,全脑懵逼中,下面配合图片来梳理一下代码所表示的含义:
  当一个默认为红色的节点插入树中,其实对应的是7中可能发生的情况,分别进行叙述:

    • 情形1:新插入的节点时红黑树的根节点,没有父节点,无需任何的操作,直接将颜色设置为黑色就可以了

    • 情形2:新节点的父节点颜色是黑色的,新插入的节点是红色的。也无需任何的操作。因为新节点的插入并没有影响到红黑书的特点

    • 情形3:新节点的父节点(左孩子节点)颜色是红色的,而父节点的兄弟节点颜色也是红色的。那么情况就出现了,此时插入的节点就违反了红黑树的特点4 ,需要对红黑树进行调整。 操作看下图:将父节点和父节点的兄弟节点,都修改为红色,然后将祖父节点修改为红色,因为修改了祖父节点的颜色,祖父节点可能会发生颜色的冲突,所以将新插入的节点修改为祖父节点,在进行调整。

                  

    • 情形4:父节点(左孩子节点)的颜色为红色,父节点的兄弟节点的颜色为黑色或者为null,新插入的节点为父节点的右孩子节点。如下图:此时以父节点为旋转点,就新插入的节点进行左旋操作。便变成了情形5对应的情况,将执行情形5的操作。

                   

    • 情形5:父节点(左孩子节点)的颜色为红色,父节点的兄弟节点颜色为黑色或者null,新插入节点为父亲的左孩子节点。如下图:

            

    • 情形6 和情形7的操作与情形4和情形5的操作相同,它们之前的区别是父节点为有孩子节点,再次不再赘述。

  3.2、remove()

public V remove(Object key) {
Entry<K,V> p = getEntry(key); //根据key查找节点,并返回该节点
if (p == null)
return null; V oldValue = p.value; //获取key对应的值
deleteEntry(p); //删除节点
return oldValue; //返回key对应的值
} final Entry<K,V> getEntry(Object key) {
//根据键寻找节点,有非为两种方式,如果定制了比较器,采用定制排序方式,否则使用自然排序
if (comparator != null)
return getEntryUsingComparator(key); //循环遍历树,寻找和key相等的节点
if (key == null)
throw new NullPointerException();
Comparable<? super K> k = (Comparable<? super K>) key;
Entry<K,V> p = root;
while (p != null) { //循环遍历树,寻找和key相等的节点
int cmp = k.compareTo(p.key);
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p;
}
return null;
}
//删除节点
private void deleteEntry(Entry<K,V> p) {
modCount++; //记录修改的次数
size--; //数量减1 //当前节点的两个孩子都不为空
if (p.left != null && p.right != null) {
//寻找继承者,继承者为当前节点的右孩子节点或者右孩子节点的最小左孩子
Entry<K,V> s = successor(p);
p.key = s.key; //key - value 的替换 ,并没有替换颜色
p.value = s.value;
p = s; //指向继承者
} // p has 2 children // Start fixup at replacement node, if it exists.
//开始修复树结构,继承者的左孩子不为空,返回左孩子,否则返回右孩子
//不可能存在左右两个孩子都存在的情况,successor寻找的就是最小节点,它的左孩子节点为null
Entry<K,V> replacement = (p.left != null ? p.left : p.right); if (replacement != null) {
// Link replacement to parent
//已经被选为继承者,当前拥有的一切放弃,所以将孩子交给爷爷抚养
replacement.parent = p.parent;
//p节点没有父节点,则指向根节点
if (p.parent == null)
root = replacement;
//如果p为左孩子,如果p为左孩子,则将p.parent.left = p.left
else if (p == p.parent.left)
p.parent.left = replacement;
else
p.parent.right = replacement; //删除p节点到左右分支,和父节点的引用
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;
}
}
}
private void fixAfterDeletion(Entry<K,V> x) {
//不是根节点,颜色为黑色,调整结构
while (x != root && colorOf(x) == BLACK) { //判断x是否为左孩子
if (x == leftOf(parentOf(x))) {
//x的兄弟节点
Entry<K,V> sib = rightOf(parentOf(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) {
setColor(sib, RED); //兄弟节点的两个孩子都是黑色的重新设置兄弟节点的颜色,修改为红色
x = parentOf(x); //将x定位到父节点
} else {
if (colorOf(rightOf(sib)) == BLACK) { //兄弟节点的右孩子是黑色的,左孩子是红色的
setColor(leftOf(sib), BLACK); //设置左孩子节点为黑色
setColor(sib, RED); //兄弟节点为红色
rotateRight(sib); //右旋
sib = rightOf(parentOf(x)); //右旋后重新设置兄弟节点
}
setColor(sib, colorOf(parentOf(x))); //兄弟节点颜色设置和父节点的颜色相同
setColor(parentOf(x), BLACK); //父节点设置为黑色
setColor(rightOf(sib), BLACK); //将兄弟节点的有孩子设置为黑色
rotateLeft(parentOf(x)); //左旋
x = root; //设置x为根节点
}
} else { // symmetric
//x为父节点的右节点,参考上面的操作
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:被删除节点为黑色,兄弟节点为红色,如下图:

          

   若删除上图中的x节点,将缺少一个黑节点,与红黑树的性质冲突,所以修改sib颜色为黑色,设置p节点为红色,并进行左旋操作。在进行后续的处理。

  • 情形3:被删除节点为黑色,x节点的兄弟节点的子节点都是黑色,如下图:
    x节点是黑色的,兄弟节点(黑色的)的子节点也是黑色的,p节点的颜色无法确定,有可能是红色的,也有可能是黑色的。如果是红色的直接设置为黑色即可,如果为黑色的,则需要将x定位的p节点,在进行处理。

                

  • 情形4:被删除节点为黑色,x的兄弟节点的右自子节点为黑色。如下图:
    情形4的调整为了转变成情形5的情况,来进行处理。

          

  • 情形5:被删除节点为黑色,x的兄弟节点右子节点为红色。如下图:
    sib的左子节点的颜色不确定,可能是红色也可能是黑色,但是对它并没有什么影响,因为变换前后它的上层分支的黑色节点数并没有改变。

          

  上面的情形只是针对删除的节点是左孩子的情况,进行的分析,被删除的节点也可能是右分支。情况完全相同只不过左右顺序发生了颠倒,不再进行复述。

  至此TreeMap中实现的最重要已经说完了。

  下面简单说一下一些方法的作用

  • firstEntry() 返回Map中最小的key
  • higherEntry(Object key ) 返回该Map中位于key后一位的key-value
  • lowerEntry(Object key ) 返回该Map中唯一key前一位的key-value
  • tailMap(Object key , boolean inclusive) 返回该Map的子Map

  3.3、总结

    • 关于红黑树的节点插入操作,首先是改变新节点,新节点的父节点,祖父节点,和新节点的颜色,能在当前分支通过节点的旋转改变的,则通过此种操作,来满足红黑书的特点。
    • 如果当前相关节点的旋转解决不了红黑树的冲突,则通过将红色的节点移动到根节点解决,最后在将根节点设置为黑色。

  若要查看查看源码全文解释,请查看博客:TreeMap源码实现类中文全解析

TreeMap核心源码实现解析的更多相关文章

  1. Java内存管理-掌握类加载器的核心源码和设计模式(六)

    勿在流沙筑高台,出来混迟早要还的. 做一个积极的人 编码.改bug.提升自己 我有一个乐园,面向编程,春暖花开! 上一篇文章介绍了类加载器分类以及类加载器的双亲委派模型,让我们能够从整体上对类加载器有 ...

  2. iOS 开源库系列 Aspects核心源码分析---面向切面编程之疯狂的 Aspects

    Aspects的源码学习,我学到的有几下几点 Objective-C Runtime 理解OC的消息分发机制 KVO中的指针交换技术 Block 在内存中的数据结构 const 的修饰区别 block ...

  3. HTTP流量神器Goreplay核心源码详解

    摘要:Goreplay 前称是 Gor,一个简单的 TCP/HTTP 流量录制及重放的工具,主要用 Go 语言编写. 本文分享自华为云社区<流量回放工具之 goreplay 核心源码分析> ...

  4. 手撕spring核心源码,彻底搞懂spring流程

    引子 十几年前,刚工作不久的程序员还能过着很轻松的日子.记得那时候公司里有些开发和测试的女孩子,经常有问题解决不了的,不管什么领域的问题找到我,我都能帮她们解决.但是那时候我没有主动学习技术的意识,只 ...

  5. Netty 学习(六):创建 NioEventLoopGroup 的核心源码说明

    Netty 学习(六):创建 NioEventLoopGroup 的核心源码说明 作者: Grey 原文地址: 博客园:Netty 学习(六):创建 NioEventLoopGroup 的核心源码说明 ...

  6. Android版数据结构与算法(五):LinkedHashMap核心源码彻底分析

    版权声明:本文出自汪磊的博客,未经作者允许禁止转载. 上一篇基于哈希表实现HashMap核心源码彻底分析 分析了HashMap的源码,主要分析了扩容机制,如果感兴趣的可以去看看,扩容机制那几行最难懂的 ...

  7. 并发编程之 SynchronousQueue 核心源码分析

    前言 SynchronousQueue 是一个普通用户不怎么常用的队列,通常在创建无界线程池(Executors.newCachedThreadPool())的时候使用,也就是那个非常危险的线程池 ^ ...

  8. Backbone事件机制核心源码(仅包含Events、Model模块)

    一.应用场景 为了改善酷版139邮箱的代码结构,引入backbone的事件机制,按照MVC的分层思想搭建酷版云邮局的代码框架.力求在保持酷版轻量级的基础上提高代码的可维护性.   二.遗留问题 1.b ...

  9. 6 手写Java LinkedHashMap 核心源码

    概述 LinkedHashMap是Java中常用的数据结构之一,安卓中的LruCache缓存,底层使用的就是LinkedHashMap,LRU(Least Recently Used)算法,即最近最少 ...

随机推荐

  1. Linux -- 管理锁争用(翻译)

    在多线程应用中,程序员会使用互斥锁(mutex)来同步线程进入可访问共享资源的代码区域的行为.受这些锁保护的代码区域被称为关键代码段(Critical Section).如果关键代码段中已存在一个线程 ...

  2. OpenStack社区中的GO语言之争

    1 背景介绍 Swift之前几乎所有的代码都是用Python实现的,但是性能一直不理想, 社区为了解决性能问题,尝试过很多方法,后来发现用Golang语言进行一部分代码重写, 性能得到了一定的提升,社 ...

  3. python学习:python的常用功能示例2

    1. python 写入txt with open("douban.txt","w") as f: f.write("这是个测试!") fi ...

  4. ABAP ole操作

    1.ole 如何保存和退出call method of sheetname 'saves' exporting #1 = filepath #2 = 1. call method of applica ...

  5. iOS-UIView的layoutSubviews和drawRect方法何时调用(转)

    转自:http://jianyu996.blog.163.com/blog/static/112114555201305113018814/ 首先两个方法都是异步执行.layoutSubviews方便 ...

  6. iOS-UIWebView去掉滚动条和黑色背景即拖拽后的上下阴影

    iOS UIWebView去掉滚动条和黑色背景即拖拽后的上下阴影 隐藏滚动条和上下滚动时出边界的后面的黑色的背景 webView.backgroundColor=[UIColor clearColor ...

  7. 前端添加视频流rtmp格式

    要求:rtmp格式, 在线直播 url地址 效果: 代码:初次打开时间较长, <!DOCTYPE html> <html> <head> <script ty ...

  8. 第一次打开PyCharm的基本操作(附图)

    第一次打开PyCharm可能需要修改一些个性化和了解一些基本操作,有助于接下来的学习过程.(后续可能会更新) 我的版本是64位的1.3 1.换界面皮肤 默认黑色的,不喜欢黑色皮肤可以换成白色的 Fil ...

  9. 逆天的flexbox布局

    Flexbox是spankin新推出的一种CSS布局模块,拥有完美的浏览器兼容性!它可以轻易做到垂直居中.重新排序.布局的动态伸展与收缩. Flexbox兼容性参考 点击查看基本教程介绍(请用电脑上的 ...

  10. TCP使用

    TCP使用注意事项总结   目录 发送或者接受数据过程中对端可能发生的情况汇总 本端TCP发送数据时对端进程已经崩溃 本端TCP发送数据时对端主机已经崩溃 本端TCP发送数据时对端主机已经关机 某个连 ...