TreeMap是Map接口的一种实现,一种基于红黑树的实现。但TreeMap并没有直接实现Map接口。TreeMap根据其键的自然顺序或在创建映射时提供的Comparator进行排序,这取决于使用的是哪个构造函数。TreeMap内部对元素的操作复杂度为O(logn)。虽然在元素的存取方面TreeMap并不占优,但是它内部的元素都是排序的,当需要查找某些元素以及顺序输出元素的时候它能够带来比较理想的结果。
一、概述
TreeMap为以下情况提供了有保证的log(n)时间成本:
containsKey
get
put
remove
算法是对Cormen,Leiserson和Rivest的“算法简介”中的算法的改编。
TreeMap实现未同步,如果有多个线程同时访问一个TreeMap,如果至少存在一个线程在结构上修改了此map,则必须在外部同步。(结构修改是任何添加或删除一个或多个映射的操作;仅仅更改与实例已经包含的键关联的值不是结构修改)。这通常是通过对一些自然封装映射的对象进行同步来完成的。
如果不存在这样的对象,则应使用Collections.synchronizedSortedMap方法“包装”map。 最好在创建时完成此操作,以防止意外不同步地访问map:
SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...));
这个类的所有“集合视图方法”返回的迭代器都是fail-fast;如果在迭代器创建之后,以任何方式在结构上修改映射,除了通过迭代器自己的remove方法,迭代器将抛出ConcurrentModificationException。因此,面对并发修改,迭代器会快速而干净地失败,而不是在将来某个不确定的时间冒险进行任意的、不确定的行为。
请注意,迭代器的fail-fast行为不能得到保证,因为一般来说,在非同步并发修改的情况下不可能做出任何硬保证。fail-fast迭代器以最大的努力抛出ConcurrentModificationException。因此,编写一个依赖于这个异常的正确性的程序是错误的:迭代器的fail-fast行为应该只用于检测bug。
此类中的方法返回的所有Map.Entry对及其视图均表示生成映射时的快照。他们不支持Entry.setValue方法(不过请注意,可以使用put更改关联map中的映射)。
该类是Java集合框架的成员。
二、继承结构
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
TreeMap同样继承了AbstractMap抽象类,此类是Map接口的骨干实现。同时还实现了NavigableMap、Cloneable和Serializable接口。
下面来看NavigableMap接口:
public interface NavigableMap<K,V> extends SortedMap<K,V>
可以看到NavigableMap接口实际上继承了SortedMap接口:
public interface SortedMap<K,V> extends Map<K,V>
最终SortedMap接口继承了Map接口。
NavigableMap接口从名字上来看,有导航的意思。扩展了导航方法的SortedMap返回给定搜索目标的最接近匹配项。(比如lowerEntry,floorEntry,ceilingEntry,higherEntry等方法)
SortedMap接口就非常好理解了,为了排序的,提供其键的总体排序的Map。
实现Cloneable和Serializable接口,分别对应克隆和序列化操作。
三、详细设计
AbstractMap在前面的文章《AbstractMap作为Map接口的骨干实现是一种怎样的存在?》中进行了详细的分析,下面就不在具体分析。
3.1 SortedMap
SortedMap接口为了排序的,提供其键的总体排序的Map。接下来看提供了那些接口方法等着实现者去实现。
SortedMap是根据其键的可比自然排序来排序的,或者通过通常在排序的映射创建时提供的Comparator来排序的。 遍历排序后的map的集合视图(由entrySet、keySet和 values方法返回)时,将反映此顺序。
下面的代码中包含注释,说明了这些方法的用途。
public interface SortedMap<K,V> extends Map<K,V> {
/**
* 返回用于在此映射中对键进行排序的比较器;
* 如果此映射使用其键的可比自然排序,则返回null。
*/
Comparator<? super K> comparator();
/**
* 返回此map的部分视图,其键值范围从fromKey(含)到toKey(不包含)
* (如果fromKey和toKey相等,则返回的映射为空)。
* 返回map中的更改将反映在该map中,反之亦然。
* 返回的map支持该map支持的所有可选map操作。
*
* 尝试插入超出其范围的键时,返回的map将抛出IllegalArgumentException。
*/
SortedMap<K,V> subMap(K fromKey, K toKey);
/**
* 返回此map的部分视图,其键严格小于toKey(不包含)。
* 返回map中的更改将反映在该map中,反之亦然。
* 返回的map支持该map支持的所有可选map操作。
*
* 尝试插入超出其范围的键时,返回的map将抛出IllegalArgumentException。
*/
SortedMap<K,V> headMap(K toKey);
/**
* 返回此map的部分视图,其键大于或等于fromKey(包含)。
* 返回map中的更改将反映在该map中,反之亦然。
* 返回的map支持该map支持的所有可选map操作。
*
* 尝试插入超出其范围的键时,返回的map将抛出IllegalArgumentException。
*/
SortedMap<K,V> tailMap(K fromKey);
/**
* 返回此映射中当前的第一个(最低)键。
*/
K firstKey();
/**
* 返回此映射中当前的最后一个(最高)键。
*/
K lastKey();
/**
* 返回此map中包含键的Set视图。set的迭代器以升序返回键。
*
* 该set由该map支持,因此对map的更改会反映在set中,反之亦然。
* 如果在对set进行迭代时修改了map(通过迭代器自己的remove操作除外),
* 则迭代的结果是不确定的。
* 该set支持元素删除,从map中删除相应的映射。
* 该元素删除通过Iterator.remove、Set.remove、removeAll、retainAll和clear操作。
* 它不支持add或addAll操作。
*/
Set<K> keySet();
/**
* 返回此map中包含的值的Collection视图。集合的迭代器按相应键的升序返回值。
*
* 该collection由该map支持,因此对map的更改会反映在collection中,反之亦然。
* 如果在对collection进行迭代时修改了map(通过迭代器自己的remove操作除外),
* 则迭代的结果是不确定的。
* 该collection支持元素删除,从map中删除相应的映射。
* 该元素删除通过Iterator.remove、Collection.remove、removeAll、retainAll和clear操作。
* 它不支持add或addAll操作。
*/
Collection<V> values();
/**
* 返回此map中包含映射的Set视图。set的迭代器以键升序返回条目。
*
* 该set由该map支持,因此对map的更改会反映在set中,反之亦然。
* 如果在对set进行迭代时修改了map
*(通过迭代器自己的remove操作或通过迭代器返回的映射条目上的setValue操作除外),
* 则迭代结果未定义。
* 该set支持元素删除,从map中删除相应的映射。
* 该元素删除通过Iterator.remove、Set.remove、removeAll、retainAll和clear操作。
* 它不支持add或addAll操作。
*/
Set<Map.Entry<K, V>> entrySet();
}
3.2 NavigableMap
NavigableMap是一个SortedMap扩展了导航方法的Map接口,返回与指定搜索目标最接近的匹配项。
public interface NavigableMap<K,V> extends SortedMap<K,V> {
/**
* 返回严格小于给定键的最大键-值映射关系;如果没有这样的键,则返回null。
*/
Map.Entry<K,V> lowerEntry(K key);
/**
* 返回严格小于给定键的最大键;如果没有这样的键,则返回null。
*/
K lowerKey(K key);
/**
* 返回小于或等于给定键的最大键关联的键值映射,如果没有这样的键,则返回null。
*/
Map.Entry<K,V> floorEntry(K key);
/**
* 返回小于或等于给定键的最大键;如果没有这样的键,则返回null。
*/
K floorKey(K key);
/**
* 返回大于或等于给定键的最小键关联的键值映射,如果没有这样的键,则返回null。
*/
Map.Entry<K,V> ceilingEntry(K key);
/**
* 返回大于或等于给定键的最小键;如果没有这样的键,则返回null。
*/
K ceilingKey(K key);
/**
* 返回至少大于给定键的最小键关联的键-值映射关系;如果没有这样的键,则返回null。
*/
Map.Entry<K,V> higherEntry(K key);
/**
* 返回严格大于给定键的最小键;如果没有这样的键,则返回null。
*/
K higherKey(K key);
/**
* 返回此映射中的最小键关联的键-值映射;如果映射为空,则返回null。
*/
Map.Entry<K,V> firstEntry();
/**
* 返回此映射中的最大键关联的键值映射,如果映射为空,则返回null。
*/
Map.Entry<K,V> lastEntry();
/**
* 删除并返回与此映射中的最小键关联的键-值映射,如果映射为空,则返回null。
*/
Map.Entry<K,V> pollFirstEntry();
/**
* 删除并返回与此映射中的最大键关联的键值映射,如果映射为空,则返回null。
*/
Map.Entry<K,V> pollLastEntry();
/**
* descend有下降的意思,从接口函数命名可知,它是返回逆序map的。
*
* 返回此映射中包含的映射的逆序视图。
* 降序map由该map支持,因此对map的更改会反映在降序map中,反之亦然。
* 如果在对映射的集合视图进行迭代时修改了映射
* (通过迭代器自己的remove操作除外),则迭代的结果是不确定的。
*/
NavigableMap<K,V> descendingMap();
/**
* 返回此映射中包含的键的NavigableSet视图。
* set的迭代器以升序返回键。
*
* 该set由map支持,因此对map的更改会反映在set中,反之亦然。
* 如果在对set进行迭代时修改了映射(通过迭代器自己的remove操作除外),则迭代的结果是不确定的。
* 该set支持元素删除,该元素删除通过Iterator.remove,Set.remove,removeAll,retainAll和clear操作。
* 它不支持add或addAll操作。
*/
NavigableSet<K> navigableKeySet();
/**
* 返回此映射中包含的键的反向顺序NavigableSet视图。
* set的迭代器以降序返回键。
*
* 该set由map支持,因此对map的更改会反映在set中,反之亦然。
* 如果在对set进行迭代时修改了映射(通过迭代器自己的remove操作除外),则迭代的结果是不确定的。
* 该set支持元素删除,该元素删除通过Iterator.remove,Set.remove,removeAll,retainAll和clear操作。
* 它不支持add或addAll操作。
*/
NavigableSet<K> descendingKeySet();
/**
* 返回此map的部分视图,其键范围为fromKey到toKey。
* 如果fromKey和toKey相等,除非fromInclusive和toInclusive都为true,否则返回的映射为空。
* 返回的map受此map支持,因此返回的map中的更改会反映在此map中,反之亦然。
* 返回的map支持该map支持的所有可选map操作。
*
* 返回的map将抛出IllegalArgumentException,以尝试在其范围之外插入一个键,
* 或者构造一个子map,其端点不在其范围内。
*/
NavigableMap<K,V> subMap(K fromKey, boolean fromInclusive,
K toKey, boolean toInclusive);
/**
* 返回此map的部分视图,该视图的键小于(或等于,如果inclusive为true)toKey。
*
* 返回的map受此map支持,因此返回的map中的更改会反映在此map中,反之亦然。
* 返回的map支持该map支持的所有可选map操作。
*
* 返回的map将抛出IllegalArgumentException,以尝试在其范围之外插入一个键,
* 或者构造一个子map,其端点不在其范围内。
*/
NavigableMap<K,V> headMap(K toKey, boolean inclusive);
/**
* 返回此map的部分视图,该视图的键大于(或等于,如果inclusive为true)fromKey。
*
* 返回的map受此map支持,因此返回的map中的更改会反映在此map中,反之亦然。
* 返回的map支持该map支持的所有可选map操作。
*
* 返回的map将抛出IllegalArgumentException,以尝试在其范围之外插入一个键,
* 或者构造一个子map,其端点不在其范围内。
*/
NavigableMap<K,V> tailMap(K fromKey, boolean inclusive);
/**
* 等效于subMap(fromKey, true, toKey, false)
*/
SortedMap<K,V> subMap(K fromKey, K toKey);
/**
* 等效于headMap(toKey, false)
*/
SortedMap<K,V> headMap(K toKey);
/**
* 等效于tailMap(fromKey, true)
*/
SortedMap<K,V> tailMap(K fromKey);
}
3.3 TreeMap
接下来我们来分析主角TreeMap的具体实现,先从构造函数入手。
3.3.1 构造函数
下面是构造函数的源码,总共有四个构造函数。分别是无参数、带有比较器Comparator入参和通过其他map初始化的两种构造方式。
public TreeMap() {
comparator = null;
}
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
public TreeMap(Map<? extends K, ? extends V> m) {
comparator = null;
putAll(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) {
}
}
1.无参构造函数,这个函数comparator置为null,说明使用自然排序的方式组织map;
2.带有比较器Comparator入参构造函数,显而易见通过自定义比较器的方式组织map;
3.Map入参构造函数,此函数首先将comparator置为null,然后是通过putAll函数转化的;
下面是putAll函数的源码,不难看出如果map没有实现SortedMap接口,直接调用super.putAll(map),这实际上调用的是AbstractMap中的putAll函数,这个函数遍历map并插入到TreeMap中。如果map实现了SortedMap接口,并且map的size不等于0(mapSize!=0),而且map的比较器为null,那么就会增加修改次数(modCount),最后会调用buildFromSorted函数。
public void putAll(Map<? extends K, ? extends V> map) {
int mapSize = map.size();
if (size==0 && mapSize!=0 && map instanceof SortedMap) {
Comparator<?> c = ((SortedMap<?,?>)map).comparator();
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;
}
}
super.putAll(map);
}
4.SortedMap入参构造函数,此函数首先将comparator置为SortedMap对应的comparator,然后直接调用buildFromSorted。
接下来是时候揭开buildFromSorted函数的“神秘面纱”了。
3.3.2 buildFromSorted函数
这个函数在两个构造函数都有被调用。根据源码详细的注释先来了解一番。
根据排序的数据线性时间创建树。 可以接受来自迭代器或流的键 和/或 值。 这会导致参数过多,但似乎比替代方法要好。 此方法接受的四种格式是:
1)Map.Entries的迭代器(it != null, defaultVal == null)
2)键的迭代器(it != null, defaultVal != null)
3)交替序列化的键和值的流(it == null, defaultVal == null)
4)序列化键流(it == null, defaultVal != null)
假定在调用此方法之前已经设置了TreeMap的比较器。
----要从迭代器或流读取的键(或键-值对)的数量
----如果为非空,则从迭代器读取条目或键创建新条目
----如果非空,则从键创建新条目,并且可能以序列化形式从此流中读取值。it和str中之一应该为非空值
----如果非空,则此默认值用于映射中的每个值。 如果为空,则如上所述,从迭代器或流中读取每个值
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);
}
内部实际上调用的是一个buildFromSorted函数的重载版本。
递归的“帮助方法”,它可以完成前一方法的实际工作。相同名称的参数具有相同的定义。其他参数记录在下面。假定在调用此方法之前已经设置了TreeMap的比较器和size字段(它忽略两个字段)。
当前树的级别。初始调用应为0;
此子树的第一个元素索引。初始值应为0;
该子树的最后一个元素索引。初始值应为 size-1;
节点应为红色的级别。对于此大小的树,必须等于computeRedLevel。
再来看computeRedLevel方法。此函数的作用是找到将所有节点分配为BLACK的级别,可以理解为计算红色节点的层级(它的作用是用来计算完全二叉树的层数)。
private static int computeRedLevel(int sz) {
int level = 0;
for (int m = sz - 1; m >= 0; m = m / 2 - 1)
level++;
return level;
}
把根结点索引看为0,那么高度为2的树的最后一个节点的索引为2,类推高度为3的最后一个节点为6,满足 m = (m + 1) * 2 ,如果逆向求m, m = m / 2 - 1。那么计算这个高度有什么用呢,如上图,如果一个树有9个节点,那么构造红黑树的时候,只要把前面3层的结点都设置为黑色,第四层的节点设置为红色,则构造完的树,就是红黑树。
@SuppressWarnings("unchecked")
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 {
/*
* 根是最中间的元素。
* 为此,我们必须首先递归构造整个左子树,以获取其所有元素。
* 然后我们可以继续右子树。
*
* lo和hi参数是要从当前子树的迭代器或流中提取的最小和最大索引。
* 它们实际上没有被索引,我们只是按顺序进行,以确保按相应顺序提取项目。
*/
if (hi < lo) return null;
// 右移1位实际上就是除以2
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;
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 { // 使用流
key = (K) str.readObject();
value = (defaultVal != null ? defaultVal : (V) str.readObject());
}
Entry<K,V> middle = new Entry<>(key, value, null);
// color nodes in non-full bottommost level red 如果当前递归的层数与 RedLevel 相等,这些结点为红色
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;
}
return middle;
}
3.3.3 put方法
put方法自然作为增删查改一环中的“增”,实际上就是向红黑树中增加节点。我们知道红黑树中增加一个节点(最终都是在底部插入新的节点),可能会引起树不再平衡,因此就需要重新平衡。
public V put(K key, V value) {
Entry<K,V> t = root;
// 如果root节点为空说明此map为空,因此用当前的key和value组成一个节点,并赋值给root节点
if (t == null) {
compare(key, key); // 类型(可能为空)检查
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// 下面的if else 结构很好理解,就是在红黑树中查找待插入位置,最后调用fixAfterInsertion修复由于插入打破平衡。
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();
@SuppressWarnings("unchecked")
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<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
3.3.4 fixAfterInsertion方法
fixAfterInsertion方法通过方法名就知道它的作用了,修复插入后引起的不平衡。
/** From CLR */
private void fixAfterInsertion(Entry<K,V> x) {
x.color = RED;
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) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == rightOf(parentOf(x))) {
x = parentOf(x);
rotateLeft(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
} else {
Entry<K,V> y = leftOf(parentOf(parentOf(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 = parentOf(x);
rotateRight(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}
root.color = BLACK;
}
观察上面的实现代码,核心调整过程都包括在了while循环里了,里面的if else结构又包含了四种情况,下面逐一具体分析。对了,分析前还有一事,把里面涉及到的一些简单函数谈一下。
1.parentOf ----获取节点的复节点
2.leftOf----获取节点的左孩子节点
3.rightOf----获取节点的右孩子节点
4.setColor----设置节点颜色
5.colorOf----获取节点颜色
6.rotateLeft----左旋
7.rotateRight----右旋
首先我们将插入的 节点设置为红色,接下来检查它的父节点颜色是否也是红色?如果是,则红黑树平衡被打破,需要调整,分为四种情况。
情况一
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
插入11
11 < 13,所以从根节点开始的左子树查找,可以结合上面的put方法代码分析
将11插入到12节点的左孩子,满足代码中情况一的条件:
1.自己为红节点
2.父节点为红节点
3.叔叔节点(祖父节点的右孩子)为红节点
调整父节点和叔叔节点为黑色,祖父节点为红色,保证从根节点出发到达最底部叶子节点所经过的黑节点数相同。
最后一步,由于红黑树的性质决定root节点为黑色,因此将其染黑。
root.color = BLACK;
情况二
} else {
if (x == rightOf(parentOf(x))) {
x = parentOf(x);
rotateLeft(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
分析上面这段代码,因为下图中
并不是右孩子节点(相对于其父节点),所以 x == rightOf(parentOf(x)) 不符合条件,直接执行下面的两个setColor 和 rotateRight方法。
下面两幅图就是右旋
右旋后对节点颜色进行调整
这和代码中的执行顺序稍微有点出入,后调整颜色和先调整颜色都是可以的。
我们看到这个片段满足前面if结构的话,就会先进行左旋。实际上就是插入的时候作为右孩子!
接下来的两幅图就是左旋,左旋以后我们看到又变成了我们熟悉的结构,接下来需要右旋调整了。
情况三
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
前两种情况
是右孩子,现在就来看
是左孩子的情况。当然现在 parentOf(x) == leftOf(parentOf(parentOf(x))) 这一条件为 false,说明此时 parentOf(x) == rightOf(parentOf(parentOf(x))) 为 true。
这里同样需要将root节点设置为黑色。
root.color = BLACK;
情况四
} else {
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
这里是最后一种情况了,还和分析情况三一样,我们先忽略其中的if结构。代码还是先设置
父节点的颜色为黑色,设置其祖父节点为红色,最后以其祖父节点左旋。
接下来考虑if结构里右旋的情况,如果
作为左孩子插入就会满足这个条件。
下面的两幅图就是右旋,右旋结束之后,回到上面熟悉的结构,再来个左旋和调整颜色就算大功告成了。
最后思考一个问题,代码中while循环中必须满足 x.parent.color == RED 条件,如果不满足就不需要调整了,为什么?
因为如果父节点是黑色的,那么其增加红色叶子节点后不影响平衡(从跟节点出发到新增的当前节点经过的黑色节点数量并没有变化)。
3.3.5 remove方法
上面详细的分析了新增节点的方法,接下来分析删除的方法。
public V remove(Object key) {
Entry<K,V> p = getEntry(key);
if (p == null)
return null;
V oldValue = p.value;
deleteEntry(p);
return oldValue;
}
上面的代码非常短,首先我们调用了getEntry方法(关于查找的方法后面分析)获取对应的节点,判断是否为null,如果为null直接返回null,否则调用deleteEntry方法进行删除,最后返回key对应的oldValue。所以说重头戏实际上就是deleteEntry方法。
/**
* 删除节点p,然后重新平衡树。
*/
private void deleteEntry(Entry<K,V> p) {
modCount++;
size--;
// 如果严格在内部,则将后继元素复制到p,然后将p指向后继。
if (p.left != null && p.right != null) {
Entry<K,V> s = successor(p);
p.key = s.key;
p.value = s.value;
p = s;
} // p有2个孩子
// 在替换节点(如果存在)上启动修复。
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的指针置为空,方便fixAfterDeletion函数使用
p.left = p.right = p.parent = null;
// 修复替换节点
if (p.color == BLACK)
fixAfterDeletion(replacement);
} else if (p.parent == null) { // 如果我们是唯一的节点,则返回。
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;
}
}
}
上面的代码先遇到函数successor,先来分析一下它的具体实现。
/**
* 返回指定Entry的后继者;如果没有,则返回null。
*/
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
if (t == null)
return null;
else if (t.right != null) {
Entry<K,V> p = t.right;
while (p.left != null)
p = p.left;
return p;
} else {
Entry<K,V> p = t.parent;
Entry<K,V> ch = t;
while (p != null && ch == p.right) {
ch = p;
p = p.parent;
}
return p;
}
}
所谓后继节点就是指找到第一个比当前节点大的节点,在中序遍历的时候排在当前节点之后,分为三种情况。
1.当前节点为null,直接就返回null;
2.当前节点存在右孩子,那么右孩子下面的子树中最小的节点一定是这颗子树的最下层左叶子节点;
3.当前节点右孩子为null,那么我们就要去找它的父节点,前提条件是当前节点一定位于左子树中,这样找到的第一个沿着父、祖父、曾祖父…的节点,就是满足要求的后继节点。
再去看deleteEntry方法,如果P的左孩子和右孩子都不为null,那么将后继元素复制到p,然后将p指向后继。接下来看后面代码的if else结构对删除节点的正真修复。
情况一
p节点的子节点有一个不为NULL,此时replacement就等于p节点不等于NULL的孩子节点。
1.replacement等于p节点的左孩子
首先将p节点的父节点赋值给replacement的父节点指针,当满足 p == p.parent.right 时, p.parent.right = replacement ,然后将p节点的指针(left、right和parent)都置为NULL,最后p的颜色如果为黑色调用fixAfterDeletion函数在replacement节点上进行修复。
2.replacement等于p节点的右孩子
同样将p节点的父节点赋值给replacement的父节点指针,当满足 p == p.parent.left 时, p.parent.left = replacement ,然后将p节点的指针(left、right和parent)都置为NULL,最后p的颜色如果为黑色调用fixAfterDeletion函数在replacement节点上进行修复。
不论replacement等于p节点的左孩子或者是右孩子,如果p的父节点为NULL,那么一定说明p节点就是之前的root节点,删除了之前的root节点,所以需要将replacement节点提升为root节点。然后将p节点的指针(left、right和parent)都置为NULL,最后p的颜色一定是黑色的,调用fixAfterDeletion函数在replacement节点上进行修复。
情况二
p节点的子节点都为NULL,此时replacement就等于NULL。
1.p的父节点为NULL,说明p是root节点,而且p的子节点都为NULL,所以p节点是这颗红黑树的唯一节点,把root节点删除了,直接赋值 root = null,意味着红黑树为空了。
2.p的父节点不为NULL
(1) 如果p的颜色为黑,删除一个黑节点可能会造成不平衡(少了黑节点),所以调用fixAfterDeletion§重新平衡。
(2) 如果p的颜色为红,删除一个红节点不会影响这条路径上黑节点的个数,因此不需要调用fixAfterDeletion§。
在以上 (1) 和 (2) 两种情况下,我们都需要断开和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;
}
如果p为左孩子节点,则将p.parent.left置为null;同理,如果p为右孩子节点,则将p.parent.right置为null;最后将p.parent置为null。
情况三
p节点的子节点都不为NULL,这个时候先需要找到p的后继节点。并用后继节点替换当前要删除的节点,接下来代码执行情况和 情况一、情况二相同。
if (p.left != null && p.right != null) {
Entry<K,V> s = successor(p);
p.key = s.key;
p.value = s.value;
p = s;
}
下面我们看一种简单的删除情况,从树中删除节点12。首先12节点的左右孩子都存在,所以就去找后继节点,发现后继节点是20,用20替换12节点,最后将后继节点之前的链接断开。
3.3.6 fixAfterDeletion方法
这个方法的作用是修复删除节点以后造成的不平衡,在deleteEntry有两处调用了此方法。我们可以看到这个方法里面的if else情况有点多,下面分别标记一下八种情况。
private void fixAfterDeletion(Entry<K,V> x) {
while (x != root && colorOf(x) == BLACK) {
if (x == leftOf(parentOf(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));
}
if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) { //情况二
setColor(sib, RED);
x = parentOf(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;
}
} else { // symmetric 对称的
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);
}
情况一
当x是左黑色节点,兄弟节点sib是红色节点,需要兄弟节点由红转黑,父节点由黑转红,绕父节点左旋,左旋后树的结构变化了,这时重新赋值sib,这个时候sib指向了x的兄弟节点。
经过一系列操作后,并没有结束,而是可能到了情况2,或者情况3和4。
情况二
节点x、x的兄弟节点sib、sib的左子节点和右子节点都为黑色时,需要将该节点sib由黑变红,同时将x指向当前x的父节点,最后将节点x设置为黑色。
情况三
节点x、x的兄弟节点sib、sib的右子节点都为黑色,sib的左子节点为红色时,需要将sib左子节点设置为黑色,sib节点设置为红色,同时绕sib右旋,再将sib指向x的兄弟节点。接下来进入情况4。
情况四
节点x、x的兄弟节点sib都为黑色,而sib的左右子节点都为红色或者右子节点为红色、左子节点为黑色,此时需要将sib节点的颜色设置成和x的父节点p相同的颜色,设置x的父节点颜色为黑色,设置sib右孩子的颜色为黑色,左旋x的父节点p,然后将x赋值为root。
后四种情况是对称的,比如看一下 情况八
我们删除50这个节点,观察结果如何?
50这个节点的左右孩子节点都不为null,先要找到其后继节点(100),用100节点替换50节点,p指向100节点。因为后继节点100的左右孩子节点都为null,并且它的颜色为黑,接着就要调用fixAfterDeletion。
sib节点也就是30,最后只满足此种情况。先将30的颜色设置为红色,替换50后的100节点设置为黑色,30的左孩子(20)设置为黑色。以替换50后的100节点作为支点右旋。
最后断开之前的后继节点链接。
参考资料:
1.https://www.cs.usfca.edu/~galles/visualization/RedBlack.html
2.https://www.cnblogs.com/LiaHon/p/11221634.html