【数据结构】红黑树超详解 ---一篇通关红黑树原理(含源码解析+动态构建红黑树)

news2025/4/13 11:23:23

一.什么是红黑树

  • 红黑树是一种自平衡的二叉查找树,是计算机科学中用到的一种数据结构。
  • 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和value
                root = 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,右子)
  • 操作

    1. Case 2:左旋父节点,结构变为:

     黑 (祖父)
         /   \
      红 (x) 黑 (叔)
       /
    红 (原父节点)   再以原父节点为准进行判断
    1. 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) {
                //设置一个节点的值为root
                RBNode<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->leftOf
                        RBNode<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以后也增加了红黑树的实现,这是因为红黑树在各方面的性能都非常好,通过解读源码部分我们已经发现,红黑树在维护结构的过程中最多的操作就是颜色的变化,但这个操作是消耗资源最少的操作,所以相比其他的二叉查找树,红黑树的优势就体现在了这里。

        为了让大家更好的去理解红黑树,我已经整理好并上传了一份红黑树操作脚本(自行下载资源),包括增删和红黑树修复的功能,可以打印出每次的图形结果,便于大家的调试。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2333780.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

uni-app初学

文章目录 1. pages.json 页面路由2. 图标3. 全局 CSS4. 首页4.1 整体框架4.2 完整代码4.3 轮播图 swiper4.3.1 image 4.4 公告4.4.1 uni-icons 4.5 分类 uni-row、uni-col4.6 商品列表 uni-row、uni-col 小程序开发网址&#xff1a; 注册小程序账号 微信开发者工具下载 uniapp …

PHP多维数组

在 PHP 中&#xff0c;多维数组是数组的数组&#xff0c;允许你存储和处理更复杂的数据结构。多维数组可以有任意数量的维度&#xff0c;但通常我们最常用的是二维数组&#xff08;数组中的数组&#xff09;。 首先来介绍一下一维数组&#xff0c; <?php//一维数组 $strAr…

数学建模:针对汽车行驶工况构建思路的延伸应用

前言&#xff1a; 汽车行驶工况构建的思简单理解为将采集的大量数据进行“去除干扰、数据处理&#xff0c;缩减至1800S的数据”&#xff0c;并可达到等效替换的目的&#xff0c;可以使在试验室快速复现&#xff1b;相应的解决思路、办法可应用在 “通过能量流采集设备大量采集…

go语言内存泄漏的常见形式

go语言内存泄漏 子字符串导致的内存泄漏 使用自动垃圾回收的语言进行编程时&#xff0c;通常我们无需担心内存泄漏的问题&#xff0c;因为运行时会定期回收未使用的内存。但是如果你以为这样就完事大吉了&#xff0c;哪里就大错特措了。 因为&#xff0c;虽然go中并未对字符串…

当DRAM邂逅SSD:新型“DRAM+”存储技术来了!

在当今快速发展的科技领域&#xff0c;数据存储的需求日益增长&#xff0c;对存储设备的性能和可靠性提出了更高的要求。传统DRAM以其高速度著称&#xff0c;但其易失性限制了应用范围&#xff1b;而固态硬盘SSD虽然提供非易失性存储&#xff0c;但在速度上远不及DRAM。 为了解…

JS实现文件点击或者拖拽上传

B站看到了渡一大师课的切片&#xff0c;自己实现了一下&#xff0c;做下记录 效果展示 分为上传前、上传中和上传后 实现 分为两步 界面交互网络请求 源码如下 upload.html <!DOCTYPE html> <html lang"zh-CN"><head><meta charset&q…

Centos7.9 升级内核,安装RTX5880驱动

系统镜像下载 https://vault.centos.org/7.9.2009/isos/x86_64/CentOS-7-x86_64-DVD-2009.iso 系统安装步骤省略 开始安装显卡驱动 远程登录查看内核 [root192 ~]# uname -a Linux 192.168.119.166 3.10.0-1160.el7.x86_64 #1 SMP Mon Oct 19 16:18:59 UTC 2020 x86_64 x8…

Xdocreport实现根据模板导出word

只使用freemaker生成简单的word文档很容易&#xff0c;但是当word文档需要插入动态图片&#xff0c;带循环数据&#xff0c;且含有富文本时解决起来相对比较复杂&#xff0c;但是使用Xdocreport可以轻易解决。 Xdocreport既可以实现文档填充也可以实现文档转换&#xff0c;此处…

当当平台商品详情接口设计与调用指南

当当平台商品详情接口设计与调用指南 接口名称 GET /api/product/detail 图书商品核心信息查询接口 请求参数说明 参数名称 类型 是否必填 说明 isbn string 是 国际标准书号(支持13位/10位) product_id string 否 平台内部商品编号&#xff08;与…

sql server分析表大小

使用自动存储过程查询 EXEC sp_spaceused YourTableName; rows&#xff1a;表中的行数。reserved&#xff1a;表占用的总空间&#xff08;包括数据和索引&#xff09;。data&#xff1a;表数据占用的空间。index_size&#xff1a;索引占用的空间。unused&#xff1a;未使用的空…

《AI大模型应知应会100篇》第13篇:大模型评测标准:如何判断一个模型的优劣

第13篇&#xff1a;大模型评测标准&#xff1a;如何判断一个模型的优劣 摘要 近年来&#xff0c;大语言模型&#xff08;LLMs&#xff09;在自然语言处理、代码生成、多模态任务等领域取得了显著进展。然而&#xff0c;随着模型数量和规模的增长&#xff0c;如何科学评估这些模…

【区块链安全 | 第三十七篇】合约审计之获取私有数据(一)

文章目录 私有数据访问私有数据实例存储槽Solidity 中的数据存储方式1. storage(持久化存储)定长数组变长数组2. memory(临时内存)3. calldata可见性关键字私有数据存储风险安全措施私有数据 私有数据(Private Data)通常指的是只对特定主体可见或可访问的数据,在区块链…

项目管理(高软56)

系列文章目录 项目管理 文章目录 系列文章目录前言一、进度管理二、配置管理三、质量四、风险管理五、真题总结 前言 本节主要讲项目管理知识&#xff0c;这些知识听的有点意思啊。对于技术人想创业&#xff0c;单干的都很有必要听听。 一、进度管理 二、配置管理 三、质量 四…

OpenCV边缘检测方法详解

文章目录 引言一、边缘检测基础概念边缘类型 二、OpenCV中的边缘检测方法1. Sobel算子2. Scharr算子3. Laplacian算子4. Canny边缘检测 三、性能比较与选择建议四、总结 引言 边缘检测是计算机视觉和图像处理中的基础技术&#xff0c;它能有效识别图像中物体的边界&#xff0c…

Linux:shell运行原理+权限

1.shell的运行原理 如果我们打开了命令终端或者是xshell进行远程登录服务器&#xff0c;就会看到命令行&#xff0c;如下图所示&#xff1a; 这个命令行本身也是系统中一个运行起来的程序&#xff0c;它用来接收用户的输入&#xff0c;帮用户来执行指令&#xff0c;将运行结果展…

【LeetCode Solutions】LeetCode 160 ~ 165 题解

CONTENTS LeetCode 160. 相交链表&#xff08;简单&#xff09;LeetCode 162. 寻找峰值&#xff08;中等&#xff09;LeetCode 164. 最大间距&#xff08;中等&#xff09;LeetCode 165. 比较版本号&#xff08;中等&#xff09; LeetCode 160. 相交链表&#xff08;简单&#…

Openssl升级至openssl9.8p1含全部踩坑内容

1、安装依赖包基础条件 yum install gcc yum install gcc-c yum install perl yum install perl-IPC-Cmd yum install pam yum install pam-devel sudo yum install perl-Data-Dumper 问题一&#xff1a;提示yum不可用 镜像源问题更换阿里源即可 wget -O /etc/yum.repos.d/…

二战蓝桥杯所感

&#x1f334; 前言 今天是2025年4月12日&#xff0c;第十六届蓝桥杯结束&#xff0c;作为二战的老手&#xff0c;心中还是颇有不甘的。一方面&#xff0c;今年的题目比去年简单很多&#xff0c;另一方面我感觉并没有把能拿的分都拿到手&#xff0c;这是我觉得最遗憾的地方。不…

查看手机在线状态,保障设备安全运行

手机作为人们日常生活中不可或缺的工具&#xff0c;承载着沟通、工作、娱乐等多种功能。保障手机设备的安全运行是我们每个人都非常重要的任务&#xff0c;而了解手机的在线状态则是其中的一环。通过挖数据平台提供的在线查询工具&#xff0c;我们可以方便快捷地查询手机号的在…

#关于数据库中的时间存储

✅ 一、是否根据“机器当前时区”得到本地时间再转 UTC&#xff1f; 结论&#xff1a;是的&#xff0c;但仅对 TIMESTAMP 字段生效。 数据库&#xff08;如 MySQL&#xff09;在插入 TIMESTAMP 类型数据时&#xff1a; 使用当前会话的时区&#xff08;默认跟随系统时区&#…