一.什么是红黑树
- 红黑树是一种自平衡的二叉查找树,是计算机科学中用到的一种数据结构。
- 1972年出现,最初被称为平衡二叉B树。1978年更名为“红黑树”。
- 是一种特殊的二叉查找树,红黑树的每一个节点上都有存储表示节点的颜色。
- 每一个节点可以是红或者黑;红黑树不是高度平衡的,他的平衡是通过“红黑规则”实现的。
与平衡二叉树的区别
平衡二叉树: 红黑树:
1.高度平衡 1.是一个二叉查找树
2.当左右子树高度差超过1时,通过旋转保持平衡 2.不是高度平衡的
3.条件:特有的红黑规则
所以红黑树并不是一个完全意义上的平衡二叉树,称它为二叉查找树最为合适,并且是自平衡的哦,那么下面我们来讲一下什么是红黑规则
二.红黑规则
红黑规则是红黑树的核心,它决定了红黑树的特点,这也是区别于其他数据结构的属性
1.每一个节点或是红色的,或是黑色的 |
2.根节点必须是黑色 |
3.如果一个节点没有子节点或父节点,则该节点相应的指针属性为Nil,这些Nil视为叶节点,每个叶节点(Nil)是 黑色的 |
4.如果某一个节点是红色,那么它的子节点必须是黑色(不能出现两个红色节点相连的情况) |
5.对于每一个节点,从该节点到其所有后代节点的简单路径上,均包含相同数目的黑色节点 |
下面我们对红黑规则进行讲解
三.红黑树原理
上图是一棵示例红黑树
红黑树的每一个节点在原有二叉树的基础上,又增加了“颜色”的属性,用来记录当前节点的颜色,但是他还有什么作用呢?我们一会讲到
通过上图,我们可以更直观的了解到红黑树的结构,并用它来理解红黑规则
我们可以看到,图中每个节点的颜色要么都是红色,要么都是黑色,并没有出现其他的颜色,这遵循了红黑规则第一条;
值为13的根节点的颜色是黑色,遵循了红黑规则第二条;
同样我们可以看到,对于没有父节点或者是子节点的节点来说,他们的对应位置的指针设置为了Nil,比如,值为1的黑色节点,它有右子节点二没有左子结点,所以它的左子结点的指针就设置为了Nil,右子节点的指针则指向了值为6的节点;对于没有左子结点也没有右子结点的节点来说,他们的下面两个指针都指向了Nil。这遵循了红黑规则的第三条;
注:Nil同样是一个叶子结点,只不过没有任何意义哦,但并非没用
第四点和第五点我们应该结合起来去理解,首先红色节点的子节点颜色不能是红色,然后每一个节点到其后代的叶子结点扥简单路径上的黑色节点数量相同。这句话有点拗口,我们来分析一下,首先,后代指的是从一个节点开始,它的所有子节点称为该节点的后代;叶子结点就是没有子节点的节点,在图中就是Nil节点;简单路径指的是从一个节点开始,顺着它的子节点一直向下的路径,就是简单路径,不能回头,例如:根节点13到节点6的简单路径就是13->8->1->6;
我们用一张图来加强理解:
所以也就是说,任何一个节点到它的最下面的叶子结点构成的所有路径上,黑色节点的数量是相同的且不存在两个红色节点相连的情况,很显然这遵循了红黑规则第四条和第五条;
我们理解了红黑树的红黑规则的体现,那么我们为什么不来亲手构建一棵红黑树来学习红黑规则具体怎么使用呢?
四.红黑树构建过程
为了加深对红黑树的深层理解和红黑规则的具体应用,我这里将展示一棵红黑树的构建过程
首先,红黑树默认的节点颜色是红色,这是因为红色节点的效率高,可是为什么呢?我将给出一个让你秒懂的对比过程!
我们先默认添加的节点初始是黑色
以添加三个节点为例:
我们先将20节点添加进去,此时根据构建规则,它应该长这个样子(此时红黑规则的五条它都满足):
我们再将18节点添加进去,根据排序规则,18比20小,应该放在左边:
这个时候就出现问题了,让我们回顾一下红黑规则,很显然它违背了第五条:任何一个节点的后代的叶子结点的简单路径上黑色节点的数量应该相同。但我们看,从根节点开始到左侧的叶子结点与右侧的叶子结点的简单路径上黑色节点的数量是不同的,所以思考一下,为了使它满足红黑规则,我们应该怎么改变?
很显然是将18节点的颜色改为红色:
接下来我们继续添加节点,将23节点添加进去,它应该被放在20根节点的右侧:
这个时候很显然他又违背了红黑规则的第五条,那我们应该怎么改呢?现在有两种改法:
1.将23号节点的颜色改为红色
2.将18号节点的颜色改为黑色
我们应该选择哪种方法?这里告诉你,应该选择第一种哦。
然后红黑树最终的形态是这个样子:
在这个过程中你是否还记得节点的颜色变过几次呢?根据结果显示,他一共变了两次颜色,让我们记住这个结果。
接下来,我们以节点颜色默认是红色来展示构建过程:
还是一样,添加第一个节点20(左图)。这时候我们发现它此时违背了第二条规则:根节点只能是黑色。于是我们进行修改,变成了右图所示结果:
接下来添加第二个节点18,按照规则他被添加到根节点的左边:
此时我们发现它是不违背规则的
继续添加第三个节点23:
很巧!它同样不违背红黑规则
所以在这个过程中,节点的颜色总共只被改变了一次
结论:可以发现,我们添加三个节点的情况下,节点颜色默认是黑色的情况下载构建的过程中要比节点颜色默认是红色的情况多调整一次,所以这也就论证了红色节点效率高的结论
其次,关于红黑树的构建,在上面(节点颜色默认为红色)的基础上,还有如下规则:
不要害怕,让我来细细讲解
我们首先要明确一个点,这些所有的构建规则,都是为了维护红黑规则而出现的,换句话说,这些构建规则实际上就是红黑规则在实际应用中的具象化体现。
接下来,我将以一个多节点的构建过程来演示这些构建规则,来帮助你理解图片中的内容,构建过程中会涵盖图中所有的情况
1.添加第一个节点18
此时我们添加的是根节点,根据图中的规则,我们需要将节点直接变为黑色(右图)
2.添加第二个节点23
按照规则我们将其放在根节点的左侧,此时我们填入的节点是非根节点,且父节点是黑色,所以按照图中的要求,我们不需要进行任何的操作
3.添加第三个节点23
按照规则我们将其放在根节点的右侧,此时我们填入的节点是非根节点,且父节点是黑色,所以按照图中的要求,我们同样不需要进行任何的操作
4.添加第四个节点22
因为22比根节点20大且比23小,所以我们应该将其放在节点23的左侧,此时我们添加的节点是非根节点,且父节点是红色,所以按照图中要求,我们应该判断一下叔叔节点的颜色,我们发现叔叔节点是红色,所以按照要求,我们需要将父节点和叔叔节点都变成黑色,然后将爷爷节点变成红色(左图),但是由于爷爷节点是根节点,所以再将其变成黑色(右图)
叔叔节点:父节点的兄弟节点,也就是爷爷节点的另一个子节点,就像你的爷爷生了两个儿子,一个是你的爸爸,另一个就是你的叔叔(bushi
5.添加第五个节点17
因为17比根节点20小,比18也小,所以应该被放在18的左边,此时我们添加的是非根节点,且父亲是黑色,所以我们不需要进行任何操作
6.添加节点24和19
和上图原理一样,因为添加非根节点且父节点是黑色所以不需要进行任何调整
最终结果是这样
这个时候你可能会说:
Q:这也不难啊,不是有手就行吗?
A:这是因为图中的规则我们还没有全部碰到哦,那么我再添加两个节点,我们继续往下看
这时候我们再添加两个节点15和14
7.添加节点15
按照排序规则,我们将15节点放在17节点的左边,此时我们添加的是非根节点,父节点是红色,叔叔也是红色,所以将父节点和叔叔节点都变为黑色,并将爷爷节点变成红色,因为爷爷节点不是根节点,根据图中要求,将爷爷节点作为当前节点再进行判断(也就是假设爷爷节点是当前加入的节点),我们发现爷爷节点是非根节点,且爷爷节点的父节点是黑色的,所以我们不进行变动
8.添加节点14
根据排序规则,我们将14节点放在15节点的左边,此时我们添加的是非根节点,父节点是红色,但叔叔节点是黑色,且我们添加的是父节点的左孩子,所以我们将父节点设置为黑色,将爷爷节点设置为红色,并以爷爷节点为轴进行右旋 ,这里我分两张图来演示
右旋:将该节点作为其左子节点的右子节点,左子节点原本的右子节点作为该节点的左子节点
右旋之前:
右旋之后:
相对的,如果我们此次添加的节点出现在了15的右子节点上,那么我们就需要以15为基准进行左旋,并进行判断(15当作此次添加的节点)
此时整棵树的结构符合红黑规则的规范。
总结:我们在理解构建红黑树的过程时,我们首先要理解红黑树的本质,最佳思路:首先思考其是否符合红黑规则的要求,其次根据红黑树构建要求进行构建,因为其本质就是遵循红黑规则而构建的。
注:红黑树的增删改查效率都很高
看了这些,相信你已经学会了红黑树的构建方法了,但是你是否觉得这些又太片面化太生硬了呢?好的,那我们----上源码。
五.红黑树源码分析
接下来我将以源码作为背景深入分析红黑树的构建过程,相信到最后你会理解为什么要有红黑树,它的效率又为什么高。
考虑到红黑树实现源码设计大量的Map操作,实现相当复杂,完整实现代码1000+行,这里摘取其核心逻辑插入、删除以及红黑树逻辑维护(旋转)的部分进行讲解
1.红黑树节点的定义
我们可以看到,由于红黑树是Map效率优化的产物,所以其在定义上存在着key、value的值
结合我上面给出的红黑树节点定义图,不难看出,源码中同样定义着左子节点、右子节点、父节点和节点颜色,节点同样是RBNode本类类型
// 红黑树节点定义 (简化自 java.util.TreeMap.Entry)static final class RBNode<K, V> {//在红黑树节点中,其值是以key、value形式存储的K key;V value;RBNode<K, V> left; // 左子节点RBNode<K, V> right; // 右子节点RBNode<K, V> parent; // 父节点boolean color; // 颜色:RED 或 BLACK//在定义节点时,需要传给其key、value和其父节点RBNode(K key, V value, RBNode<K, V> parent) {this.key = key;this.value = value;this.parent = parent;this.color = RED; // 新节点默认红色}}
红黑树节点类同样给出了一个构造方法,我们需要传入节点的值以及其父节点,其中节点颜色默认为红色我们前面也是提到过的,后续我们需要通过这个构造方法对红黑树的节点进行插入。
2.红黑树操作类
了解了红黑树节点的定义,我们再来看看红黑树中具体的操作,比如增删旋转是如何实现的
由于整体代码偏长,我分出几个方法分别讲解
红黑树操作类定义
可以看到,红黑树操作类上定义了两个泛型,分别是key和value,其中key实现了Comparable接口便于进行标准的排序比较操作。
在类内部首先定义了红黑树的根节点,并定义了两个静态常量,分别是RED和BLACK用来表示节点颜色,其用Boolean类型标识就是因为它只有两个值且互相对立。
// 红黑树核心操作类 (简化版,基于 TreeMap 逻辑)public class RedBlackTree<K extends Comparable<K>, V> {private RBNode<K, V> root; // 根节点private static final boolean RED = false;private static final boolean BLACK = true;
插入操作与平衡修复
1.插入
public void put(K key, V value) {//首先定义一个节点,将根节点的值复制给该节点RBNode<K, V> t = root;//判断t是否为空,如果t为空则表示树为空,根节点还未定义if (t == null) {//初始化根节点,值设置为key和valueroot = new RBNode<>(key, value, null);//颜色设置为黑色root.color = BLACK; // 根节点必须黑色return;}// 查找插入位置int cmp; //定义比较参数//定义一个父节点RBNode<K, V> parent;do {//首先将父节点定义临时为根节点parent = t;//cmp为比较参数cmp = key.compareTo(t.key);//如果cmp小于0,表示key小于当前父节点的键,则将父节点的左子节点作为新的父节点(向左子树查找)if (cmp < 0) t = t.left;//如果cmp小于0,表示key大于当前父节点的键,则将父节点的右子节点作为新的父节点(向右子树查找)else if (cmp > 0) t = t.right;//因为红黑树不允许存在相同的值,所以遇到相同的值则更新else { t.value = value; return; } // 已存在则更新} while (t != null); //结束条件是遍历到叶子节点// 创建新节点并插入RBNode<K, V> newNode = new RBNode<>(key, value, parent); //最终parent就是当前新增节点的父节点,调用构造函数插入//根据最后有一次的比较参数的值来决定该节点作为左子树还是右子树if (cmp < 0) parent.left = newNode; else parent.right = newNode; // 插入后修复红黑树性质fixAfterInsertion(newNode);}
插入节点方法代码的逻辑就是先判断根节点是否为空,如果根节点为空就初始化根节点;根节点不为空就不断遍历树去寻找到合适的插入位置(key比根节点key小则向左子树查找,反之亦然),最终找到位置后再判断是左子节点还是右子节点。
最终在插入节点后进行修复红黑树的操作
总结:
-
从根节点开始,逐步向下查找插入位置:
-
若当前键
<
当前节点的键,向左子树查找。 -
若当前键
>
当前节点的键,向右子树查找。 -
若键相等,直接更新值并返回(红黑树不允许重复键)。
-
-
终止条件:当
t
移动到null
时,parent
即为新节点的父节点。
2.修复红黑树(插入后)
private void fixAfterInsertion(RBNode<K, V> x) {x.color = RED; // 新插入节点设为红色//当该节点非空、节点不是根节点、节点的父节点的颜色为红色时while (x != null && x != root && x.parent.color == RED) {// 父节点是祖父的左子节点if (parentOf(x) == leftOf(grandparentOf(x))) {RBNode<K, V> uncle = rightOf(grandparentOf(x)); // 获取叔叔节点// Case 1: 叔叔节点是红色(颜色翻转)if (colorOf(uncle) == RED) {//将父节点颜色设置为黑色setColor(parentOf(x), BLACK);//将叔叔节点颜色设置为黑色setColor(uncle, BLACK);//将爷爷节点设置为红色setColor(grandparentOf(x), RED);x = grandparentOf(x); // 向上回溯} else {// Case 2: 当前节点是父的右子节点(转为 Case 3)if (x == rightOf(parentOf(x))) {x = parentOf(x);rotateLeft(x); // 左旋父节点}// Case 3: 当前节点是父的左子节点//将当前节点父节点的颜色设置为黑色setColor(parentOf(x), BLACK);//将爷爷节点的颜色设置为红色setColor(grandparentOf(x), RED);rotateRight(grandparentOf(x)); // 右旋祖父}} else { // 对称情况:父节点是祖父的右子节点//定义叔叔节点,此时叔叔节点一定是爷爷节点的左子节点RBNode<K, V> uncle = leftOf(grandparentOf(x));//如果叔叔节点的颜色为红色if (colorOf(uncle) == RED) { // Case 1//将父节点的颜色改为黑色setColor(parentOf(x), BLACK);//将叔叔节点的颜色改为黑色setColor(uncle, BLACK);//将爷爷节点的颜色设置为红色setColor(grandparentOf(x), RED);//以祖父为当前节点进行判断x = grandparentOf(x);} else {//如果叔叔节点是黑色且当前节点是父节点的左子节点if (x == leftOf(parentOf(x))) { // Case 2//以父节点作为当前节点并进行右旋x = parentOf(x);rotateRight(x);}// Case 3//将父节点颜色设置为黑色setColor(parentOf(x), BLACK);//将爷爷节点颜色设置为红色setColor(grandparentOf(x), RED);//以爷爷节点为基准进行左旋rotateLeft(grandparentOf(x));}}}root.color = BLACK; // 确保根节点为黑色 }
由于我们上面已经对红黑树构建逻辑进行分析和讲解,所以对于这个方法我们也不难理解,只不过这里的判断顺序与我们讲过的修些许不同,这里是先将父节点相对于爷爷节点的位置进行了分类,再对叔叔节点的颜色加以判断,并分别分出三种情况进行处理。
以下是这几种情况的操作演示
场景 1:叔叔节点为红色
黑 (祖父)/ \红 (父) 红 (叔)/红 (新节点 x)
-
操作:父、叔变黑,祖父变红。
-
结果:
红 (祖父)/ \黑 黑/红 (x)
-
后续:将
x
回溯到祖父节点,继续检查更高层的连续红色问题。
场景 2:叔叔节点为黑色(Case 2 → Case 3)
黑 (祖父)/ \红 (父) 黑 (叔)\红 (x,右子)
-
操作:
-
Case 2:左旋父节点,结构变为:
黑 (祖父)/ \红 (x) 黑 (叔)/ 红 (原父节点) 再以原父节点为准进行判断
-
Case 3:右旋祖父,父变黑,祖父变红:
黑 (父)/ \ 红 (x) 红 (祖父)\黑 (叔)
-
删除操作与平衡修复
1.删除
//删除节点操作方法public void remove(K key) {// 1. 查找要删除的节点RBNode<K, V> p = getNode(key);if (p == null) return; // 节点不存在// 2. 实际删除节点并修复红黑树deleteNode(p);}//查找节点方法private RBNode<K, V> getNode(K key) {//设置一个节点的值为rootRBNode<K, V> t = root;//循环条件为节点不为空while (t != null) {//要查找的节点的key不断与当前t节点的的key进行比较//如果比t的key小,就继续向其左子树查找,反之则向右子树查找int cmp = key.compareTo(t.key);if (cmp < 0) t = t.left;else if (cmp > 0) t = t.right;//如果值相等直接返回t的位置else return t;}return null;}//删除节点具体操作private void deleteNode(RBNode<K, V> p) {// --- 情况1: 节点有两个子节点 ---if (p.left != null && p.right != null) {// 找到后继节点(右子树的最小节点)RBNode<K, V> s = successor(p);// 用后继节点的键值替换当前节点p.key = s.key;p.value = s.value;p = s; // 实际删除后继节点(问题简化为删除单子或无子节点)}// --- 情况2: 删除节点是叶节点或只有一个子节点 ---RBNode<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;}// 清空被删节点的指针p.left = p.right = p.parent = null;// 删除黑色节点后需要修复(红色节点不影响黑高)if (p.color == BLACK) {fixAfterDeletion(replacement);}} else if (p.parent == null) {root = null; // 树中仅剩根节点} else {// 删除叶节点(无子节点)if (p.color == BLACK) {fixAfterDeletion(p); // 先修复再删除}// 从父节点断开链接if (p.parent != null) {if (p == p.parent.left) {p.parent.left = null;} else {p.parent.right = null;}p.parent = null;}}}//找到节点的后继结点,右子树最小节点private RBNode<K, V> successor(RBNode<K, V> t) {if (t == null) return null;//不断向下查找t的右子节点的左子树的最小值else if (t.right != null) {RBNode<K, V> p = t.right;while (p.left != null) p = p.left;return p;} else {// ...(此处省略向上查找逻辑,实际删除中不需要)return null;}}
删除节点的流程:
1.先查找要删除节点的位置,这个过程是通过不断进行key的比较完成的(小则左,大则右)
2.删除节点,这个过程分为两种大情况:1.要删除的节点有两个子节点,2.除第一种情况的所有情况。
对于第一种情况,我们需要找到比要删除节点的大的最小节点s,将其节点的信息赋值给要删除的节点,由于s是叶节点或单子节点,所以此时删除操作可以转化为第二种情况。
第二种情况,先取出该节点的叶子结点,因为其并没有两个子节点,所以通过
BNode<K, V> replacement = (p.left != null ? p.left : p.right);
取出其不为空的一个节点,但是考虑到该节点还有可能是叶子结点,所以下面又分出了两种情况
1.当replacement不为空时表示其不是叶子结点,将替代节点的父指针指向 p
的父节点。更新父节点的子指针(左或右)指向替代节点。断开 p
的所有指针。
若 p
是黑色:调用 fixAfterDeletion(replacement)
修复黑高。
2.replacement为空时表示其为叶子结点,这里还要分出两种情况,我们首先要判断要删除的节点是不是根节点,是根节点就直接将根节点设置为Null,否则就将该节点的父指针置空+父节点指向该节点的指针置空,但是需要先修复红黑树再进行删除。
关键问题解答
Q1. 为什么删除有两个子节点的节点时要找后继节点?
-
保持二叉搜索树性质:后继节点是右子树的最小节点,替换后能保证左子树所有节点仍小于它,右子树所有节点仍大于它。
-
简化操作:后继节点至多有一个右子节点,将问题简化为删除单子节点或叶节点。
Q2. 为什么删除黑色节点后需要修复?
-
黑高失衡:删除黑色节点会减少其路径上的黑色节点数量,违反红黑树性质 5(所有路径黑高相同)。
-
连续红色风险:若父节点和兄弟节点均为红色,可能导致连续红色节点。
Q3. 为什么叶节点删除要先修复再断开链接?
-
修复依赖父指针:
fixAfterDeletion(p)
需要访问p
的父节点和兄弟节点。若先断开父指针,修复逻辑将无法正确执行。
Q4. 如何处理根节点删除?
-
直接更新根指针:若删除的是根节点且无子节点,直接将
root
设为null
。若有子节点,替代节点成为新根。
2.修复红黑树(删除后)
private void fixAfterDeletion(RBNode<K, V> x) {//开始条件:删除节点不是根节点且节点颜色是黑色while (x != root && colorOf(x) == BLACK) {//当节点是父节点的左节子点时if (x == leftOf(parentOf(x))) {//设置一个节点赋值为父节点的右子节点,也就是叔叔节点RBNode<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 { //当前节点是父节点的右子节点// 对称逻辑//所有操作与上面的情况完全对立,比如左旋->右旋、rightOf->leftOfRBNode<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);}
当删除一个黑色节点后,可能导致路径黑高减少或连续红色节点问题。fixAfterDeletion
的目标是通过颜色调整和旋转操作,恢复红黑树的五个性质,也就是红黑规则。其核心处理逻辑围绕 兄弟节点 的状态展开。
1. 循环条件
while (x != root && colorOf(x) == BLACK)
-
含义:当
x
不是根节点且为黑色时,需要修复。 -
原因:删除黑色节点会减少路径黑高,需通过调整兄弟节点子树来补偿。
2. 分支处理:x 是父节点的左子
if (x == leftOf(parentOf(x))) {RBNode<K, V> sib = rightOf(parentOf(x)); // 获取兄弟节点// 后续处理...
}
Case 1:兄弟节点为红色
if (colorOf(sib) == RED) {setColor(sib, BLACK);setColor(parentOf(x), RED);rotateLeft(parentOf(x));sib = rightOf(parentOf(x)); // 更新兄弟节点
}
-
操作:
-
将兄弟节点设为黑色,父节点设为红色。
-
对父节点左旋,使原兄弟节点的左子成为新兄弟。
-
-
目的:将情况转换为兄弟节点为黑色,进入后续处理。
Case 2:兄弟节点的子节点均为黑色
if (colorOf(leftOf(sib)) == BLACK && colorOf(rightOf(sib)) == BLACK) {setColor(sib, RED);x = parentOf(x); // 向上回溯
}
-
操作:将兄弟节点设为红色,
x
上移至父节点。 -
影响:父节点路径的黑高减少 1,需继续循环处理父节点。
Case 3 & 4:兄弟节点的子节点至少有一个红色
else {if (colorOf(rightOf(sib)) == BLACK) { // Case 3:兄弟右子为黑(左子为红)setColor(leftOf(sib), BLACK);setColor(sib, RED);rotateRight(sib); // 右旋兄弟节点sib = rightOf(parentOf(x)); // 更新兄弟节点}// Case 4:兄弟右子为红setColor(sib, colorOf(parentOf(x)));setColor(parentOf(x), BLACK);setColor(rightOf(sib), BLACK);rotateLeft(parentOf(x)); // 左旋父节点x = root; // 终止循环
}
-
Case 3(兄弟右子为黑):
-
将兄弟左子设为黑色,兄弟设为红色。
-
右旋兄弟节点,使兄弟的右子成为新兄弟。
-
-
Case 4(兄弟右子为红):
-
兄弟继承父节点颜色,父节点设为黑色,兄弟右子设为黑色。
-
左旋父节点,平衡黑高。
-
设置
x = root
强制退出循环。
-
3. 对称分支:x 是父节点的右子
else { // 对称逻辑:x 是父的右子RBNode<K, V> sib = leftOf(parentOf(x));// 处理逻辑与左子分支对称(left ↔ right,rotateLeft ↔ rotateRight)
}
-
操作与左子分支完全对称,方向相反。
4. 最终修正
setColor(x, BLACK); // 确保当前节点为黑色
-
作用:无论循环如何退出,最终确保
x
为黑色(可能直接修复根节点颜色)。
关键场景示例
场景 1:兄弟节点为红色
B (父,黑) B (父,红)/ \ / \x D (兄弟,红) →→ x D (黑)/ \ / \C E C E
-
操作:兄弟变黑,父变红,左旋父节点,更新兄弟为
C
。
场景 2:兄弟子节点均为黑
B (父,黑) B (父,黑)/ \ / \x D (兄弟,黑) →→ x D (红)/ \ / \C E (均黑) C E
-
操作:兄弟变红,
x
上移至父节点,继续处理父节点。
场景 3 & 4:兄弟右子为红
B (父,黑) D (兄弟,父颜色)/ \ / \x D (兄弟,黑) →→ B E (黑)/ \ / \C E (红) x C
-
操作:兄弟继承父颜色,父变黑,兄弟右子变黑,左旋父节点,结束循环。
所以删除+修复操作相比插入+修复操作的逻辑更加复杂,因为我们需要逆向去思考这个过程。
红黑树操作工具方法
//---------------- 旋转操作 (核心工具方法) ----------------/** 左旋(维护红黑树平衡) */private void rotateLeft(RBNode<K, V> p) {if (p == null) return;// 1. 获取 p 的右子节点 r(左旋必须保证 r 存在)RBNode<K, V> r = p.right;if (r == null) return; // 无法左旋// 2. 将 r 的左子节点 rl 挂载到 p 的右子p.right = r.left;if (r.left != null) {r.left.parent = p; // 更新 rl 的父指针}// 3. 将 r 的父指针指向 p 的父节点r.parent = p.parent;// 4. 处理父节点的子指针if (p.parent == null) {root = r; // p 是根节点 → r 成为新根} else if (p == p.parent.left) {p.parent.left = r; // p 是父的左子 → r 替代 p 的位置} else {p.parent.right = r; // p 是父的右子 → r 替代 p 的位置}// 5. 将 p 作为 r 的左子r.left = p;p.parent = r;}/** 右旋(与左旋对称) */private void rotateRight(RBNode<K, V> p) {if (p == null) return;// 1. 获取 p 的左子节点 l(右旋必须保证 l 存在)RBNode<K, V> l = p.left;if (l == null) return; // 无法右旋// 2. 将 l 的右子节点 lr 挂载到 p 的左子p.left = l.right;if (l.right != null) {l.right.parent = p; // 更新 lr 的父指针}// 3. 将 l 的父指针指向 p 的父节点l.parent = p.parent;// 4. 处理父节点的子指针if (p.parent == null) {root = l; // p 是根节点 → l 成为新根} else if (p == p.parent.right) {p.parent.right = l; // p 是父的右子 → l 替代 p 的位置} else {p.parent.left = l; // p 是父的左子 → l 替代 p 的位置}// 5. 将 p 作为 l 的右子l.right = p;p.parent = l;}//---------------- 工具方法 ----------------private RBNode<K, V> parentOf(RBNode<K, V> p) {return (p == null ? null : p.parent);}private RBNode<K, V> leftOf(RBNode<K, V> p) {return (p == null) ? null : p.left;}private boolean colorOf(RBNode<K, V> p) {return (p == null ? BLACK : p.color);}private void setColor(RBNode<K, V> p, boolean c) {if (p != null) p.color = c;}private RBNode<K, V> rightOf(RBNode<K, V> node) {// 返回节点的右子节点return (node == null) ? null : node.right;}private RBNode<K, V> grandparentOf(RBNode<K, V> node) {if (node != null && node.parent != null) {return node.parent.parent;}return null; // 如果节点没有父节点或祖父节点,则返回null}
这些工具方法都是在维护红黑树结构时发挥作用。左旋、右旋的具体方法和原理我将会在后续的平衡二叉树部分中展开讲解。
看到这里相信你对红黑树的实现和有了更进一步的理解,由于原始源码部分内容过于繁重,这里无法全部进行分析,原始源码可见:Open_JDK的TreeMap部分的实现
六.总结
红黑树在数据结构中有着举足轻重的地位,像我们熟知的HashMap,它在jdk1.8以后也增加了红黑树的实现,这是因为红黑树在各方面的性能都非常好,通过解读源码部分我们已经发现,红黑树在维护结构的过程中最多的操作就是颜色的变化,但这个操作是消耗资源最少的操作,所以相比其他的二叉查找树,红黑树的优势就体现在了这里。
为了让大家更好的去理解红黑树,我已经整理好并上传了一份红黑树操作脚本(自行下载资源),包括增删和红黑树修复的功能,可以打印出每次的图形结果,便于大家的调试。