TreeMap详解
TreeMap是什么?
public class TreeMap<K,V> extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable {...}
TreeMap 是一个有序的key-value集合,它是通过红黑树实现的,继承于AbstractMap,实现了NavigableMap接口和Cloneable接口还有java.io.Serializable接口,所以也就支持一系列的导航方法,能被克隆以及支持序列化.
TreeMap基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法;TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n),另外,TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fastl的。
TreeMap的本质是R-B Tree(红黑树),它包含几个重要的成员变量: root, size, comparator;root 是红黑数的根节点。它是Entry类型,Entry是红黑数的节点,它包含了红黑数的6个基本组成成分:key(键)、value(值)、left(左孩子)、right(右孩子)、parent(父节点)、color(颜色)。Entry节点根据key进行排序,Entry节点包含的内容为value,红黑数排序时,根据Entry中的key进行排序;Entry中的key比较大小是根据比较器comparator来进行判断的,size是红黑数中节点的个数
所以想要学习TreeMap之前必须要先了解红黑树的原理,下边我们就分析一下红黑树.
TreeMap怎么实现的?
红黑树的由来(以下图片中的数值只是例子,为了画图方便,不准确,仅供参考):
我们都知道或者听说过,红黑树是为了提高检索效率而生的,二叉树本身就是提高检索效率的数据结构,正是因为节点之前的的大小关系(任意节点都大于该节点的左节点,小于该节点的右节点)决定的,正是因为节点之间的关系,恰恰也造成了二叉树在生成中的失衡现象,最坏的就是一边倒的情况,也就是只有右/左子树,这样自然就导致二叉树的检索效率大大降低变成了O(n),所以为了维持二叉树的平衡,就生成了一些更高级的算法来维持树结构,那就是常见的AVL,SBT,伸展树,TREAP ,红黑树等等算法.正因如此,所以红黑树从根到叶子的最长的可能路径不多于最短的可能路径的两倍长,结果是这棵树大致上是平衡的.因为操作比如插入. 删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。所以红黑树它是复杂而高效的,其检索效率O(log n).
算法导论中红黑树的5大特性(重要需牢记):
- 每个节点或是红色的,或是黑色的
- 根节点是黑色的
- 每个叶节点(NIL)是黑色的
- 如果一个节点是红色的,则它的两个子节点都是黑色的
- 对每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点.
红黑树的特点总结:
1. 具体二叉树所有的特性,同时红黑树更是一颗自平衡的排序二叉树
2. 红黑树(Red Black Tree) 是一种自平衡二叉查找树,时间复杂度是O(log n),这里的n 是树中元素的数目
3. 根节点是黑色
4. 节点只有红黑两色
5. 每个红色节点的两个子节点都是黑色,也就是说在一条路径上不能出现相邻的两个红色结点
6. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点
7. 从每个叶子到根的所有路径上不能有两个连续的红色节点
8. 左旋是逆时针旋转
9. 右旋是顺时针旋转
10. 任意节点都大于该节点的左节点,小于该节点的右节点
11. 每个叶节点(NIL节点,空节点)是黑色的(可以理解为最底层的节点)
12. 它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树
13. 红黑树从根到叶子的最长路经不会超过最短路径的2倍
14. 新增节点都会设置成红色
备注:
nil是一个对象值,如果要把一个对象设置为空的时候就用nil。
Nil是一个类对象的值,如果要把一个Class类型的对象设置为空的时候就用Nil,也就是本身是空节点并且没有左右子节点
NULL是一个通用指针
红黑树主要具有三个动作,同时也是最重点的内容:
左旋:
左旋源码:
private void rotateLeft(Entry<K,V> p) {
if (p != null) {
// 得到p右孩子r
Entry<K,V> r = p.right;
// p的右孩子 为 r的左孩子
p.right = r.left;
//如果r的左孩子非空,r的左孩子的父亲为p
if (r.left != null) r.left.parent = p;
//r的父亲为p的父亲
r.parent = p.parent;
// 如果p的父亲是空节点,则说明原来的p是根节点,则根节点为r
if (p.parent == null){
root = r;
}else{
// 如果p是它父节点的左孩子,则p的父节点的左孩子为r
if (p.parent.left == p){
p.parent.left = r;
}else{
//如果p是它父节点的右孩子,则p的父节点的右孩子为r
p.parent.right = r;
}
}
//r的左孩子为p
r.left = p;
//p的父节点为r
p.parent = r;
}
}
右旋:
右旋源码:
private void rotateRight(Entry<K,V> p) {
if (p != null) {
//获取p的左孩子
Entry<K,V> l = p.left;
//p的左孩子为l的右孩子
p.left = l.right;
//如果l的右孩子不是空节点,就把p给l的右孩子的父亲
if (l.right != null) l.right.parent = p;
//l的父亲为p的父亲
l.parent = p.parent;
//如果p的父亲是空节点,就把l变成root
if (p.parent == null)
root = l;
//如果p的父亲不是空节点,并且p是它父节点的右孩子,则p的父节点的右孩子为l
else if (p.parent.right == p)
p.parent.right = l;
//如果p的父亲不是空节点,并且p是它父节点的左孩子,则p的父节点的左孩子为l
else p.parent.left = l;
//l的右孩子为p
l.right = p;
//p的父节点为l
p.parent = l;
}
}
画图实在是太麻烦了,右旋和左旋类似,只不过旋转方向不同而已,图就不画了,自己脑补一下即可,其实不难理解.右旋是顺时针旋转
着色:
- 以升序插入构建红黑树:
- 以降序插入构建红黑树:
- 随机插入构建红黑树:
图片来源:http://www.cnblogs.com/yangecnu/p/Introduce-Red-Black-Tree.html
插入后调整(也是重中之重的核心,红黑树的调整基本都是通过这里体现出来的):
private void fixAfterInsertion(Entry<K,V> x) {
//把新增的x节点设置成红色(也就是说新加入的节点都是红色)
x.color = RED;
//一直循环遍历,直到新增节点不为空,并且不是根节点,而且它的父节点是红色
//如果它是null,那自然不用说了,如果它是根节点直接跳到最底部设置为黑色,如果它的父亲不是红色,也直接跳过修复颜色过程.
while (x != null && x != root && x.parent.color == RED) {
//如果x是它爷爷左孩子的孩子(也就是爷爷的孩子(爸爸-左分支)的孩子) 说白了就是它父亲是它爷爷的左孩子
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
//获取x的爷爷的右孩子为y(叔叔)
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
//如果y也就是叔叔是红色
if (colorOf(y) == RED) {
//把x的爸爸设置成黑色
setColor(parentOf(x), BLACK);
//把x的叔叔设置成黑色
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);
//然后以x为当前节点进行左旋操作
rotateLeft(x);
}
//设置x的爸爸为黑色
setColor(parentOf(x), BLACK);
//设置x的爷爷为红色
setColor(parentOf(parentOf(x)), RED);
//然后以x的爷爷为当前节点进行右旋操作
rotateRight(parentOf(parentOf(x)));
}
//如果x是它爷爷右孩子的孩子(也就是爷爷的孩子(叔叔-右分支)的孩子) 说白了就是它父亲是它爷爷的右孩子
} else {
//获取x的爷爷的左孩子为y(叔叔-原来的爸爸,可能小时候被狸猫换太子了!)
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
//如果y也就是叔叔是红色
if (colorOf(y) == RED) {
//把x的爸爸设置成黑色
setColor(parentOf(x), BLACK);
//把x的叔叔设置成黑色
setColor(y, BLACK);
//把x的爷爷设置成红色
setColor(parentOf(parentOf(x)), RED);
//把爷爷的值给x
x = parentOf(parentOf(x));
//如果y也就是叔叔是黑色
} else {
//如果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;
}
源码流程解析:
文字解析:
新插入节点都设置成红色
一、父节点是黑色,直接插入不需要重构
二、父节点是红色 (基准节点不是根节点)
1. 父亲是祖父的左儿子
1.1 叔叔(右)节点是红色:表示祖父节点必然是黑色。直接开始变色(父亲和叔叔变黑,祖父变红。把基准节点定位到祖父节点,重复这步操作)
1.2 叔叔(右)节点是黑色:check:该节点是父亲的右儿子,则执行左旋操作(该节点变为父亲,原父亲节点变为左儿子);此时基准节点变为他的原父亲节点,也就是左旋后新的左儿子(最左儿子)父亲变黑,祖父变红;此时的情况是父亲黑、叔叔黑、祖父红。导致从祖父到叔叔的路径黑色节点变少1个,以祖父节点为基点来一个右旋
2. 父亲是祖父的右儿子
2.1 叔叔(左)节点是红色:表示祖父节点必然是黑色。职介开始变色(父亲和叔叔变黑,祖父变红。把基准节点定位到祖父节点,重复这步操作)
2.2 叔叔(左)节点是黑色:check: 该节点是父亲的左儿子,则执行右旋操作(该节点变为父亲,原父亲节点变为右儿子);此时基准节点变为他的原父亲节点,也就是右旋后新的右儿子(最右儿子)父亲变黑,祖父变红;此时的情况是父亲黑,叔叔黑,祖父红。导致从祖父到叔叔的路径黑色节点少了1个,以祖父节点为基准来一个左旋
总结一下fixAfterInsertion的源码解释下来就是:
1. 插入节点是红色,父亲是黑色,则直接插入
2. 插入节点是红色,不是空节点,不是根节点并且父亲是爷爷的左孩子,那爷爷的右孩子就是叔叔,父亲颜色是红色
2.1 叔叔是红色
2.1.1 把父亲改成黑色,叔叔改成黑色,爷爷改成红色,然后把爷爷的key赋值到插入节点的key,最后把根节点设置成黑色
2.2 叔叔是黑色
2.2.1 如果插入节点是父亲的右孩子,就把父亲的key赋值到插入节点的key,然后以插入节点为基准进行左旋,左旋完成后把父亲设置成黑色,爷爷设置成红色,然后再以爷爷为基准进行右旋,最后把根节点设置成黑色
2.2.2 如果插入节点是父亲的左孩子,把父亲设置成黑色,爷爷设置成红色,然后再以爷爷为基准进行右旋,最后把根节点设置成黑色
3. 插入节点是红色,是空节点或是根节点或者父亲是爷爷的右孩子,那爷爷的左孩子就是叔叔,父亲颜色是黑色
3.1 叔叔是红色
3.1.1 把父亲改成黑色,叔叔设置成黑色,爷爷设置成红色,然后把爷爷的key赋值到插入节点的key,最后把根节点设置成黑色
3.2 叔叔是黑色
3.2.1 如果插入节点是父亲的左孩子,就把父亲的key赋值到插入节点的key,然后以插入节点为基准进行右旋,右旋完成后把父亲设置成黑色,爷爷设置成红色,然后再以爷爷为基准进行左旋,最后把根节点设置成黑色
3.2.2 如果插入节点是父亲的右孩子,把父亲设置成黑色,爷爷设置成红色,然后再以爷爷为基准进行左旋,最后把根节点设置成黑色
插入疑问:
红黑树在做插入时,插入的都是红色节点,为什么会这么做呢?其实每次插入都是红色节点会让难度减小不少,插入红色会违反的规则比较少,就拿上边的特点来说,插入红色节点不会违背二叉树,根节点一样是黑色,也满足节点都是红黑两色,也不违反空节点和NIL节点是黑色的规则,唯一违反的估计就是如果一个节点是红色,那么孩子必须是黑色了,所以红黑树的源码就解决了插入后颜色修复的部分,而其他部分也就不用再写一堆代码去维护了,事情也就变得简单了许多.也就是说只有在父节点为红色节点的时候才需要插入修复操作的,插入修复操作如果遇到父节点的颜色为黑则不需要修复,修复操作都是从下往上修复的,直到根节点为止.
好了,到这里基本的概念说的差不多了,就像盖大楼,设计图纸画好了,我们就要开始实施落地了,treemap的实现是一个很复杂的过程,估计需要很长一段时间才能解析清楚,我打算把多种情况下的场景分析一遍,以便更好的理解treemap中多种多样的情况,下边我们就开始吧!
插入
put源码:
public V put(K key, V value) {
//用t表示二叉树的当前节点
Entry<K,V> t = root;
//t为null表示一个空树,即TreeMap中没有任何元素,直接插入
if (t == null) {
//比较key值,个人觉得这句代码没有任何意义,空树还需要比较. 排序?
compare(key, key); // type (and possibly null) check
//将新的key-value键值对创建为一个Entry节点,并将该节点赋予给root
root = new Entry<>(key, value, null);
//容器的size = 1,表示TreeMap集合中存在一个元素
size = 1;
//修改次数 + 1
modCount++;
return null;
}
int cmp; //cmp表示key排序的返回结果
Entry<K,V> parent; //父节点
// split comparator and comparable paths
Comparator<? super K> cpr = comparator; //指定的排序算法
//如果cpr不为空,则采用既定的排序算法进行创建TreeMap集合
if (cpr != null) {
do {
**parent** **= t; //****parent指向上次循环后的t
//比较新增节点的key和当前节点key的大小
cmp =** **cpr.compare(key, t.key);
//cmp返回值小于0,表示新增节点的key小于当前节点的key,则以当前节点的左子节点作为新的当前节点
if (cmp < 0****)
t =** **t.left;
//cmp返回值大于0,表示新增节点的key大于当前节点的key,则以当前节点的右子节点作为新的当前节点
else if (cmp > 0****)
t =** **t.right;
//cmp返回值等于0,表示两个key值相等,则新值覆盖旧值,并返回新值
else
return** **t.setValue(value);**
} while (t != null);
}
//如果cpr为空,则采用默认的排序算法进行创建TreeMap集合
else {
if (key == null) //key值为空抛出异常
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);
}
//将新增节点当做parent的子节点
Entry<K,V> e = new Entry<>(key, value, parent);
//如果新增节点的key小于parent的key,则当做左子节点
if (cmp < 0)
parent.left = e;
//如果新增节点的key大于parent的key,则当做右子节点
else
parent.right = e;
/*
* 上面已经完成了排序二叉树的的构建,将新增节点插入该树中的合适位置
* 下面fixAfterInsertion()方法就是对这棵树进行调整. 平衡,具体过程参考上面的五种情况
*/
fixAfterInsertion(e);
//TreeMap元素数量 + 1
size++;
//TreeMap容器修改次数 + 1
modCount++;
return null;
}
从着色修复这个操作可以看出来,其实就是treemap的插入之后的调整,根据源码可以看出来,插入的时候很简单,就是为了满足二叉树的特性,比较一下key大小来决定分配到左还是右之类的逻辑,主要的内容其实还是在fixAfterInsertion()这个方法上,它是分很多情况的,总结下来分为5种情况,为了说起来方便一点,先定义几个节点的名字:
节点名字 | 节点位置 |
---|---|
N | 新插入的节点 |
P | N 的父节点为 P |
G | N的爷爷为G |
U | N的叔叔为U |
节点的结构:
static final class Entry<K,V> implements Map.Entry<K,V> {
//自己的key
K key;
//自己的value
V value;
//左孩子
Entry<K,V> left;
//右孩子
Entry<K,V> right;
//父亲
Entry<K,V> parent;
//黑色,true
boolean color = BLACK;
...
}
下边来详细说说这五种情况:
情况1:(插入的就是根节点或者插入节点N既不是根节点也不是空并且父节点是黑色)
public V put(K key, V value) {
//用t表示二叉树的当前节点
Entry<K,V> t = root;
//t为null表示一个空树,即TreeMap中没有任何元素,直接插入
if (t == null) {
//比较key值,个人觉得这句代码没有任何意义,空树还需要比较. 排序?
compare(key, key); // type (and possibly null) check
//将新的key-value键值对创建为一个Entry节点,并将该节点赋予给root
root = new Entry<>(key, value, null);
//容器的size = 1,表示TreeMap集合中存在一个元素
size = 1;
//修改次数 + 1
modCount++;
return null;
}
...
fixAfterInsertion(e);
//TreeMap元素数量 + 1
size++;
//TreeMap容器修改次数 + 1
modCount++;
return null;
}
private void fixAfterInsertion(Entry<K,V> x) {
//把新增的x节点设置成红色(也就是说新加入的节点都是红色)
x.color = RED;
//一直循环遍历,直到新增节点不为空,并且不是根节点,而且它的父节点是红色
//如果它是null,那自然不用说了,如果它是根节点直接跳到最底部设置为黑色,如果它的父亲不是红色,也直接跳过修复颜色过程.
while (x != null && x != root && x.parent.color == RED) {
...
}
//将根节点强制设置为黑色
root.color = BLACK;
}
也就是if (t == null) {不成立,直接跳过,说白了就是新插入的节点N就是根节点,再有就是while (x != null && x != root && x.parent.color == RED)不成立,那就是插入节点N既不是根节点也不是空节点并且父节点是黑色,这种情况下,每个红色节点必须有两个黑色的子节点和从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点的特点也没有受到影响,所以也就不需要调整了.
情况2:(父亲P是爷爷G的左孩子,那爷爷的右孩子U就是叔叔,父亲P颜色是红色,并且叔叔U也是红色)
这种情况(2.1.1)下N是红色,P也是红色显然不符合规则,把父亲改成黑色,叔叔改成黑色,爷爷改成红色,然后把爷爷的key赋值到插入节点的key,最后把根节点设置成黑色,这里可能有点晕,其实也很好解释,不管什么情况,只要叔叔是红色的情况下,基本是不需要进行旋转操作的,只需要颜色修复就可以了,但是这个颜色修复是一个自下向上的递归过程,while循环嘛,N是红,向上递归自然P和U都是黑,G自然就变成红了,但是G是根节点怎么可以是红呢,说是这么说,但是这只是我们脑子里得数据,真实数据中G这个红色要是考虑是不是根节点的,如果不是根节点,自然继续变色了,那是怎么继续的呢?也就是x = parentOf(parentOf(x));起到作用了,把基准节点从N换成G,那么G就成了下一次递归的基准,自然G又进行了一次颜色修复,但是这里发现G是根节点,自然也就不满足 x != root这个条件了,直接跳出递归进行最后的root.color = BLACK;根节点变色了,最后就满足了G是黑了.
情况3:(父亲P是爷爷G的左孩子,那爷爷的右孩子U就是叔叔,父亲P颜色是红色,并且叔叔U是黑色)
还是那样的规律,叔叔是红色不旋转,叔叔是黑色那肯定要旋转,但是怎么旋转就需要考虑情况了,也就是上边解释的那两种场景了
2.2.1 如果插入节点N是父亲P的右孩子,就把父亲P的key赋值到插入节点N的key,也就是以P为基准节点进行左旋,左旋完成后把父亲P设置成黑色,爷爷G设置成红色,然后再以爷爷G为基准进行右旋,最后把根节点设置成黑色
2.2.2 如果插入节点N是父亲P的左孩子,把父亲P设置成黑色,爷爷G设置成红色,然后再以爷爷G为基准进行右旋,最后把根节点设置成黑色
情况4:(与情况二对立,父亲P是爷爷G的右孩子,那爷爷的左孩子U就是叔叔,父亲P颜色是黑色,并且叔叔U是红色)
叔叔是红色,那就是只变色不旋转,也就是3.1.1的情况,把父亲P改成黑色,叔叔U设置成黑色,爷爷G设置成红色,然后把爷爷的key赋值到插入节点的key,最后把根节点设置成黑色
情况5:(与情况三对立,父亲P是爷爷G的右孩子,那爷爷的左孩子U是叔叔,父亲P是黑色,并且叔叔U也是黑色)
叔叔是黑色,那就是需要旋转,也就是3.2的情况,此处也是具有2种场景:
3.2.1 如果插入节点N是父亲P的左孩子,就把父亲P的key赋值到插入节点N的key,然后以P为基准进行右旋,右旋完成后把父亲P设置成黑色,爷爷G设置成红色,然后再以爷爷G为基准进行左旋,最后把根节点设置成黑色
3.2.2 如果插入节点N是父亲P的右孩子,把父亲P设置成黑色,爷爷G设置成红色,然后再以爷爷G为基准进行左旋,最后把根节点设置成黑色
删除
相比于插入,删除就显得更复杂一点,很多博客即便在经典的算法导论这本书上,也没有把所有的插入、删除情况说完,红黑树删除节点,最大的麻烦是要保持各分支黑色节点数目相等,因为是删除,所以也就不用过多担心颜色冲突问题因为插入才会引起颜色冲突的问题,由于红黑树就是二叉搜索树,因此节点的删除方式和二叉搜索树相同.不过红黑树删除操作的难点不在于节点的删除,而在于删除节点后的调整操作.因此红黑树的删除操作分为两步:
- 首先确定被删除节点的位置,主要部分就是下边要讲的找到代替节点相关的部分
- 然后调整红黑树的平衡性
老样子,先来看一下源码:
//map的remove
public V remove(Object key) {
//获取这个key对应的节点
Entry<K,V> p = getEntry(key);
//如果是空直接返回null
if (p == null)
return null;
V oldValue = p.value;
//执行删除
deleteEntry(p);
return oldValue;
}
//final的获取节点方法
//如果根节点是空直接返回null
//如果根节点不是空递归判断要获取的节点是根节点的那个分支上的节点然后返回
final Entry<K,V> getEntry(Object key) {
// Offload comparator-based version for sake of performance
if (comparator != null)
return getEntryUsingComparator(key);
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
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;
}
//寻找代替节点的方法
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;
}
}
//删除逻辑
private void deleteEntry(Entry<K,V> p) {
//修改次数+1
modCount++;
//容量-1
size--;
//被删除节点有左右分支,此处用successor永远只取右分支左边的最小值
if (p.left != null && p.right != null) {
//successor是寻找代替节点的方法
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) {
replacement.parent = p.parent;
//若P没有父节点,则用根节点当replacement
if (p.parent == null)
root = replacement;
//如果P为左节点,则用replacement来替代为左节点
else if (p == p.parent.left)
p.parent.left = replacement;
//如果P为右节点,则用replacement来替代为右节点
else
p.parent.right = replacement;
//同时将P节点从这棵树中剔除掉
p.left = p.right = p.parent = null;
/*
* 若P为红色直接删除,红黑树保持平衡
* 但是若P为黑色,则需要调整红黑树使其保持平衡
*/
if (p.color == BLACK)
fixAfterDeletion(replacement);
} else if (p.parent == null) { //p没有父节点,表示为P根节点,直接删除即可
root = null;
} else { //P节点不存在子节点,直接删除即可
if (p.color == BLACK) //如果P节点的颜色为黑色,对红黑树进行调整
fixAfterDeletion(p);
//删除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;
}
}
}