一、概述
TreeMap类是一个有序的key-value的集合,与HashMap不同,TreeMap底层只有一个红黑树的结构(对红黑树不了解或者不熟悉的和觉得文字太枯燥的,推荐一个在线演示地址: https://rbtree.phpisfuture.com/ ),结点TreeMap类的内部类Entry,维护树结构;并且由于红黑树的特性,使得元素保存在TreeMap中默认是根据key值的自然顺序排序,也可以传入特定的比较器实例使TreeMap维持指定的顺序。所以,当你既想利用Map的高效查找特性,又想维持元素特定的顺序,那么你就需要用到TreeMap类。
二、源码分析
1. 类的声明
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
可以看到TreeMap类继承自AbstractMap类,并且实现了NavigableMap接口、Cloneable接口以及Serializable接口。具体如下:
a. 继承于AbstractMap,所以它是一个Map,即一个key-value集合。
b. 实现了NavigableMap接口,意味着它支持一系列的导航方法。比如返回有序的key集合。
c. 实现了Cloneable接口,意味着它能被克隆。
d. 实现了java.io.Serializable接口,意味着它支持序列化。
2. 成员变量
//一个比较器实例
private final Comparator<? super K> comparator;
//TreeMap根结点元素
private transient Entry<K,V> root;
//TreeMap中元素的数量
private transient int size = 0;
//修改计数,同于快速失败
private transient int modCount = 0;
//存放所有Entry的Set
private transient EntrySet entrySet;
//存放所有key的Set
private transient KeySet<K> navigableKeySet;
//定义一个初始化的视图
private transient NavigableMap<K,V> descendingMap;
//定义一个空对象
private static final Object UNBOUNDED = new Object();
//定义false来表示红色,用于维护红黑树结构
private static final boolean RED = false;
//定义true来表示红色,用于维护红黑树结构
private static final boolean BLACK = true;
//定义序列化标识id
private static final long serialVersionUID = 919286545866124006L;
3. 构造函数
// 默认构造函数。使用该构造函数,TreeMap中的元素按照自然排序进行排列。
public TreeMap() {
comparator = null;
}
// 指定Tree的比较器
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
//传入指定的Map转换为TreeMap,设置比较器为null
public TreeMap(Map<? extends K, ? extends V> m) {
comparator = null;
putAll(m);
}
//传入指定的SortedMap转换为TreeMap,设置比较器为SortedMap的比较器,也就是维持原来的排序规则
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) {
}
}
4. 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;
//用key,value和父结点构造一个Entry,默认为黑色
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;
}
//设置vlue值并且返回旧的value值
public V setValue(V value) {
V oldValue = this.value;
this.value = value;
return oldValue;
}
//重写了Map.Entry的equals()方法
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
}
//重写了Map.Entry的hashCode()方法
public int hashCode() {
int keyHash = (key==null ? 0 : key.hashCode());
int valueHash = (value==null ? 0 : value.hashCode());
return keyHash ^ valueHash;
}
public String toString() {
return key + "=" + value;
}
}
TreeMap类的内部类Entry类主要是拓展了Map.Entry类,增加左右结点以及父结点来维持树形结构,并且重写了equals()方法和hashCode()方法。
5.红黑树的特点
在进行方法源码分析前,我们先来根据标准红黑树结构,看一下红黑树的特点,也方便我们在学习源码的时候,能看懂那些步骤。
总结起来红黑树的特性为以下5点:
a. 每个结点只有两种颜色:红色和黑色。
b. 根结点是黑色的。
c. 每个叶子结点(NIL)都是黑色的空结点。
d. 从根结点到叶子结点,不会出现两个连续的红色结点。
e. 从任何一个结点出发,到叶子结点,这条路径上都有相同数目的黑色结点。
在下面的源码学习中,源码在维护红黑树结构时,只要不满足它的特性,就会调用调整方法进行调整,后面我们再具体分析。
6. putAll()方法
在构造函数中,我们看到调用了putAll()方法来进行初始化,那么我们看下源码时如何操作的。
public void putAll(Map<? extends K, ? extends V> map) {
int mapSize = map.size();
//判断map是否SortedMap,不是则采用AbstractMap的putAll
if (size==0 && mapSize!=0 && map instanceof SortedMap) {
Comparator<?> c = ((SortedMap<?,?>)map).comparator();
//同为null或者不为null,类型相同,则进入有序map的构造
if (c == comparator || (c != null && c.equals(comparator))) {
++modCount;
try {
buildFromSorted(mapSize, map.entrySet().iterator(),
null, null);
} catch (java.io.IOException cannotHappen) {
} catch (ClassNotFoundException cannotHappen) {
}
return;
}
}
//以上条件都不满足,则调用父类AbstractMap的putAll()方法,AbstractMap中的putAll()又会调用到TreeMap的put(),所以也能保证有序
super.putAll(map);
}
/**
* 根据排序进行红黑树结点的构建(与下面的方法同名,但是参数不同)
* size: map里键值对的数量
* it: 传入的map的entries迭代器
* str: 如果不为空,则从流里读取key-value
* defaultVal:见名知意,不为空,则value都用这个值
*/
private void buildFromSorted(int size, Iterator<?> it,
java.io.ObjectInputStream str,
V defaultVal)
throws java.io.IOException, ClassNotFoundException {
this.size = size;
root = buildFromSorted(0, 0, size-1, computeRedLevel(size),
it, str, defaultVal);
}
//计算包含sz个元素的二叉树的层级
private static int computeRedLevel(int sz) {
int level = 0;
for (int m = sz - 1; m >= 0; m = m / 2 - 1)
level++;
return level;
}
/**
* 构建红黑树结点
* level: 当前树的层数,注意:是从0层开始
* lo: 子树第一个元素的索引
* hi: 子树最后一个元素的索引
* redLevel: 红结点所在层数
* it: 传入的map的entries迭代器
* str: 如果不为空,则从流里读取key-value
* defaultVal:见名知意,不为空,则value都用这个值
*/
private final Entry<K,V> buildFromSorted(int level, int lo, int hi,
int redLevel,
Iterator<?> it,
java.io.ObjectInputStream str,
V defaultVal)
throws java.io.IOException, ClassNotFoundException {
//边界判断
if (hi < lo) return null;
//将子树的第一个元素索引和最后一个元素的索引加起来除以2,得出一个索引中间值mid
int mid = (lo + hi) >>> 1;
Entry<K,V> left = null;
//如果子树的第一个结点索引小于了父树的索引中间值,则递归调用构建左子树
if (lo < mid)
left = buildFromSorted(level+1, lo, mid - 1, redLevel,
it, str, defaultVal);
K key;
V value;
//如果参数迭代器不为null,则取出key和value值
if (it != null) {
if (defaultVal==null) {
Map.Entry<?,?> entry = (Map.Entry<?,?>)it.next();
key = (K)entry.getKey();
value = (V)entry.getValue();
} else {
key = (K)it.next();
value = defaultVal;
}
} else { //如果参数迭代器为Null,就从流中读取key和value
key = (K) str.readObject();
value = (defaultVal != null ? defaultVal : (V) str.readObject());
}
Entry<K,V> middle = new Entry<>(key, value, null);
//如果当前层级level就是红色结点的层级,则将当前的结点置为红色
if (level == redLevel)
middle.color = RED;
if (left != null) {
middle.left = left;
left.parent = middle;
}
//如果子树的最后一个结点索引大于了父树的索引中间值,则递归调用构建右子树
if (mid < hi) {
Entry<K,V> right = buildFromSorted(level+1, mid+1, hi, redLevel,
it, str, defaultVal);
middle.right = right;
right.parent = middle;
}
//返回当前Entry对象
return middle;
}
7. put()方法
public V put(K key, V value) {
//获取根结点t
Entry<K,V> t = root;
//判断是否根结点为null,则将传入的key和value组成的结点作为根结点
if (t == null) {
//compare()方法用于比较两个对象的大小,此处是检验key的类型包括null处理
compare(key, key);
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
//定义变量cmp存放当前结点插入的位置
int cmp;
//定义变量parent记录父结点
Entry<K,V> parent;
//获取比较器
Comparator<? super K> cpr = comparator;
if (cpr != null) {
//利用key值查找位置,因为红黑树是利用key值排序的
do {
parent = t;
cmp = cpr.compare(key, t.key);
//如果小于父结点parent的key,则往左子树找
if (cmp < 0)
t = t.left;
//如果大于,则往右子树找
else if (cmp > 0)
t = t.right;
//如果存在key值相同的结点,则替换旧值
else
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
//没有默认的比较器就获取key值的比较器,利用key实现的Comparable中的compareTo来参与比较。这样就保证了自定义比较器优先,我们甚至可以间接改变基本数据类型的比较方式。
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
//如果小于父结点parent的key,则往左子树找
if (cmp < 0)
t = t.left;
//如果大于,则往右子树找
else if (cmp > 0)
t = t.right;
//如果存在key值相同的结点,则替换旧值
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);
//元素数量+1
size++;
//修改标记+1
modCount++;
return null;
}
put()方法大致分为以下三步:
a. 判断根结点是否为空,如果为空,设置当前结点为根结点 。
b. 根据比较器(可能是外部传进来的,也可能是默认的),找到父结点。
c. 插入结点之后,调用fixAfterInsertion()方法进行修正。
8. fixAfterInsertion()方法
private void fixAfterInsertion(Entry<K,V> x) {
//设置结点的初始化颜色为红色
x.color = RED;
//如果当前结点x不为null,并且不为根结点root,并且当前结点的父结点是红色结点,则进行循环
while (x != null && x != root && x.parent.color == RED) {
//如果当前结点x的父结点是x的祖结点的左结点
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
//获取结点x的叔叔结点y,也就是x祖结点的右结点
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
//如果叔结点y是红色
if (colorOf(y) == RED) {
//设置x的父结点为黑色
setColor(parentOf(x), BLACK);
//设置x的叔结点y为黑色
setColor(y, BLACK);
//设置x的祖结点为红色
setColor(parentOf(parentOf(x)), RED);
//设置祖结点为x
x = parentOf(parentOf(x));
//如果叔结点y是黑色
} else {
//判断当前结点x是否是父结点的右结点
if (x == rightOf(parentOf(x))) {
//x的父结点作为x
x = parentOf(x);
//将原来的父结点进行左旋操作
rotateLeft(x);
}
//设置x的父结点为黑色
setColor(parentOf(x), BLACK);
//设置x的祖结点为红色
setColor(parentOf(parentOf(x)), RED);
//x的祖结点进行右旋操作
rotateRight(parentOf(parentOf(x)));
}
//如果当前结点x的父结点是x的祖结点的右结点
} else {
//获取x结点的祖结点的左结点,也就是x的叔叔结点赋值给y
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
//如果叔结点y是红色的
if (colorOf(y) == RED) {
//设置x结点的父结点为黑色
setColor(parentOf(x), BLACK);
//设置x结点的叔结点y为黑色
setColor(y, BLACK);
//设置x的祖结点为红色
setColor(parentOf(parentOf(x)), RED);
//获取x结点祖结点赋值给x
x = parentOf(parentOf(x));
//如果叔结点y是黑色的
} else {
//如果x结点是x父结点的左结点
if (x == leftOf(parentOf(x))) {
//取x结点的父结点赋值为x
x = parentOf(x);
//将x结点右旋
rotateRight(x);
}
//设置x结点的父结点为黑色
setColor(parentOf(x), BLACK);
//设置x结点的祖结点为红色
setColor(parentOf(parentOf(x)), RED);
//将x结点的祖结点进行左旋
rotateLeft(parentOf(parentOf(x)));
}
}
}
//始终让根结点保持黑色,这是红黑树的特性
root.color = BLACK;
}
红黑树的自我检验调整还是非常重要的一个过程,上述源码中我已经在每一行进行了注释,现在再详细归纳一遍(此处引用其他平台作者winner_0715的图文总结,原文地址: https://www.cnblogs.com/winner-0715/p/9738734.html ):
首先由代码可以看出,只有在当前插入结点的父结点是红色结点的时候,才会进行处理, 红黑树新节点的添加一定是红色节点,添加完新的节点之后会进行旋转操作以保持红黑树的特性。 如果添加的是黑色节点,那么就会导致根到叶子的路径上有一条路上,多一个额外的黑节点,这个是很难调整的;但是如果插入的是红色节点,只需要解决其父节点也为红色节点的这个冲突即可。 以N为新插入节点,P为其父节点,U为其父节点的兄弟节点,R为P和U的父亲节点进行分析。如果N的父节点为黑色节点,那直接添加新节点即可,没有产生冲突。如果出现P节点是红色节点,那便产生冲突,可以分为以下几种冲突:
(1) P为红色节点,且U也为红色节点,P不论是R的左节点还是右节点
将P和U接口变成黑色节点,R节点变成红色节点。修改之后如果R节点的父节点也是红色节点,那么在R节点上执行相同操作,形成了一个递归过程。如果R节点是根节点的话,那么直接把R节点修改成黑色节点。
(2) P为红色节点,U为黑色节点或缺少,且N是P的右节点、P是R的左节点 或者 P为红色节点,U为黑色节点或缺少,且N是P的左节点、P是R的右节点
这两种情况分别对P进行左旋和右旋操作。操作结果就变成了冲突3。 (总结起来就是左右变左左,右左变右右)
(3) P为红色节点,U为黑色节点或缺少,且N是P的左节点、P是R的左节点 或者 P为红色节点,U为黑色节点或缺少,且N是P的右节点、P是R的右节点
这两种情况分别对祖父R进行右旋和左旋操作。完美解决冲突。(总结起来就是左左祖右,右右祖左)
左旋与右旋找到了一张网络图片方便大家理解,如下:
通过上面put()方法的源码以及原理的解读,已经对维护红黑树有了深刻的理解,那么再起来看一下remove()方法又会发生哪些问题。
9. remove()方法
//移除键为key的元素
public V remove(Object key) {
//获取键为key的元素p
Entry<K,V> p = getEntry(key);
//如果p为null直接返回null
if (p == null)
return null;
//如果p不为null,获取p的value赋值给oldValue
V oldValue = p.value;
//执行删除操作
deleteEntry(p);
//返回旧的value值
return oldValue;
}
//执行删除操作的具体做法
private void deleteEntry(Entry<K,V> p) {
//修改标识+1
modCount++;
//元素数量-1
size--;
//如果当前删除p的左结点和右结点都不为null,也就是说p有两个子节点
if (p.left != null && p.right != null) {
//获取p结点的后继结点赋值为s(后继结点就是会顶替删除结点的位置的结点,一般是左子树的最大元素或者右子树的最小元素)
Entry<K,V> s = successor(p);
//后继结点的key和value赋值给删除结点p
p.key = s.key;
p.value = s.value;
//将后继结点s设置为要删除的结点p
p = s;
}
//找出替代节点replacement,左子树存在的话使用左结点,否则使用右结点。这个替代节点就是被删除节点的左子节点或右子节点
Entry<K,V> replacement = (p.left != null ? p.left : p.right);
//如果替代结点replacement存在
if (replacement != null) {
//将删除结点p的父结点作为替代结点replacement的父节点,也就是删除了p结点
replacement.parent = p.parent;
//如果删除结点p就是根节点,那么设置替代结点replacement为根节点
if (p.parent == null)
root = replacement;
//如果删除结点p是p的父节点的左节点,那么将p结点的父节点的左子树设置为替代结点replacement
else if (p == p.parent.left)
p.parent.left = replacement;
//否则,将p结点的父节点的右子树设置为替代结点replacement
else
p.parent.right = replacement;
//将删除结点p的左右以及父结点都设置为null
p.left = p.right = p.parent = null;
//如果删除结点p是黑色的,那么需要调用fixAfterDeletion()方法进行平衡调整
if (p.color == BLACK)
fixAfterDeletion(replacement);
//如果被删除结点p的父节点为空,说明被删除结点是根结点
} else if (p.parent == null) { // return if we are the only node.
//设置根节点为null
root = null;
//如果删除结点p没有子结点
} else {
//如果删除结点p是黑色的,那么需要调用fixAfterDeletion()方法进行平衡调整
if (p.color == BLACK)
fixAfterDeletion(p);
//如果删除结点p的父节点不为null
if (p.parent != null) {
//如果删除结点p是其父节点的左节点,就将父结点的左结点设置为null
if (p == p.parent.left)
p.parent.left = null;
//如果删除结点p是其父节点的右节点,就将父结点的右结点设置为null
else if (p == p.parent.right)
p.parent.right = null;
//设置删除结点p的父结点为null,以保证删除结点p的左右以及父结点都为null
p.parent = null;
}
}
}
删除操作分为2个步骤:
(1) 将红黑树当作一颗二叉查找树,将节点删除
(2) 通过”旋转和重新着色”等一系列来修正该树,使之重新成为一棵红黑树
步骤1的删除操作可分为几种情况:
(1) 删除节点没有儿子:先判断结点是否是黑色结点,如果是则需要进行平衡性调整,否则不用调整平衡性。然后再删除结点。
(2) 删除节点有1个儿子:删除该节点,并用该节点的儿子节点顶替它的位置。顶上来之后再进行平衡性调整。
(3) 删除节点有2个儿子:可以转成成删除节点只有1个儿子的情况,跟二叉查找树一样,找出节点的右子树的最小元素(或者左子树的最大元素,这种节点称为后继节点),并把它的值转移到删除节点,然后删除这个后继节点。这个后继节点最多只有1个子节点(如果有2个子节点,说明还能找出右子树更小的值),所以这样删除2个儿子的节点就演变成了删除没有儿子的节点和删除只有1个儿子的节点的情况
总的来说就是,没有儿子节点的情况下先进行调整,然后再删除节点,而有儿子节点的情况下,先把节点删除,删除之后儿子节点顶上来,然后再做平衡性调整。
删除节点只有1个儿子节点还分几种情况:
(1) 如果被删除的节点是红色节点,那说明它的父节点是黑色节点,儿子节点也是黑色节点,那么删除这个节点就不会影响红黑树的属性,直接使用它的黑色子节点代替它即可
(2) 如果被删除的节点是黑色节点,而它的儿子节点是红色节点。删除这个黑色节点之后,它的红色儿子节点顶替之后,会破坏性质5,只需要把儿子节点重绘为黑色节点即可,这样原先通过黑色删除节点的所有路径被现在的重绘后的儿子节点所代替
(3) 如果被删除的节点是黑色节点,而它的儿子节点也是黑色节点。这是一种复杂的情况,因为路径路过被删除节点的黑色节点路径少了1个,导致违反了性质5,所以需要对红黑树进行平衡调整。
为了方便理解,我们以N为删除节点的儿子节点(删除之后,处于新的位置上),它的兄弟节点为S,它们的父节点为P,Sl和Sr为S节点的左右子节点为例,进行讲解,其中N是父节点P的左子节点,如果N是父节点P的右子节点,做对称处理。针对上述第三种情况,又可以详细分为以下几种情况进行处理:
(3.1) N是新的根节点。这种情况下不用做任何处理,因为原先的节点也是一个根节点,相当于所有的路径都需要经过这个根节点,删除之后没有什么影响,而且新根也是黑色节点,符合所有特性,不需要进行调整
(3.2) S节点是红色节点,那么P节点,Sl,Sr节点是黑色节点。在这种情况下,对P节点进行左选操作并且交换P和S的颜色。完成这2个操作之后,所有路径上的黑色节点没有变化,但是N节点有了一个黑色兄弟节点Sl和一个红色的父亲节点P,左子树删除节点后还有存在着少1个黑色节点路径的问题。接下来按照N节点新的位置(兄弟节点S是个黑色节点,父节点P是个红色节点)进行(3.4)、(3.5)或(3.6)情况处理
(3.3) N的父亲节点P、兄弟节点S,还有S的两个子节点Sl,Sr均为黑色节点。在这种情况下,重绘S为红色。重绘之后路过S节点这边的路径跟N节点一样也少了一个黑色节点,但是出现了另外一个问题:不经过P节点的路径还是少了一个黑色节点。 接下来,要调整以P作为N递归调整树
(3.4) S和S的儿子节点Sl、Sr为黑色节点,但是N的父亲节点P为红色节点。在这种情况下,交换N的兄弟S与父亲P的颜色,颜色交换之后左子树多了1个黑色节点路径,刚好填补了左子树删除节点的少一个黑色节点路径的问题,而右子树的黑色路径没有改变,解决平衡问题
(3.5) S是黑色节点,S的左儿子节点Sl是红色,S的右儿子节点Sr是黑色节点。在这种情况下,在S上做右旋操作交换S和它新父亲的颜色。操作之后,左子树的黑色节点路径和右子树的黑色节点路径没有改变。但是现在N节点有了一个黑色的兄弟节点,黑色的兄弟节点有个红色的右儿子节点,满足了3.6的情况,按照(3.6)处理
(3.6) S是黑色节点,S的右儿子节点Sr为红色节点,S的左儿子Sl是黑色节点,P是红色或黑色节点。在这种情况下,N的父亲P做左旋操作,交换N父亲P和S的颜色,S的右子节点Sr变成黑色。这样操作以后,左子树的黑色路径+1,补了删除节点的黑色路径,右子树黑色路径不变,解决平衡问题
我们在了解了具体处理的做法后,再进行源码分析,就要简单很多啦,fixAfterDeletion()方法源码如下:
10. fixAfterDeletion()方法
// 删除节点后的平衡性调整,对应之前分析的节点昵称,N、S、P、Sl、Sr
private void fixAfterDeletion(Entry<K,V> x) {
while (x != root && colorOf(x) == BLACK) { // N节点是黑色节点并且不是根节点就一直循环
if (x == leftOf(parentOf(x))) { // 如果N是P的左子节点
Entry<K,V> sib = rightOf(parentOf(x)); // sib就是N节点的兄弟节点S
if (colorOf(sib) == RED) { // 如果S节点是红色节点,满足删除冲突3.2,对P节点进行左旋操作并交换P和S的颜色
// 交换P和S的颜色,S原先为红色,P原先为黑色(2个红色节点不能相连)
setColor(sib, BLACK); // S节点从红色变成黑色
setColor(parentOf(x), RED); // P节点从黑色变成红色
rotateLeft(parentOf(x)); // 删除冲突3.2中P节点进行左旋
sib = rightOf(parentOf(x)); // 左旋之后N节点有了一个黑色的兄弟节点和红色的父亲节点,S节点重新赋值成N节点现在的兄弟节点。接下来按照删除冲突3.4、3.5、3.6处理
}
// 执行到这里S节点一定是黑色节点,如果是红色节点,会按照冲突3.2交换成黑色节点
// 如果S节点的左右子节点Sl、Sr均为黑色节点并且S节点也为黑色节点
if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) {
// 按照删除冲突3.3和3.4进行处理
// 如果是冲突3.3,说明P节点也是黑色节点
// 如果是冲突3.4,说明P节点是红色节点,P节点和S节点需要交换颜色
// 3.3和3.4冲突的处理结果S节点都为红色节点,但是3.4冲突处理完毕之后直接结束,而3.3冲突处理完毕之后继续调整
setColor(sib, RED); // S节点变成红色节点,如果是3.4冲突需要交换颜色,N节点的颜色交换在跳出循环进行
x = parentOf(x); // N节点重新赋值成N节点的父节点P之后继续递归处理
} else { // S节点的2个子节点Sl,Sr中存在红色节点
if (colorOf(rightOf(sib)) == BLACK) { // 如果S节点的右子节点Sr为黑色节点,Sl为红色节点[Sl如果为黑色节点的话就在上一个if逻辑里处理了],满足删除冲突3.5
// 删除冲突3.5,对S节点做右旋操作,交换S和Sl的颜色,S变成红色节点,Sl变成黑色节点
setColor(leftOf(sib), BLACK); // Sl节点变成黑色节点
setColor(sib, RED); // S节点变成红色节点
rotateRight(sib); // S节点进行右旋操作
sib = rightOf(parentOf(x)); // S节点赋值现在N节点的兄弟节点
}
// 删除冲突3.5处理之后变成了删除冲突3.6或者一开始就是删除冲突3.6
// 删除冲突3.6,P节点做左旋操作,P节点和S接口交换颜色,Sr节点变成黑色
setColor(sib, colorOf(parentOf(x))); // S节点颜色变成P节点颜色,红色
setColor(parentOf(x), BLACK); // P节点变成S节点颜色,也就是黑色
setColor(rightOf(sib), BLACK); // Sr节点变成黑色
rotateLeft(parentOf(x)); // P节点做左旋操作
x = root; // 准备跳出循环
}
} else { // 如果N是P的右子节点,处理过程跟N是P的左子节点一样,左右对换即可
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); // 删除冲突3.4循环调出来之后N节点颜色设置为黑色 或者 删除节点只有1个红色子节点的时候,将顶上来的红色节点设置为黑色
}
11. getEntry()方法
final Entry<K,V> getEntry(Object key) {
//判断比较器是否为null,不为null就用比较器比较快速获得满足key的元素
if (comparator != null)
return getEntryUsingComparator(key);
//如果没有比较器,并且key为null,则直接抛出异常
if (key == null)
throw new NullPointerException();
//声明一个比较器k
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
//将root结点赋给p,也就是使得下面循环从根节点开始比较
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;
}
//根据原本的比较器规则快速查询满足key的元素,思路和上面声明一个比较器的比较方式相同
final Entry<K,V> getEntryUsingComparator(Object key) {
@SuppressWarnings("unchecked")
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;
}
getEntry()方法相比put()方法和remove()方法都简单不少,只需要比较关键字key即可,要查找的关键字比节点关键字要小的话找左节点,否则找右节点,一直递归操作,直到找到或找不到。
三、总结
本文主要是学习了TreeMap类的红黑树结构的重要方法。TreeMap类主要应用场景是在需要利用Map的高效查找特性,又想维持元素特定的顺序的时候,着重要掌握的就是红黑树的特性以及红黑树维护自身特性的方法过程,包括各种情况的判断以及左旋右旋对树结构的影响。
敬请期待《 我的jdk源码(二十):TreeSet类 》。
更多精彩内容,敬请扫描下方二维码,关注我的微信公众号【Java觉浅】,获取第一时间更新哦!
来源:oschina
链接:https://my.oschina.net/qq785482254/blog/4304425