8分钟,复习一遍红黑树!

8分钟,复习一遍红黑树 *** 红黑树节点定义**/// 颜色T key;// 关键字(键值)// 左孩子// 右孩子// 父结点​}​}​"(R)":"B");}}复制代码。 阅读详情

简单介绍

什么是红黑树?

红黑树(Red Black Tree)是一种平衡二叉搜索树。红黑树是一种特化的AVL树(平衡二叉树),都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。

关于AVL树可以查看我的博客:juejin.cn/post/713638…

为什么使用红黑树?

红黑树相比AVL树,在检索的时候效率其实差不多,都是通过平衡来二分查找。但对于插入删除等操作效率提高很多。

其次红黑树不像AVL树一样追求绝对的平衡,他允许局部很少的不完全平衡,这样对于效率影响不大,但省去了很多没有必要的调平衡操作,AVL树调平衡有时候代价较大,所以效率不如红黑树。红黑树只是做到了近似平衡,并不严格的平衡,所以在维护的成本上,要比AVL树要低。

红黑树的高度只比高度平衡的AVL树的高度(log2n)仅仅大了一倍,在性能上却好很多。

红黑树的插入、删除、查找各种操作性能都比较稳定。对于工程应用来说,要面对各种异常情况,为了支撑这种工业级的应用,我们更倾向于这种性能稳定的平衡二叉查找树。

红黑树的前身 234树(4阶B树)

它的效率不如红黑树

234树:是一种多叉树,它的每个节点最多有3个数据项和四个子节点。

节点分为3类:

  • 2节点:存储1个数据项,2个子节点

  • 3节点:存储2个数据项,3个子节点

  • 4节点:存储3个数据项,4个子节点

插入操作

我们将1~10依次插入到234树中,观察如何平衡调整。

首先是1~3的插入,插完以后已经变成一个4节点了。 

接着我们插入4,但是234树最多就是4节点,不能直接接在后面了。我们需要进行一步节点上溢操作。上溢中间的节点2,形成一个新的二节点,然后4就接在3后面。接着插入5就直接接在4后面形成4节点了。

 接着插入6,由于不能超过4节点又要进行节点上溢了,这里4上溢,然后形成的新树和2要进行合并。

 同理插入到8的时候也一样。

最后插入10的时候,将8进行上溢。

上溢并合并以后发现一久大于4节点,于是再将4进行上溢。

最终我们的234树为

234树与红黑树的关联

左边是一棵红黑树,我们将红色节点上移到和他们的父节点同一高度上,实际上就是一棵234树。

 所以红黑树和234树是具有等价性的。

红黑树的黑色节点个数=234树的节点数

234树的每一个节点中:

黑色节点必为父节点,红色节点为子节点(黑色中间,红色两边)

那么红黑树的每一种插入情况我们都可以转换为234树来理解。

23树

根据234树的逻辑我们可以完美套用到23树上,只需要知道234树最大是4节点,23树最大是3节点就行了。

红黑树的性质

红黑树是每个结点都带有颜色属性的二叉查找树,颜色或红色或黑色。在二叉搜索树的基础上,对于任何有效的红黑树有如下性质:

  • 性质1. 结点是红色或黑色。
  • 性质2. 根结点是黑色。
  • 性质3. 所有叶子都是黑色,且都为空。
  • 性质4. 每个红色结点的父节点 and 子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点)
  • 性质5. 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点

234树思维理解红黑树的操作

下面我们采用234树来辅助理解红黑树的插入操作和删除操作。

红黑树的插入操作

如果插入的是根节点,则为黑色。

其余情况插入的节点最开始一定为红色。(如果插入红色节点,仅有一种冲突状况,就是可能出现连续两个红色节点,这时候只需要旋转和变色进行调整。)

红黑树的插入操作分为12种情况:

  • 插入节点的父节点为黑色(4种):直接插入,不做调整。因为不会影响到它红黑树的性质。

  • 叔父节点不是红色(4种):变色+旋转。 

     对于(6)RR型 以234树的思维进行构想,我们会把13加在12的后面,但是红黑树不能有两个一样的红节点,所以我们需要进行变色,将中间的12变成黑色,两边的节点变成红色。  光变色还是不行的,因为黑色节点必须是父节点。我们得进行一次左旋操作。 

    对于(7)LL型 同理上面的RR型,直接染色加右旋即可。

    对于(5)RL型 我们实际上希望11是在10和12中间的,这样的话一次旋转操作是不够的,需要做两次旋转。

    首先插入节点染成黑色,然后插入节点的祖父节点也就是10染成红色。 

    染色到此为止,接下来是旋转操作 首先是父节点12进行一次右旋 

    然后祖父节点10进行一次左旋。最终到了下图所示的状态,这个是满足红黑树的性质的。

    对于(8)LR型 和上面的RL型同理,染色然后对父节点进行一次左旋,然后对祖父节点进行一次右旋。

  • 叔父节点是红色(4种上溢):变色。

    我们依旧用234树的思维来理解,这里无论是哪种情况都是肯定需要上溢的(将4抬高)

    然后开始染色,将插入节点的父节点和叔父节点染成黑色。上溢节点4作为新节点染成红色插入到上面一层。

 接着由于上溢节点在逻辑上是新插入到上一层的,于是依旧需要再用上面的方式再处理一次4这个节点。从整体上来看就是一种递归的形式。

其他的情况实际上都是一样的,首先将父节点和叔父节点染黑,将祖父节点染红,祖父作为新插入节点上溢,然后递归处理。(写代码的时候需要将4种情况写明,毕竟位置不同。)

红黑树的删除操作

什么是下溢?

讲删除之前首先还是讲下溢,上溢情况我们知道是超过了4节点了需要把中间节点抬高。下溢则是原来是一个2||3||4节点,但是删除掉下面一层的节点后子节点数目和2||3||4节点需要的数目对不上了。

讲完下溢之后我们开始正式讲删除操作

红黑树的删除操作比较复杂,分支很多,耐心看还是有收获的。

对于非叶子节点是转换为其前驱/后继节点的删除:

我们删除8这个节点。前驱节点就是6,后继节点就是10。我们这里选择前驱节点,进行交换操作,然后删除。

所以对应红黑树来说,删除操作最后都是对应红黑树最后两行的删除。

删除实际上分成两类

  • 红色节点可以直接删除(删除最后一层的节点对红黑树性质没有影响)

  • 黑色节点的删除

    • 有2个红色子节点的黑色节点(会转化成对子节点的删除,不作考虑)

    • 有1个红色子节点的黑色节点

      我们这里删除10这个节点

      用唯一的红色子节点12来代替被删除的节点10,然后删除掉节点10,最后将替代的红色节点12染成黑色。

      总结步骤:替代,删除,将替代节点染黑。

    • 黑色叶子节点(下溢的情况)

      • 删除节点为根节点,直接删除

      • 删除节点的兄弟节点为黑色

        • 兄弟节点有红色子节点(借用兄弟子节点修复)

          (1)当要删除的节点的兄弟节点有一个左子节点。

          对于这个红黑树我们删除36,它的兄弟就是20,然后有一个红色左子节点18。

          我们首先删除36,第一层本来就是4节点了,而删完后下面指向的数据项只有3个了,少了一个。所以我们需要通过旋转和变色来补上空缺的这个数据项。

          我们对删除节点的父节点25进行一次右旋,然后进行染色,将父节点染成红色,子节点染成黑色,这样才能保持性质。

          最后来看实际是当时被删除节点父节点25来替代了被删除节点的位置。

          (2)当要删除的节点的兄弟节点有一个右子节点。

          对于这个红黑树我们删除36,它的兄弟就是20,然后有一个红色右子节点22。

          和上面同理,我们对被删除节点的兄弟节点进行左旋,然后对父节点进行右旋,最后进行染色,父染红,子染黑。

          (3)当要删除的节点的兄弟节点有一个左子节点和一个右子节点。

          对于这个红黑树我们删除36,它的兄弟就是20,然后有一个红色左子节点18和一个红色右子节点22。

          这种我们可以自行选择左子节点或者右子节点进行修复。

          总结的话步骤就是:

          1.在删除节点后进行旋转

          2.中心节点染成父节点颜色

          3.两个子节点染为黑色

        • 兄弟节点没有红色子节点(兄弟节点无节点可借出,父节点向下合并)

          (1)父节点是红色

          对于下面的树,我们删除36这个节点。有一个红色父节点25,兄弟节点20无红色子节点。

          删除掉36后,第一层一样出现了下溢的情况。修复需要父节点25向下合并,与被删除节点的兄弟节点形成一个3节点。

          染色的话将父节点25染成黑色,将之前的兄弟节点20染成红色。修复就完成了。

          (2)父节点是黑色

          对于下面的树,我们删除36,父节点25为黑色,兄弟节点无红色子节点。

          这个时候我们删除后处理下溢再染色会变成这样,25的父节点又出现下溢了。于是我们把这个父节点当作一个被删除的节点进行一个递归的操作。

      • 删除节点的兄弟节点为红色(转变为黑色处理)

        删除节点36,兄弟节点20为红色。

        对被删除节点36的父节点25进行一次右旋,然后将被删除节点的兄弟节点20染成黑色,将父节点25染成红色。

        总结就是:父节点右旋,兄弟节点染黑,父节点染红,然后使用兄弟为黑色的方法进行修复。

红黑树的编码

红黑树节点定义

/**
     * 红黑树节点定义
     *
     * @author pixel-revolve
     * @date 2022/08/28
     */
public static class RBTNode<T extends Comparable<T>> {
    boolean color;        // 颜色
    T key;                // 关键字(键值)
    RBTNode<T> left;    // 左孩子
    RBTNode<T> right;    // 右孩子
    RBTNode<T> parent;    // 父结点
​
    public RBTNode(T key, boolean color, RBTNode<T> parent, RBTNode<T> left, RBTNode<T> right) {
        this.key = key;
        this.color = color;
        this.parent = parent;
        this.left = left;
        this.right = right;
    }
​
    public T getKey() {
        return key;
    }
​
    public String toString() {
        return ""+key+(this.color==RED?"(R)":"B");
    }
}

旋转操作

左旋

/**
     * 对红黑树的节点(x)进行左旋转
     *
     * 左旋示意图(对节点x进行左旋):
     *      px                              px
     *     /                               /
     *    x                               y
     *   /  \      --(左旋)-.           / \                #
     *  lx   y                          x  ry
     *     /   \                       /  \
     *    ly   ry                     lx  ly
     *
     *
     */
private void leftRotate(RBTNode<T> x) {
    // 设置x的右孩子为y
    RBTNode<T> y = x.right;
​
    // 将 “y的左孩子” 设为 “x的右孩子”;
    // 如果y的左孩子非空,将 “x” 设为 “y的左孩子的父亲”
    x.right = y.left;
    if (y.left != null)
        y.left.parent = x;
​
    // 将 “x的父亲” 设为 “y的父亲”
    y.parent = x.parent;
​
    if (x.parent == null) {
        this.mRoot = y;            // 如果 “x的父亲” 是空节点,则将y设为根节点
    } else {
        if (x.parent.left == x)
            x.parent.left = y;    // 如果 x是它父节点的左孩子,则将y设为“x的父节点的左孩子”
        else
            x.parent.right = y;    // 如果 x是它父节点的左孩子,则将y设为“x的父节点的左孩子”
    }
​
    // 将 “x” 设为 “y的左孩子”
    y.left = x;
    // 将 “x的父节点” 设为 “y”
    x.parent = y;
}

右旋

/**
 * 对红黑树的节点(y)进行右旋转
 *
 * 右旋示意图(对节点y进行左旋):
 *            py                               py
 *           /                                /
 *          y                                x
 *         /  \      --(右旋)-.            /  \                     #
 *        x   ry                           lx   y
 *       / \                                   / \                   #
 *      lx  rx                                rx  ry
 *
 */
private void rightRotate(RBTNode<T> y) {
    // 设置x是当前节点的左孩子。
    RBTNode<T> x = y.left;
​
    // 将 “x的右孩子” 设为 “y的左孩子”;
    // 如果"x的右孩子"不为空的话,将 “y” 设为 “x的右孩子的父亲”
    y.left = x.right;
    if (x.right != null)
        x.right.parent = y;
​
    // 将 “y的父亲” 设为 “x的父亲”
    x.parent = y.parent;
​
    if (y.parent == null) {
        this.mRoot = x;            // 如果 “y的父亲” 是空节点,则将x设为根节点
    } else {
        if (y == y.parent.right)
            y.parent.right = x;    // 如果 y是它父节点的右孩子,则将x设为“y的父节点的右孩子”
        else
            y.parent.left = x;    // (y是它父节点的左孩子) 将x设为“x的父节点的左孩子”
    }
​
    // 将 “y” 设为 “x的右孩子”
    x.right = y;
​
    // 将 “y的父节点” 设为 “x”
    y.parent = x;
}

插入操作

看着很多,但是我做了很多的注释,配合上面的图看理解不难。

/**
     * 红黑树插入修正函数
     *
     * 在向红黑树中插入节点之后(失去平衡),再调用该函数;
     * 目的是将它重新塑造成一颗红黑树。
     *
     * 参数说明:
     *     node 插入的结点        // 对应《算法导论》中的z
     */
private void insertFixUp(RBTNode<T> node) {
    RBTNode<T> parent, gparent;
    // 若“父节点存在,并且父节点的颜色是黑色”
    // 不做任何调整
​
    // 若“父节点存在,并且父节点的颜色是红色”
    while (((parent = parentOf(node))!=null) && isRed(parent)) {
        gparent = parentOf(parent);
​
        //若“父节点”是“祖父节点的左孩子”
        if (parent == gparent.left) {
            // Case 1条件:叔叔节点是红色 我们直接变色处理就行
            RBTNode<T> uncle = gparent.right;
            if ((uncle!=null) && isRed(uncle)) {
                setBlack(uncle);
                setBlack(parent);
                setRed(gparent);
                node = gparent;
                continue;
            }
​
            // 到这里,叔父节点是黑色,我们分成功左右孩讨论
            // Case 2条件:叔叔是黑色,且当前节点是右孩子
            if (parent.right == node) {
                RBTNode<T> tmp;
                leftRotate(parent);
                // parent和插入节点进行变量逻辑交换
                tmp = parent;
                parent = node;
                node = tmp;
            }
​
            // Case 3条件:叔叔是黑色,且当前节点是左孩子。
            setBlack(parent);// 将插入节点现在是parent的位置,染成黑色
            setRed(gparent);// 将插入节点的祖父节点(位置没变)染成红色
            rightRotate(gparent); // 完成第二次旋转,对于RR和LL型,此次旋转无效
        } else {    //若“z的父节点”是“z的祖父节点的右孩子”
            // Case 1条件:叔叔节点是红色,直接染色处理
            RBTNode<T> uncle = gparent.left;
            if ((uncle!=null) && isRed(uncle)) {
                setBlack(uncle);
                setBlack(parent);
                setRed(gparent);
                node = gparent;
                continue;
            }
​
            // Case 2条件:叔叔是黑色,且当前节点是左孩子
            if (parent.left == node) {
                RBTNode<T> tmp;
                rightRotate(parent);
                tmp = parent;
                parent = node;
                node = tmp;
            }
​
            // Case 3条件:叔叔是黑色,且当前节点是右孩子。
            setBlack(parent);// 将插入节点现在是parent的位置,染成黑色
            setRed(gparent);// 将插入节点的祖父节点(位置没变)染成红色
            leftRotate(gparent); // 完成第二次旋转,对于RR和LL型,此次旋转无效
        }
    }
​
    // 将根节点设为黑色
    setBlack(this.mRoot);
}
​
/**
     * 将结点插入到红黑树中
     *
     * 参数说明:
     *     node 插入的结点        // 对应《算法导论》中的node
     */
private void insert(RBTNode<T> node) {
    int cmp;
    RBTNode<T> y = null;
    RBTNode<T> x = this.mRoot;
​
    // 1. 将红黑树当作一颗二叉查找树,将节点添加到二叉查找树中。
    while (x != null) {
        y = x;
        cmp = node.key.compareTo(x.key);
        if (cmp < 0)
            x = x.left;
        else
            x = x.right;
    }
    // y用来作为node的父亲
    node.parent = y;
    // 执行node的插入
    if (y!=null) {
        cmp = node.key.compareTo(y.key);
        if (cmp < 0)
            y.left = node;
        else
            y.right = node;
    } else {
        this.mRoot = node;
    }
​
    // 2. 设置节点的颜色为红色(我们只插入红色节点)
    node.color = RED;
​
    // 3. 将它重新修正为一颗二叉查找树
    insertFixUp(node);
}
​
/**
     * 新建结点(key),并将其插入到红黑树中
     * 对外的接口
     *
     * 参数说明:
     *     key 插入结点的键值
     */
public void insert(T key) {
    RBTNode<T> node= new RBTNode<T>(key, BLACK, null, null, null);
    insert(node);
}

 

删除操作

代码的结构和插入操作是一样的,同样做了大量注释。请放心食用。

/**
     * 红黑树删除修正函数
     *
     * 在从红黑树中删除插入节点之后(红黑树失去平衡),再调用该函数;
     * 目的是将它重新塑造成一颗红黑树。
     *
     * 参数说明:
     *     node 待修正的节点
     */
private void removeFixUp(RBTNode<T> node, RBTNode<T> parent) {
    RBTNode<T> other;
    // 当被删除节点为黑色节点
    while ((node==null || isBlack(node)) && (node != this.mRoot)) {
        if (parent.left == node) {// 被删除的是父节点的左孩子
            other = parent.right;
            // Case 1: x的兄弟w是红色的。(借用兄弟节点修复)
            if (isRed(other)) {
                setBlack(other);
                setRed(parent);
                leftRotate(parent);
                other = parent.right;
            }
            // Case 2: x的兄弟w是黑色,且w的俩个孩子也都是黑色的。兄弟节点无红色子节点(兄弟无节点借出,父节点向下合并)
            if ((other.left==null || isBlack(other.left)) &&
                (other.right==null || isBlack(other.right))) {
                setRed(other);
                node = parent;
                parent = parentOf(node);
            } else {// 兄弟节点有红子节点
                // Case 3: x的兄弟w是黑色的,并且w的左孩子是红色,右孩子为黑色。兄弟节点有一个红色左子节点
                if (other.right==null || isBlack(other.right)) {
                    setBlack(other.left);
                    setRed(other);
                    rightRotate(other);
                    other = parent.right;
                }
                // Case 4: x的兄弟w是黑色的;并且w的右孩子是红色的,左孩子任意颜色。兄弟节点有一个红色右子节点
                setColor(other, colorOf(parent));
                setBlack(parent);
                setBlack(other.right);
                leftRotate(parent);
                node = this.mRoot;
                break;
            }
        } else {// 被删除的是父节点的左孩子
            other = parent.left;
            // Case 1: x的兄弟w是红色的(借用兄弟节点修复)
            if (isRed(other)) {
                setBlack(other);
                setRed(parent);
                rightRotate(parent);
                other = parent.left;
            }
            // Case 2: x的兄弟w是黑色,且w的俩个孩子也都是黑色的。兄弟节点无红色子节点(兄弟无节点借出,父节点向下合并)
            if ((other.left==null || isBlack(other.left)) &&
                (other.right==null || isBlack(other.right))) {
                setRed(other);
                node = parent;
                parent = parentOf(node);
            } else {// 兄弟节点有红子节点
                // Case 3: x的兄弟w是黑色的,并且w的左孩子是红色,右孩子为黑色。兄弟节点有一个红色左子节点
                if (other.left==null || isBlack(other.left)) {
                    setBlack(other.right);
                    setRed(other);
                    leftRotate(other);
                    other = parent.left;
                }
                // Case 4: x的兄弟w是黑色的;并且w的右孩子是红色的,左孩子任意颜色。
                setColor(other, colorOf(parent));
                setBlack(parent);
                setBlack(other.left);
                rightRotate(parent);
                node = this.mRoot;
                break;
            }
        }
    }
​
    if (node!=null)
        setBlack(node);
}
​
/**
     * 删除结点(node),并返回被删除的结点
     *
     * 参数说明:
     *     node 删除的结点
     */
private void remove(RBTNode<T> node) {
    RBTNode<T> child, parent;
    boolean color;
​
    // 被删除节点的"左右孩子都不为空"的情况。对于非叶子节点转换为对前驱/后继的删除
    if ( (node.left!=null) && (node.right!=null) ) {
        // 被删节点的后继节点。(称为"取代节点")
        // 用它来取代"被删节点"的位置,然后再将"被删节点"去掉。
        RBTNode<T> replace = node;
​
        // 获取后继节点
        replace = replace.right;
        while (replace.left != null)
            replace = replace.left;
​
        // "node节点"不是根节点(只有根节点不存在父节点)
        if (parentOf(node)!=null) {
            if (parentOf(node).left == node)
                parentOf(node).left = replace;
            else
                parentOf(node).right = replace;
        } else {
            // "node节点"是根节点,更新根节点。
            this.mRoot = replace;
        }
​
        // child是"取代节点"的右孩子,也是需要"调整的节点"。
        // "取代节点"肯定不存在左孩子!因为它是一个后继节点。
        child = replace.right;
        parent = parentOf(replace);
        // 保存"取代节点"的颜色
        color = colorOf(replace);
​
        // "被删除节点"是"它的后继节点的父节点"
        if (parent == node) {
            parent = replace;
        } else {
            // child不为空
            if (child != null)
                setParent(child, parent);
            parent.left = child;
​
            replace.right = node.right;
            setParent(node.right, replace);
        }
​
        replace.parent = node.parent;
        replace.color = node.color;
        replace.left = node.left;
        node.left.parent = replace;
​
        // 完成替换后开始真正的删除和调整
        // Case1 红色节点可以直接删除(删除最后一层的节点对红黑树性质没有影响)
​
        // Case2 黑色节点的删除调整
        if (color == BLACK)
            removeFixUp(child, parent);
​
        node = null;
        return ;
    }
​
    if (node.left !=null) {
        child = node.left;
    } else {
        child = node.right;
    }
​
    parent = node.parent;
    // 保存"取代节点"的颜色
    color = node.color;
​
    if (child!=null)
        child.parent = parent;
​
    // "node节点"不是根节点
    if (parent!=null) {
        if (parent.left == node)
            parent.left = child;
        else
            parent.right = child;
    } else {
        this.mRoot = child;
    }
​
    if (color == BLACK)
        removeFixUp(child, parent);
    node = null;
}
​
/**
     * 删除结点(z),并返回被删除的结点
     * 对外接口
     *
     * 参数说明:
     *     tree 红黑树的根结点
     *     z 删除的结点
     */
public void remove(T key) {
    RBTNode<T> node;
​
    if ((node = search(mRoot, key)) != null)
        remove(node);
}

 其他操作

public class RedBlackTree<T extends Comparable<T>> {
​
    public RBTNode<T> mRoot;    // 根结点
​
    private static final boolean RED   = false;
    private static final boolean BLACK = true;
    
     public RedBlackTree() {
        mRoot=null;
    }
​
    private RBTNode<T> parentOf(RBTNode<T> node) {
        return node!=null ? node.parent : null;
    }
​
    /**
     * 返回红黑树节点的颜色
     *
     * @param node 节点
     * @return boolean
     */
    private boolean colorOf(RBTNode<T> node) {
        return node!=null ? node.color : BLACK;
    }
    private boolean isRed(RBTNode<T> node) {
        return ((node!=null)&&(node.color==RED)) ? true : false;
    }
    private boolean isBlack(RBTNode<T> node) {
        return !isRed(node);
    }
    private void setBlack(RBTNode<T> node) {
        if (node!=null)
            node.color = BLACK;
    }
    private void setRed(RBTNode<T> node) {
        if (node!=null)
            node.color = RED;
    }
    private void setParent(RBTNode<T> node, RBTNode<T> parent) {
        if (node!=null)
            node.parent = parent;
    }
    private void setColor(RBTNode<T> node, boolean color) {
        if (node!=null)
            node.color = color;
    }
    
    /*
     * (递归实现)查找"红黑树x"中键值为key的节点
     */
    private RBTNode<T> search(RBTNode<T> x, T key) {
        if (x==null)
            return x;
​
        int cmp = key.compareTo(x.key);
        if (cmp < 0)
            return search(x.left, key);
        else if (cmp > 0)
            return search(x.right, key);
        else
            return x;
    }
    
    /*
     * (非递归实现)查找"红黑树x"中键值为key的节点
     */
    private RBTNode<T> iterativeSearch(RBTNode<T> x, T key) {
        while (x!=null) {
            int cmp = key.compareTo(x.key);
​
            if (cmp < 0)
                x = x.left;
            else if (cmp > 0)
                x = x.right;
            else
                return x;
        }
​
        return x;
    }
​
    public RBTNode<T> iterativeSearch(T key) {
        return iterativeSearch(mRoot, key);
    }
​
    /*
     * 查找最小结点:返回tree为根结点的红黑树的最小结点。
     */
    private RBTNode<T> minimum(RBTNode<T> tree) {
        if (tree == null)
            return null;
​
        while(tree.left != null)
            tree = tree.left;
        return tree;
    }
​
    public T minimum() {
        RBTNode<T> p = minimum(mRoot);
        if (p != null)
            return p.key;
​
        return null;
    }
​
    /*
     * 查找最大结点:返回tree为根结点的红黑树的最大结点。
     */
    private RBTNode<T> maximum(RBTNode<T> tree) {
        if (tree == null)
            return null;
​
        while(tree.right != null)
            tree = tree.right;
        return tree;
    }
​
    public T maximum() {
        RBTNode<T> p = maximum(mRoot);
        if (p != null)
            return p.key;
​
        return null;
    }
​
​
    public RBTNode<T> search(T key) {
        return search(mRoot, key);
    }
​
    /**
     * 销毁红黑树
     */
    private void destroy(RBTNode<T> tree) {
        if (tree==null)
            return ;
​
        if (tree.left != null)
            destroy(tree.left);
        if (tree.right != null)
            destroy(tree.right);
​
        tree=null;
    }
​
    public void clear() {
        destroy(mRoot);
        mRoot = null;
    }
​
​
    ...
}

功能测试

我们这里引入一个红黑树展示工具类,它能形象的为我们打印红黑树。

public class TreeOperation {

      /*
    树的结构示例:
              1
            /   \
          2       3
         / \     / \
        4   5   6   7
    */

    /**
     * 用于获得树的层数
     *
     * @param root
     * @return
     */
    public static int getTreeDepth(RedBlackTree.RBTNode<?> root) {
        return root == null ? 0 : (1 + Math.max(getTreeDepth(root.left), getTreeDepth(root.right)));
    }

    private static void writeArray(RedBlackTree.RBTNode<?> currNode, int rowIndex, int columnIndex, String[][] res, int treeDepth) {
        // 保证输入的树不为空
        if (currNode == null) {
            return;
        }
        // 先将当前节点保存到二维数组中
        res[rowIndex][columnIndex] = String.format("%s-%s ", currNode.key, "true".equals(String.valueOf(currNode.color)) ? "R" : "B");

        // 计算当前位于树的第几层
        int currLevel = ((rowIndex + 1) / 2);
        // 若到了最后一层,则返回
        if (currLevel == treeDepth) {
            return;
        }
        // 计算当前行到下一行,每个元素之间的间隔(下一行的列索引与当前元素的列索引之间的间隔)
        int gap = treeDepth - currLevel - 1;

        // 对左儿子进行判断,若有左儿子,则记录相应的"/"与左儿子的值
        if (currNode.left != null) {
            res[rowIndex + 1][columnIndex - gap] = "/";
            
            writeArray(currNode.left, rowIndex + 2, columnIndex - gap * 2, res, treeDepth);
        }

        // 对右儿子进行判断,若有右儿子,则记录相应的""与右儿子的值
        if (currNode.right != null) {
            res[rowIndex + 1][columnIndex + gap] = <img src=""\";" alt="" width="50%" />
            writeArray(currNode.right, rowIndex + 2, columnIndex + gap * 2, res, treeDepth);
        }

    }


    public static void show(RedBlackTree.RBTNode<?> root) {
        if (root == null) {
            System.out.println("EMPTY!");
        }
        // 得到树的深度
        int treeDepth = getTreeDepth(root);

        // 最后一行的宽度为2的(n - 1)次方乘3,再加1
        // 作为整个二维数组的宽度
        int arrayHeight = treeDepth * 2 - 1;
        int arrayWidth = (2 << (treeDepth - 2)) * 3 + 1;
        // 用一个字符串数组来存储每个位置应显示的元素
        String[][] res = new String[arrayHeight][arrayWidth];
        // 对数组进行初始化,默认为一个空格
        for (int i = 0; i < arrayHeight; i++) {
            for (int j = 0; j < arrayWidth; j++) {
                res[i][j] = " ";
            }
        }

        // 从根节点开始,递归处理整个树
        // res[0][(arrayWidth + 1)/ 2] = (char)(root.val + '0');
        writeArray(root, 0, arrayWidth / 2, res, treeDepth);

        // 此时,已经将所有需要显示的元素储存到了二维数组中,将其拼接并打印即可
        for (String[] line : res) {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < line.length; i++) {
                sb.append(line[i]);
                if (line[i].length() > 1 && i <= line.length - 1) {
                    i += line[i].length() > 4 ? 2 : line[i].length() - 1;
                }
            }
            System.out.println(sb.toString());
        }
    }
}

 

接下来开始测试功能

测试插入:

public final int[] a = {10, 40, 30, 60, 90, 70, 20, 50, 80};
​
@Test
public void insertTest(){
    int i, ilen = a.length;
    RedBlackTree<Integer> tree=new RedBlackTree<>();
​
    for(i=0; i<ilen; i++) {
        tree.insert(a[i]);
    }
​
    TreeOperation.show(tree.mRoot);
}

测试结果:

            30-R           
         /     \         
      10-R          60-B     
        \       /   \    
          20-B  40-R      80-R 
               \     / \ 
                50-B  70-B  90-B 

测试删除:

public final int[] a = {10, 40, 30, 60, 90, 70, 20, 50, 80};
   
@Test
public void deleteTest(){
    int i, ilen = a.length;
    RedBlackTree<Integer> tree=new RedBlackTree<>();
​
    for(i=0; i<ilen; i++) {
        tree.insert(a[i]);
    }
​
    tree.remove(30);
​
    TreeOperation.show(tree.mRoot);
}

测试结果:

            40-R           
         /     \         
      10-R          60-B     
        \       /   \    
          20-B  50-R      80-R 
                     / \ 
                    70-B  90-B 

小结

红黑树 并不追求“完全平衡 ”——它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能。

红黑树能够以 O(logn) 的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。当然,还有一些更好的,但实现起来更复杂的数据结构 能够做到一步旋转之内达到平衡,但红黑树能够给我们一个比较“便宜”的解决方案。

总得来说红黑树是一个查找,删除效率都很好的树结构。并且操作稳定,是适用于工业化的数据结构。

相信看完本篇文章后,你能对这个底层偏爱的数据结构有所了解。

面试官:为什么有了平衡二叉树,还要选择红黑树这种数据结构? 首先讲解为什么要使用红黑树之前,有必要先了解二叉查找树和平衡二叉树,然后继续介绍红黑树,带着这个问题,让我们一起来学习今天的内容吧。 二叉查找树 二叉查找树的特点就是左子树的节点值比父亲节点小,而右子树的节点值比父亲节点大,如下图: 二叉查找树中对于目标节点的查找过程类似于有序数组的二分查找,快速找到某个节点。n 个节点的二叉查找树,正常的情况下,查找的时间复杂度为 O(logn)。 为什么说是正常的情况下呢? 是因为二叉查找树有一种极端的存在,二叉树的大部分子节点都比父节点值小,然后导致所有的数据偏向左 阅读详情

相关推荐

Java面试复习Day 2

JDK 1.8 的 put 流程通过引入‌红黑树‌和‌尾插法‌,在保持高效插入的同时,显著提升了极端冲突下的查询性能,并消除了并发扩容的死循环隐患。在实际开发中,若涉及高并发场景,仍建议使用 ConcurrentHashMap。

lotus_csdn888的博客 322

2-3-4树与红黑树

概述 234树和红黑树是完全等价的,红黑树本质上就是234树;由于234树是一棵高度平衡的树,实现起来比较复杂,所以一般通过红黑树来实现234树,而红黑树同样保证了O(logN)的时间内完成查找、插入和删除操作。 红黑树的定义 节点只能是黑色或者红色。 根节点必须是黑色。 每个叶子节点是黑色节点(称之为NIL节点,又被称为黑哨兵);可以理解为红黑树中每个节点都有两个子节点,除了黑色的空节点。 每个红色节点的两个子节点都是黑色(或者说从每个叶子节点到根的所有路径上不能有两个连续的红色节点)。 从任一节点到它

抽象的螺旋 1096

Java八股文复习指南:理解原理并构建知识体系

在Java技术面试中,集合框架、JVM内存模型与并发机制是考察开发者基本功的核心知识域。理解HashMap底层为何引入红黑树、synchronized与volatile的语义差异、Spring IoC容器的Bean生命周期,以及MySQL索引的B+树设计,不仅是为了应对面试提问,更是排查线上CPU飙升、内存溢出、接口变慢等现实问题的工程能力基石。这些基础知识的价值在于帮助开发者从运行机制层面理解系统行为,在技术选型与性能调优中做出合理的判断。无论是校招还是社招,通过知识树梳理、费曼技巧自我讲解、以及结合实战

weixin_33913332的博客 556

红黑树(基于2-3树等价)

红黑树是一种常见的自平衡二叉查找树,经常使用于关联数组、字典,在各类语言的底层实现中被普遍应用,Java的TreeMap和TreeSet就是基于红黑树实现的。

数据工匠记 456

七天Java面试八股文冲刺:从HashMap到JVM的体系化复习法

技术面试中,基础知识的扎实程度往往决定成败。HashMap底层原理、JVM内存模型、并发锁机制等核心概念,不仅是面试高频考点,更是日常开发中的技术基石。理解这些原理的价值在于:既能应对面试官的连环追问,也能指导实际工程中的性能优化与故障排查。然而,视频学习往往存在被动输入、信息密度低的陷阱,而文字资料配合主动回忆与刻意练习,能在短时间内构建完整的知识网络。本文以七天冲刺为例,拆解Java基础、JVM、并发编程、Spring、MySQL与Redis等模块的复习节奏,并强调通过模拟面试将“背得出”转化为“讲得好

weixin_33552698的博客 271

Java后端面试7天高效复习:核心考点、高频八股与实战策略

在软件工程领域,面试准备是每位开发者职业生涯中的关键环节,尤其对于Java后端岗位,其知识体系庞大且深入。理解计算机科学的基础原理,如数据结构、算法、操作系统和网络,是构建扎实技术能力的基石。从原理层面看,掌握核心数据结构(如HashMap的哈希表与红黑树实现)、并发编程的线程安全机制(如锁、CAS、AQS)以及JVM的内存管理与垃圾回收,是应对技术考察的根本。这些技术的价值在于,它们直接决定了系统的高并发处理能力、内存使用效率及稳定性,是构建高性能、可扩展后端服务的核心。在实际应用场景中,无论是设计秒杀系

cigang4063的博客 406

2-3-4树与红黑树的转换规则及插入、删除操作等价示例

2-3-4树与红黑树是多叉树与二叉树的关系,看上去完全不同,但是在某种意义上它们又是完全相同的,一个可以通过应用一些简单的规则变成另一个,而且使他们保持平衡的操作也是一样,数学上称他们为同构。 转化规则 应用如下三条规则可以将2-3-4树转化为红黑树: 把2-3-4树中的每个2-节点转化为红黑树的黑色节点。 把每个3-节点转化为一个子节点和一个父节点,子节点有两个自己的子节点:W和X或X和Y。父节点有另一个子节点:Y或W。哪个节点变成子节点或父节点都无所谓。子节点涂成红色,父节点涂成...

ystyaoshengting的专栏 996

7天速成八股文:Java后端面试高频考点复习清单

技术面试的核心挑战,在于将零散的基础知识组织成清晰、有逻辑的口头表达。对于Java后端岗位而言,HashMap的底层结构、JVM内存模型、并发机制、TCP三次握手、MySQL索引与Redis缓存等,都是面试官高频考察的知识点。理解这些原理,并形成结构化的答题框架,是提升面试表现的关键。八股文复习的本质,是借助系统化的考点索引,将“知道”转化为“能讲出来”。无论是备战春招、暑期实习,还是准备跳槽,一套覆盖Java核心、操作系统、网络、数据库与分布式的7天速成计划,都能帮助候选人在高压环境下稳定输出,将背诵内化

weixin_30621959的博客 494

【红黑树】——五分钟搞懂什么是红黑树(全程图解)

前言 红黑树,对很多童鞋来说,是既熟悉又陌生。熟悉是因为在校学习期间,准备面试时,这是重点。然后经过多年的荒废,如今已经忘记的差不多了。如果正在看文章的你,马上快要毕业,面临着找工作的压力;又或者你觉得需要将这块知识重新复习一遍;又或者只是看看,那么恭喜你,赚到了。那么我将带领大家重新认识下红黑树,用简单的语言,搞懂红黑树。 在学习红黑树之前,咱们需要先来理解下二叉查找树(BST)。 二...

我爱吃土豆 6869

2026计算机面试八股文速成:7天高效复习路线

技术面试中,基础知识考察始终是筛选候选人的第一道关卡,而八股文正是对原理理解深度的直接检验。无论是JVM内存模型、并发编程的锁机制,还是MySQL索引优化与Redis持久化策略,面试官真正关注的是候选人能否讲清“为什么”以及“如何排查”,而非机械背诵结论。在2026年求职竞争愈发激烈的背景下,掌握核心考点的底层逻辑、能够结构化表达技术方案,成为斩获Offer的关键能力。本文梳理了Java、C++、前端、测试等方向的高频考点,结合面试官深挖的经典题目与答题框架,提供一份可直接落地的7天速成复习路线,帮助求职者

weixin_34146805的博客 342

k3s快速搭建k8s集群环境

k8s 搭建方式有多种,如果你是内网环境,希望占用服务器资源少或是边缘系统(如ARM系统),强烈推荐您使用 k3s进行安装 目录k3s 简介k3s架构中文官方文档三台服务器快速搭建k8s集群安装说明步骤1 修改主机名,保持唯一步骤2 安装docker步骤3 安装master节点步骤4 安装worker节点加入集群步骤5 查看集群步骤6 部署 k8s仪表盘 dashboard k3s 简介 轻量级 Kubernetes。安装简单,内存只有一半,所有的二进制都不到 100MB K3s 是一个完全符合 Ku.

gzt19881123的专栏 2445

5分钟了解红黑树二叉树

前戏 红黑树,对很多童鞋来说,是既熟悉又陌生。熟悉是因为在校学习期间,准备面试时,这是重点。然后经过多年的荒废,如今已经忘记的差不多了。如果正在看文章的你,马上快要毕业,面临着找工作的压力;又或者你觉得需要将这块知识重新复习一遍;又或者只是看看,那么恭喜你,赚到了。那么我将带领大家重新认识下红黑树,用简单的语言,搞懂红黑树。 在学习红黑树之前,咱们需要先来理解下二叉查找树(BST)...

没有伞的孩子必须努力奔跑!—小林 429

彻底清理Easy Connect残留:从服务到驱动的完整卸载指南

在企业网络接入场景中,远程客户端常因内核驱动、系统服务和网络过滤组件导致卸载残留,进程无法结束、文件被锁定、注册表项顽固存在。其原理在于驱动挂载网络协议栈,服务守护进程,卸载时资源占用导致清理不彻底。彻底清理的技术价值在于恢复系统纯净、避免网络异常和软件冲突。典型应用场景包括企业办公机、个人笔记本更换或重装客户端。本文针对深信服Easy Connect,提供从停止服务、删除驱动到清理注册表和虚拟网卡的分步操作链路,并给出验证方法和避坑经验。

weixin_31292729的博客 246

五分钟搞懂什么是红黑树(全程图解)

这篇文章写得简单易懂,很容易看懂,转载一下http://www.360doc.com/content/19/0510/10/63985477_834745870.shtml 前言 红黑树,对很多童鞋来说,是既熟悉又陌生。熟悉是因为在校学习期间,准备面试时,这是重点。然后经过多年的荒废,如今已经忘记的差不多了。如果正在看文章的你,马上快要毕业,面临着找工作的压力;又或者你觉得需要将这块知识重新复...

Hollake的博客 820

TP4056在单节锂电池充电管理中的创新应用与电路优化

本文深入探讨了TP4056线性充电管理芯片在单节锂电池充电电路中的创新应用与优化方案。针对“边充边用”的核心挑战,文章详细解析了利用PMOS管构建智能供电切换电路的原理,实现了外部电源与电池供电的无缝、安全切换,有效杜绝了电池“边充边放”的风险。同时,提供了关键的元器件选型、PCB布局布线技巧及常见问题排查指南,为创客和硬件开发者提供了高性价比、高可靠性的锂电池电源管理实践方案。

rock5的博客 501

ARM功耗管理软件之DVFS&AVS

思考:功耗管理软件栈及示例?WFI&WFE?时钟&电源树?DVFS&AVS?

AI-安全-功耗 1232

5分钟搞定!javasuper关键字

一、前言 红黑树,是一种高效的自平衡二叉查找树 Rudolf Bayer 于1978年发明红黑树,在当时被称为对称二叉 B 树(symmetric binary B-trees)。后来,在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的红黑树。 红黑树具有良好的效率,它可在近似O(logN) 时间复杂度下完成插入、删除、查找等操作,因此红黑树在业界也被广泛应用,比如 Java 中的 TreeMap,JDK 1.8 中的 HashMap、C++ STL 中的 map

m0_56604931的博客 166

Python爬虫实战:手把手教你园林植物分类全维度字典自动化采集实战!

我长期专注 Python 爬虫工程化实战,主理专栏 👉 《Python爬虫实战》:从采集策略到反爬对抗,从数据清洗到分布式调度,持续输出可复用的方法论与可落地案例。内容主打一个“能跑、能用、能扩展”,让数据价值真正做到——抓得到、洗得净、用得上。

喵手的博客 388

Java面试高效复习指南:从问题驱动到实战表达

在技术面试准备中,构建清晰的知识体系和高效的复习路径是关键。其核心原理在于将传统的知识驱动复习,转变为以面试问题为目标的驱动模式,这能显著提升学习效率和面试表现。从技术价值看,这种方法不仅帮助求职者系统化掌握Java核心、并发编程、JVM、MySQL及Spring生态等关键技术栈,更能培养结构化思维和场景化解决问题的能力。在实际应用场景中,开发者常面临知识碎片化、准备与考察脱节等痛点。本文聚焦于如何通过定义能力画像、构建问题树、进行刻意练习等具体策略,帮助有经验的Java开发者,特别是那些需要应对高并发、分

weixin_34006965的博客 336

【Godot】【工程】中文字体与多语言(缺字/体积/打包:一次性配置到位)

本文介绍了Godot游戏开发中的中文字体配置与多语言支持方案。主要内容包括:1)中文字体的全局与局部设置;2)使用子集化技术减小字体体积;3)多语言资源管理及翻译系统实现;4)语言切换机制与控件刷新方法;5)字体回退策略优化显示效果;6)打包优化建议。文章提供了从字体导入到多语言实现的完整解决方案,帮助开发者系统性解决中文显示和多语言支持问题,同时优化资源体积和性能表现。配套案例可供学习参考。

技术小甜甜:传播可落地技术,帮助个人和企业降低解决真实场景问题的成本 598

芒果YOLOv8改进52:主干Backbone篇之GhostNetV2:提出原创GhostNetV2 架构升级版:引入长距离注意力机制增强廉价操作,构建更强端侧轻量型骨干,打造高效轻量级检测器

长距离注意力机制增强廉价操作,打造轻量级检测器

包括YOLOv5、YOLOv7、YOLOv8等模型改进 1612

2026计算机面试八股文核心考点与高效复习全攻略

在计算机技术面试中,基础知识常以标准化问答形式出现,这就是俗称的“八股文”。它覆盖计算机网络、操作系统、数据结构与算法、数据库、编程语言及框架原理等核心模块,是面试官快速验证候选人基本功的最低成本方式。掌握这些概念与原理,不仅有助于通过笔试面试,更能为系统设计、性能优化、故障排查等工程实践打下坚实基础。无论是校招还是社招,无论是Java、前端、嵌入式还是测试方向,高频考点如TCP三次握手、HashMap底层原理、Redis缓存问题、索引失效场景等,都要求候选人既懂概念又能讲出实际应用。面对2026年面试题型

386

【深度学习时间序列预测案例】零基础入门经典深度学习时间序列预测项目实战(附代码+数据集+原理介绍)

本专栏整理了《深度学习时间序列预测案例》,内包含了各种不同的基于深度学习模型的时间序列预测方法,例如LSTM、GRU、CNN(一维卷积、二维卷积)、LSTM-CNN、BiLSTM、Self-Attention、LSTM-Attention、Transformer等经典模型,包含项目原理以及源码,每一个项目实例都附带有完整的代码+数据集。

优快云 精品推荐 4万+

Java面试八股文合集:500道核心知识点与复习方法

在Java技术栈中,基础知识的扎实程度往往决定面试成败。集合框架、JVM内存模型、并发编程、Spring容器及MySQL索引等核心模块,既是日常开发的基石,也是大厂面试的高频考点。理解HashMap的底层结构、synchronized的锁升级、JVM垃圾回收算法等原理,不仅能从容应对连环追问,更能帮助开发者构建系统化知识体系。透过八股文训练,将理论要点转化为工程实践中的排查与优化能力,在技术面试与真实项目中均具重要价值。本文结合面试官追问逻辑,系统梳理500道高频面试题,覆盖Java基础、并发、框架与数据库

weixin_34116482的博客 321

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

lwip-2.0.3

lwip是瑞典计算机科学院(SICS)的Adam Dunkels 开发的一个小型开源的TCP/IP协议栈。实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用。

Java后端面试一周冲刺:从八股文到项目场景的复习策略

面试准备是计算机求职者常遇到的挑战,尤其Java后端领域,既要求扎实的基础原理,也强调工程问题的分析与解决。从集合框架、JVM内存到并发机制,这些核心知识构成了技术面试的“八股文”部分,而项目场景题则考验候选人如何将知识应用于线上问题排查、缓存设计与性能优化等真实场景。高效的复习并非死记硬背,而是建立知识主干,围绕高频考点形成可复用的答题框架,再结合自身项目经历进行结构化表达。本文提供一套七天冲刺计划,帮助读者在有限时间内梳理知识体系、验证薄弱环节,并在面试中稳定输出技术判断力。

weixin_34183910的博客 343

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

后端面试八股文怎么刷?7天高效复习路径与核心考点梳理

在Java后端开发岗位的面试准备中,“八股文”常被误解为死记硬背的答案合集,但真正的价值在于构建高频考点地图,并训练将知识点清晰表达的能力。本文从面试官考察逻辑出发,探讨集合、并发、JVM、MySQL、Redis等核心模块的串联方法——例如通过HashMap的put流程理解线程安全问题,通过一条带缓存的查询链路打通Spring、数据库与Redis的关联。文章提供了一套7天闭环复习节奏,包括盘点、补漏、串讲、模拟四个阶段,并强调“能讲出来”比“背下来”更重要。同时,针对面试中遇到陌生问题的情况,给出了从已知推

weixin_31220401的博客 368

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

上一篇: 面试官:如何安全地使用List
下一篇: Spring 源码阅读 09:加载 BeanDefinition 的过程(注册阶段)
java_lujj
博客等级 码龄4年 18粉丝 152原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值