csdn 连接:blog.csdn.net/ziwang_/art…java
注:本文的源码摘自 jdk1.8 中 TreeMap数组
如下有几个违反上述规则的结点示例:bash
结点必须是红色或黑色学习
根结点必须是黑色的ui
叶子结点必须是黑色的spa
以上三个都是错误的红黑树示例,每一个红色结点的两个子结点都是黑色,而以下是合格的.net
固然,细心的读者应该发现了我只是展现了前四条性质而没有展现第五条性质,没有什么理由,笔者就是懒,第五条挺好理解的。翻译
这里的左旋右旋都是针对根节点而言的,因此左图到右图是 y 结点右旋,右图到左图是 x 结点左旋。设计
如今不理解这俩概念有什么用不重要,可是但愿读者能理解它的变幻过程,到后面会涉及到。3d
提及来枯燥无心,咱们能够结合 TreeMap 来看看左旋右旋的源码 ——
在这里咱们就针对左旋源码看看 ——
笔者就直接一行一行解释吧:
private void rotateLeft(Entry<K,V> p) {
if (p != null) {
Entry<K,V> r = p.right; // r 是根结点右子结点
p.right = r.left; // 为根结点的左结点指向右子结点(也就是 r)的左结点
if (r.left != null)
r.left.parent = p; // 意义同第二步,这步是右子结点(也就是 r)的左结点将父结点引用指向 p
r.parent = p.parent; // 将 r 结点的父引用指向 p 结点的父引用
if (p.parent == null)
root = r; // 将根结点替换为 r
else if (p.parent.left == p)
p.parent.left = r; // 意义同上
else
p.parent.right = r; // 意义同上
r.left = p; // r 左结点引用指向 p 结点
p.parent = r; // p 结点父引用指向 r 结点
}
}复制代码
设置成黑色的吧,就违反了性质5,设置成了红色的吧,就容易违反了性质4。那怎么办?总要给一个颜色,那咱们就给红色的吧。为何?由于若是设置成黑色的话,该分支的黑色结点数量确定比其余分支多一个,而这样的话至关地很差作调整。若是将插入结点颜色置为红色的话,运气比较好的状况下该父结点就是黑色的,那这样就不须要作任何调整。另外一种状况是插入结点的父结点颜色是红色的,这种状况咱们就须要详细讨论了,具体分为如下两种(此处咱们以插入结点的父结点是爷爷结点的左子结点为例(有点拗口),镜像操做道理相同):
父结点与叔叔结点都为红的话那么一定爷爷结点为黑,实际上此时咱们最简单的操做就是将父结点和叔叔结点染黑,将爷爷结点染红(将爷爷结点染红的目的是为了保证爷爷结点路径的黑色结点数量不改变),以下 ——
如今目标结点、父结点、叔叔结点都符合要求了,可是爷爷结点的父结点是红色的,那么就冲突了,聪明的读者可能已经发现了,此时的爷爷结点就至关于目标结点,咱们不妨将爷爷结点置换为目标结点,再进行递归操做就能够达到解决冲突的目的了。
但凡是有一个结点是红色,那么它的父结点一定是黑色(性质4),因此爷爷结点必定是黑色的。
有细心的小伙伴可能觉察到,上图违反了性质五。实际上上图是一张简化后的图,为了咱们后面的内容更加便于理解,上图的原图应该是如下模样 ——
![]()
上图原图 ps:上图中叔叔结点和兄弟结点能够理解成 java 中的 null 结点,笔者特意将它们的个头缩小了,以便区分。
那么此时该怎么操做呢?爷爷结点右旋,爷爷结点置红,父结点置黑。这条操做事后,性质四、5都没有违反。
固然,上图也只是一张简化图,实际上原图以下:
![]()
上图原图
那么结合 TreeMap 源码咱们来看看:
翻译以下:
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))); // y 是叔叔结点
// 状况1 叔叔结点也为红
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK); // 父结点赋黑
setColor(y, BLACK); // 叔叔结点赋黑
setColor(parentOf(parentOf(x)), RED); // 爷爷结点赋红
x = parentOf(parentOf(x)); // 爷爷结点置为目标结点,递归
} else {
// 状况2 叔叔结点为黑
// 小插曲,若是目标结点是父结点的右子结点,左旋父结点
// 固然,此时目标结点应改成父结点
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; // 根结点必须赋黑
}复制代码
看完代码咱们发现咱们好像漏了一个小插曲(固然,这是笔者故意的),那么小插曲是一个什么状况呢?言语来讲,在叔叔结点为黑的前提下,当目标结点是父结点的右子结点的时候,须要对父结点进行左旋而后才能接续下一步操做,为何会这样,咱们一图胜千言 ——
若是忽略上述状况,那么最终会获得如下状况:
因为目标结点是父结点的右子节点,在爷爷结点右旋过程当中,它会转为原爷爷结点的左子结点,这样的话就违反了特性4和特性5。解决方法就是上面所提到的将父结点先进行左旋而后再进行前面所提到的操做,以下图 ——
固然,不要忘了,如今须要调整的结点是原父结点,也就是要将上图左下角那个结点做为目标结点进行调整。
因此红黑树的添操做分为如下三步:
翻译以下:
private void deleteEntry(Entry<K,V> p) {
// 优先选择左子结点做为被删结点的替代结点
Entry<K,V> replacement = (p.left != null ? p.left : p.right);
// 若是替代结点不为空
if (replacement != null) {
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;
// 将删除结点的各个引用置 null
p.left = p.right = p.parent = null;
// 若是删除结点颜色为黑色,那么须要进行删后调整
if (p.color == BLACK)
fixAfterDeletion(replacement);
} else if (p.parent == null) {
// 若是替代结点为空且删除结点为 root 结点
root = null;
} else {
// 若是删除结点为空且不是 root 结点
// 若是删除结点颜色为黑色,那么须要进行删后调整
if (p.color == BLACK)
fixAfterDeletion(p);
// 将删除结点的各个引用置 null
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;
}
}
}复制代码
删除时可能分为三种状况,具体的作法也在上述代码中作了清晰的解释,笔者在此就不扩展了,细心的读者可能发现了,上述删除操做凡是涉及到了删除结点是黑色的状况下,都须要调用 fixAfterDeletion()
方法对红黑树进行调整。这是由于若是删除结点是黑色的,当它被删除后就会违反性质5,因此咱们须要对红黑树进行结构调整。
为了便于理解红色结点为何不会影响红黑树总体结构,笔者仍是举了一个例子给各位读者理解一下,下图是删除前:
![]()
删除前 下图是删除后:
![]()
删除后
实际上红黑树是使用如下2点思想来进行调整的(笔者认为,在分析 fixAfterDeletion()
代码实现以前,做为开发者应该去自行思考一下若是咱们做为源码设计者,咱们会如何来解决这个问题。) ——
1.给删除结点的路径增长一个黑色结点(将兄弟路径的一个黑色结点移过来)
2.给删除结点的兄弟路径减小一个黑色结点(将兄弟路径的一个红色结点染黑)
说完思想,咱们讨论一下具体删除操做是如何进行的。红黑树在保障删除结点的兄弟结点为黑色的状况下(没有什么特殊原因,仅仅是为了后期好操做),分如下两点来进行分析:
1.兄弟结点的两个子结点都是黑色的
2.另外一种状况(兄弟结点的两个子结点至多一个黑色的)
对于状况1来讲,红黑树采用思想2,将兄弟结点置为红色,可是这样带来了两个问题——对于父路径来讲,它与兄弟路径黑色结点数量不一样,违反性质5;且若是父结点也是红色,那么它势必与孩子结点冲突,还会违反性质4,以下图——
下图示例违反性质5:
![]()
原图 ![]()
违反性质5 下图示例违反性质5且违反性质4:
![]()
原图 ![]()
违反性质四、5
对于前一个问题用递归的思想来解决,将父亲结点置为目标结点,让父亲结点的兄弟结点也要减小一个黑色结点就能够了(借鉴思想2);而对于后一个问题,只须要将父结点置黑便可(借鉴思想2)。jdk 中相关实现源码以下:
while (x != root && colorOf(x) == BLACK) {
Entry<K,V> sib = rightOf(parentOf(x));
if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
}
}
setColor(x, BLACK);复制代码
前面阐述的是针对状况1而言,针对于状况2而言,红黑树采用的是思想1,具体作法分为又得分为如下两种小状况:
对于第一种小状况,红黑树采用如下操做:
1.兄弟结点置父结点颜色(准备谋权篡位)
2.父结点置黑、兄弟结点右结点置黑
3.父结点左旋
该思想不只保证了更新结点后不会冲突(父结点与兄弟结点不冲突,兄弟结点与右子结点不冲突,兄弟结点左子结点与父结点不冲突),而且保证了黑色结点数量不会改变,一图胜千言——
jdk 中相关源码以下:
while (x != root && colorOf(x) == BLACK) {
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(rightOf(sib), BLACK);
rotateLeft(parentOf(x));
x = root;
}
setColor(x, BLACK);复制代码
而对于第二种小状况,红黑树采用如下操做:
1.将兄弟结点的左子结点染黑
2.兄弟结点染红
3.兄弟结点右旋
实际上细心的读者发现了,转换后的结构是等同于第一种小状况的初始结构,因此接下来就按照第一种小状况的步骤去变换结构,相关源码以下:
while (x != root && colorOf(x) == BLACK) {
if (colorOf(rightOf(sib)) == BLACK) { // 状况2
setColor(leftOf(sib), BLACK);
setColor(sib, RED);
rotateRight(sib);
sib = rightOf(parentOf(x));
}
// 状况1
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(rightOf(sib), BLACK);
rotateLeft(parentOf(x));
x = root;
}
setColor(x, BLACK);复制代码
这一块可能有一些复杂,但记住如下三点核心思想问题就不是很大了:
那么接下来就是看看 fixAfterDeletion()
的代码实现了 ——
解释以下:
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));
// 小插曲1,若是兄弟结点为红
// 这步是保障兄弟结点必定为黑
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 {
// 兄弟结点的子结点至多一个是黑色的
// 小插曲2,兄弟结点左子结点为红,右子结点为黑的状况
// 这步的意义是让兄弟结点的右子结点的数量多一个
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 { // 镜像操做
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);
}复制代码
红黑树在 java 中的运用实际上仍是挺多的,例如 TreeSet
的默认底层实现实际上也是 TreeMap
;jdk 8中的 HashMap
实现也由原来的数组+链表更改成了数组+链表/红黑树。