概述
文章的内容基于JDK1.7进行分析,之所以选用这个版本,是因为1.8的有些类做了改动,增加了阅读的难度,虽然是1.7,但是对于1.8做了重大改动的内容,文章也会进行说明。
TreeMap实现了SotredMap接口,它是有序的集合。而且是一个红黑树结构,每个key-value都作为一个红黑树的节点。如果在调用TreeMap的构造函数时没有指定比较器,则根据key执行自然排序。这点会在接下来的代码中做说明,如果指定了比较器则按照比较器来进行排序。
数据结构
继承关系
1 | public class TreeMap<K,V> |
实现接口
1 | Serializable, Cloneable, Map<K,V>, NavigableMap<K,V>, SortedMap<K,V> |
基本属性
1 | private final Comparator<? super K> comparator; //比较器,是自然排序,还是定制排序 ,使用final修饰,表明一旦赋值便不允许改变 |
源码解析
由于TreeMap中源码较长,接下来将分段解析部分源码。既然是红黑树存储,肯定要有数据结构(Node)节点的。看一下TreeMap中关于节点的定义部分。
数据结构
1 | static final class Entry<K,V> implements Map.Entry<K,V> { |
构造方法
1 | //构造方法,comparator用键的顺序做比较 |
TreeMap提供了四个构造方法,实现了方法的重载。无参构造方法中比较器的值为null,采用自然排序的方法,如果指定了比较器则称之为定制排序.
- 自然排序:TreeMap的所有key必须实现Comparable接口,所有的key都是同一个类的对象
- 定制排序:创建TreeMap对象传入了一个Comparator对象,该对象负责对TreeMap中所有的key进行排序,采用定制排序不要求Map的key实现Comparable接口。等下面分析到比较方法的时候在分析这两种比较有何不同。
对于Map来说,使用的最多的就是put()/get()/remove()等方法,下面依次进行分析
put()
1 | public V put(K key, V value) { |
红黑树是一个更高效的检索二叉树,有如下特点:
- 每个节点只能是红色或者黑色
- 根节点永远是黑色的
- 所有的叶子的子节点都是空节点,并且都是黑色的
- 每个红色节点的两个子节点都是黑色的(不会有两个连续的红色节点)
- 从任一个节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点(叶子节点到根节点的黑色节点数量每条路径都相同)
上面的代码,详细的标注了每条语句的作用,但是我相信,如果你没有一定的功力,即使注释已经很详细了,你也会是一脸懵逼 ,二脸懵逼,全脑懵逼中,下面配合图片来梳理一下代码所表示的含义:
当一个默认为红色的节点插入树中,其实对应的是7中可能发生的情况,分别进行叙述:
- 情形1:新插入的节点时红黑树的根节点,没有父节点,无需任何的操作,直接将颜色设置为黑色就可以了
- 情形2:新节点的父节点颜色是黑色的,新插入的节点是红色的。也无需任何的操作。因为新节点的插入并没有影响到红黑书的特点
- 情形3:新节点的父节点(左孩子节点)颜色是红色的,而父节点的兄弟节点颜色也是红色的。那么情况就出现了,此时插入的节点就违反了红黑树的特点4 ,需要对红黑树进行调整。 操作看下图:
调整操作如上图,将父节点和父节点的兄弟节点,都修改为红色,然后将祖父节点修改为红色,因为修改了祖父节点的颜色,祖父节点可能会发生颜色的冲突,所以将新插入的节点修改为祖父节点,在进行调整。 - 情形4:父节点(左孩子节点)的颜色为红色,父节点的兄弟节点的颜色为黑色或者为null,新插入的节点为父节点的右孩子节点。如下图:
此时以父节点为旋转点,就新插入的节点进行左旋操作。便变成了情形5对应的情况,将执行情形5的操作 - 情形5:父节点(左孩子节点)的颜色为红色,父节点的兄弟节点颜色为黑色或者null,新插入节点为父亲的左孩子节点。如下图:
- 情形6 和情形7的操作与情形4和情形5的操作相同,它们之前的区别是父节点为有孩子节点,再次不再赘述。
remove()
1 | public V remove(Object key) { |
删除红黑树的操作比插入操作要稍微麻烦一点,分为两步:
- 以排序二叉树的方法删除指定节点。删除的节点存在三种情况:
- 被删除节点,没有左右孩子节点,直接删除即可
- 被删除节点,有一个孩子节点,那么让它的孩子节点指向它的父节点即可
- 本删除的节点,有两个非空的孩子节点,那么需要找到该节点的前驱或者后继节点,更换元素值,在将前驱或者后继节点删除(任意一个节点的前驱或者后继都必定至多有一个非空的子节点,可以按照前面的两种情形进行操作)
- 进行颜色的调换和树的旋转,满足红黑树的特征
下面来分情形讨论一下可能发生的情况:
- 情形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
总结
- 关于红黑树的节点插入操作,首先是改变新节点,新节点的父节点,祖父节点,和新节点的颜色,能在当前分支通过节点的旋转改变的,则通过此种操作,来满足红黑书的特点。
- 如果当前相关节点的旋转解决不了红黑树的冲突,则通过将红色的节点移动到根节点解决,最后在将根节点设置为黑色