手撕红黑树

一、概述

红黑树是一种自平衡二叉查找树,最早由一位名叫Rudolf Bayer的德国计算机科学家于1972年发明。然而,最初的树形结构不是现在的红黑树,而是一种称为B树的结构,它是一种多叉树,可用于在磁盘上存储大量数据。

在1980年代早期,计算机科学家Leonard Adleman和Daniel Sleator推广了红黑树,并证明了它的自平衡性和高效性。从那时起,红黑树成为了最流行的自平衡二叉查找树之一,并被广泛应用于许多领域,如编译器、操作系统、数据库等。

红黑树的名字来源于红色节点和黑色节点的交替出现,它们的颜色是用来维护树的平衡性的关键。它们的颜色具有特殊的意义:

  • 黑色节点代表普通节点,
  • 红色节点代表一个新添加的节点,

它们必须满足一些特定的规则才能维持树的平衡性。

红黑树也是一种自平衡的二叉搜索树,较之 AVL,插入和删除时旋转次数更少

二、红黑树的特性

  • 每个节点要么是红色,要么是黑色。
  • 根节点是黑色。
  • 所有叶子节点(NIL节点)都是黑色。这些叶子节点通常表示空节点或没有实际存储值的节点。
  • 红色节点不能相连:如果一个节点是红色,它的两个子节点必须是黑色。
  • 黑色完美平衡:从任何节点到其所有后代叶子节点的简单路径上,必须包含相同数量的黑色节点。这个属性确保了红黑树的平衡性,避免了树变得过高。

三、实现红黑树

3.1 节点定义

public class RedBlackTree {

    /**
     * 根节点
     */
    private Node root;

    /**
     * 颜色枚举
     */
    enum Color {
        RED, BLACK;
    }

    private static class Node {
        int key;
        Object value;
        Node left;
        Node right;

        // 父节点
        Node parent;
        // 颜色 默认为红色
        Color color = Color.RED;

        /**
         * 判断节点是否是左孩子
         *
         * @return
         */
        boolean isLeftChild() {
            return parent != null && this == parent.left;
        }

        /**
         * 获取当前节点的叔叔:爸爸的兄弟(前提必须有爷爷存在)
         *
         * @return
         */
        Node getUncle() {
            if (parent == null || parent.parent == null) {
                return null;
            }
            // 如果当前节点的父亲是属于左孩子,那么爷爷的右孩子就是叔叔
            if (parent == parent.parent.left) {
                return parent.parent.right;
            } else {
                return parent.parent.left;
            }
        }

        /**
         * 获取当前节点的兄弟
         *
         * @return
         */
        Node getBrother() {
            if (parent == null) {
                return null;
            }
            // 当前节点是属于左孩子,那么父亲的右孩子就是兄弟
            if (this.isLeftChild()) {
                return parent.right;
            } else {
                return parent.left;
            }
        }
    }
}

判断节点颜色:

/**
 * 判断节点是否是红色
 *
 * @param node
 * @return
 */
boolean isRed(Node node) {
    return node != null && node.color == Color.RED;
}

/**
 * 判断节点是否是黑色
 * @param node
 * @return
 */
boolean isBlack(Node node) {
    return !isRed(node);
    //return node == null || node.color == Color.BLACK;
}

3.2 左右旋代码

这里的思路与AVL树的相同,只是多了parent属性要维护,所以对每一个移动了的节点,都需要更新parent属性。(关于AVL树旋转的详细逻辑可以看我上一篇文章)
右旋:

/**
 * 右旋:
 * 1.旋转本身的逻辑:失衡节点左孩子上位 ,处理失衡节点的后事
 * 2.处理 待处理后事节点,上位节点,失衡节点 的父亲
 * 3.处理旋转节点父亲的孩子
 * @param node 失衡的节点
 */
private void rightRotate(Node node) {
    // 失衡的节点 node
    // 失衡节点的左孩子:要上位的节点
    Node upNode = node.left;
    // 待处理的上位节点的后代:上位节点的右孩子
    Node toChangeParent = upNode.right;

    // 1.正常的右旋处理:
    // 1.1上位
    upNode.right = node;
    // 1.2处理后事
    node.left = toChangeParent;

    // 2.更新相关移动节点的父亲
    // 2.1 更新上位节点后代的父亲 为 要失衡的节点
    if (toChangeParent != null) {
        toChangeParent.parent = node;
    }
    // 2.2 更新上位节点的父亲:为 失衡节点的父亲
    upNode.parent = node.parent;
    // 2.3 更新失衡节点的父亲
    node.parent = upNode;

    // 3.处理上位节点父亲的孩子(因为是双向的,步骤二只处理了节点的父亲,针对父亲还要更新父亲的孩子)
    // 判断旋转节点原本是属于左还是还是右孩子
    if (node.parent == null) {
        // 只需要把上位节点更新为根节点
        root = upNode;
    }
    // 旋转节点原本是属于左孩子
    else if (node.parent.left == node) {
        node.parent.left = upNode;
    }
    // 旋转节点原本是属于右孩子
    else {
        node.parent.right = upNode;
    }
}

左旋:

/**
 * 左旋:
 * 1.旋转本身的逻辑:失衡节点右孩子上位 ,处理失衡节点的后事
 * 2.处理 待处理后事节点,上位节点,失衡节点 的父亲
 * 3.处理失衡节点父亲的孩子
 * @param node 失衡的节点
 */
private void leftRotate(Node node) {
    // 失衡节点node
    // 上位节点:失衡节点的右孩子
    Node upNode = node.right;
    // 待处理后事的节点:上位节点的左孩子
    Node toChangeParent = upNode.left;

    // 1.正常的左旋处理
    // 1.1 节点上位
    upNode.left = node;
    // 1.2 处理后事: 后代toChangeParent父亲更换为原本的失衡节点
    node.right = toChangeParent;

    // 2. 更新相关移动节点的父亲
    // 2.1 上位节点后代toChangeParent的父亲 更新为:失衡的节点
    if (toChangeParent != null) {
        toChangeParent.parent = node;
    }
    // 2.2 更新上位节点的父亲 为 失衡节点的父亲
    upNode.parent = node.parent;
    // 2.3 更新失衡节点的父亲 为 上位节点
    node.parent = upNode;

    // 3.处理失衡节点的父亲的孩子
    // 如果失衡节点原本是根节点
    if (node.parent == null) {
        root = upNode;
    }
    // 失衡节点原本是属于左孩子
    else if (node.parent.left == node) {
        node.parent.left = upNode;
    } else {
        node.parent.right = upNode;
    }
}

3.3 插入

假设插入的节点为红色,一共包含以下四种情况:

1.插入节点为根节点

将根节点变黑

2.插入节点的父亲为黑色

树的红黑性质不变,无需调整

3.插入节点的父亲和叔叔都为红色

此种情况只需要变色:

  • 父亲变为黑色(此时违反了黑色平衡),为了保证黑色平衡,连带的叔叔也变为黑色。
  • 此时当前路径多了一个黑色,其他路径没有多的黑色,所以需要在当前路径中再减少黑色:爷爷变成红色。
  • 爷爷变成红色,可能又会接着触发红红相邻,因此对将爷爷进行递归调整,最后根节点需要变黑

在下图的情况下插入1:


插入1.png

树的变色过程:
第一轮:对于插入节点来说:父亲和叔叔变黑,爷爷变红;
第二轮:对于爷爷来说:爷爷的父亲和爷爷的叔叔变黑,爷爷的爷爷变红;(与第一步相同,直接递归)
最后爷爷的爷爷是根节点,所以变黑


红黑树插入.gif

总结:父亲和叔叔变黑,爷爷变红,递归变化直到根节点,最后根节点变为黑色

4.插入节点的父亲为红色,叔叔为黑色

此种情况需要变色+旋转:

  • ①父亲为左孩子,插入节点也是左孩子,此时即 LL 不平衡

    • 让父亲变黑,为了保证这颗子树黑色不变,将爷爷变成红,但叔叔子树少了一个黑色
    • 爷爷右旋,补齐一个黑色给叔叔,父亲旋转上去取代爷爷,由于它是黑色,不会再次触发红红相邻


      image.png
  • ②父亲为左孩子,插入节点是右孩子,此时即 LR 不平衡

    • 父亲左旋,变成 LL 情况,按 1. 来后续处理


      image.png
  • ③父亲为右孩子,插入节点也是右孩子,此时即 RR 不平衡

    • 让父亲变黑,为了保证这颗子树黑色不变,将爷爷变成红,但叔叔子树少了一个黑色
    • 爷爷左旋,补齐一个黑色给叔叔,父亲旋转上去取代爷爷,由于它是黑色,不会再次触发红红相邻


      image.png
  • ④父亲为右孩子,插入节点是左孩子,此时即 RL 不平衡

    • 父亲右旋,变成 RR 情况,按 3. 来后续处理


      image.png

插入方法:寻找位置插入:

/**
 * 插入/更新节点
 * @param key
 * @param value
 */
public void put(int key,Object value) {
    // 找到插入的位置
    Node pointer = root;
    // 插入位置的父亲
    Node parent = null;
    while (pointer != null) {
        parent = pointer;
        if (key < pointer.key) {
            // 往左找
            pointer = pointer.left;
        } else if (key > pointer.key) {
            // 往右找
            pointer = pointer.right;
        } else {
            // 找到了 直接更新值
            pointer.value = value;
            return;
        }
    }
    // 没有找到,当前指针pointer指向的是要插入的位置
    Node added = new Node(key, value);
    // 树为空,新增节点作为根节点
    if (parent == null) {
        root = added;
    } else if (key < parent.key) {
        // 新增节点作为左孩子
        parent.left = added;
        // 设置新增节点的父亲
        added.parent = parent;
    } else {
        // 新增节点作为右孩子
        parent.right = added;
        added.parent = parent;
    }

    // 插入结束后,不平衡的情况下需要对树进行旋转和变色
    fixRedRed(added);
}

在插入了节点之后,树失衡,对树进行旋转变色方法:

/**
 * 当出现两个相邻红色节点的时候对树的调整
 * 1.插入的是根节点直接变黑
 * 2.插入节点的父亲就是黑色,无需做任何操作
 * 3.插入节点的父亲是红色,本身也是红色,触发红红相连
 *  - 叔叔是红色:父亲和叔叔一起变红,爷爷变黑;递归直到根节点
 *  - 叔叔是黑色:此种情况,因为变色时叔叔跟父亲颜色不同,所以变色做不到平衡,需要旋转
 *      - 父亲是左孩子,插入节点也是左孩子 LL
 *      - 父亲是左孩子,插入节点是右孩子  LR
 *      - 父亲是右孩子,插入节点是右孩子  RR
 *      - 父亲是右孩子,插入节点是左孩子  RL
 */
public void fixRedRed(Node node) {
    // 1.插入的是根节点直接变黑
    if (node == root) {
        node.color = Color.BLACK;
        return;
    }
    // 2.插入节点的父亲就是黑色,无需做任何操作
    else if (isBlack(node.parent)) {
        return;
    }
    //3. 父亲和叔叔都是红色
    // 代码执行到这里 父亲一定是红色
    // 父亲
    Node parent = node.parent;
    // 叔叔
    Node uncle = node.getUncle();
    // 爷爷
    Node grandpa = parent.parent;
    // 叔叔是红色(此时父亲是红色):
    if (isRed(uncle)) {
        // 父亲和叔叔变黑 (路径多了黑,所以爷爷要变红)
        parent.color = Color.BLACK;
        uncle.color = Color.BLACK;
        // 爷爷变红 (又可能触发红红相连)
        grandpa.color = Color.RED;
        // 递归执行
        fixRedRed(grandpa);
    }
    // 叔叔是黑色(此时父亲是红色)叔叔和父亲颜色不同,通过变色做不到平衡,需要旋转
    // 父亲是左孩子,插入节点也是左孩子 LL不平衡
    if (parent.isLeftChild() && node.isLeftChild()) {
        // 1.变色:父亲变黑,爷爷变红
        parent.color = Color.BLACK;
        grandpa.color = Color.RED;
        // 2.右旋:爷爷右旋
        rightRotate(grandpa);
    }
    // 父亲是左孩子,插入节点是右孩子 LR不平衡
    else if (parent.isLeftChild() && !node.isLeftChild()) {
        // 1.父亲左旋 变成了LL
        leftRotate(parent);
        // LL:变色+旋转
        // 变色:父亲变黑(在父亲左旋之后,此时父亲是插入的节点),爷爷变红
        node.color = Color.BLACK;
        grandpa.color = Color.RED;
        rightRotate(grandpa);
    }
    // 父亲是右孩子,插入节点也是右孩子 RR不平衡
    else if (!parent.isLeftChild() && !node.isLeftChild()) {
        // 1.变色:父亲变黑,爷爷变红
        parent.color = Color.BLACK;
        grandpa.color = Color.RED;
        // 2.爷爷左旋
        leftRotate(grandpa);
    }
    // 父亲是右孩子,插入节点是左孩子,RL不平衡
    else {
        // 父亲右旋,变成RR场景 此时父亲变成了node
        rightRotate(parent);
        // 1.变色:父亲变黑,爷爷变红
        node.color = Color.BLACK;
        grandpa.color = Color.RED;
        // 2.旋转:爷爷左旋
        leftRotate(grandpa);
    }
}

平衡的过程就是变色+旋转

3.4 删除

1.删除节点没有孩子

  • 删除的节点就是根节点:直接删除
  • 删除的节点不是根节点:直接把删除节点的父亲的孩子置空,删除节点的父亲也置空(有助于垃圾回收)

2.删除节点有一个孩子

  • 删除的节点是根节点:
    • 这个孩子一定是红色(如果是黑色的话就不平衡了)。
      • 剩余节点和根节点的 key,value交换,再把根节点孩子设置为null,颜色保持黑色不变
  • 删除的节点不是根节点:让删除节点的父亲指向删剩下的节点(向下的指针),删剩下的父亲指向删除节点的父亲(向上的指针);本身删除节点的左右孩子和父亲置空(垃圾回收)

3.删除节点有两个孩子

想办法把两个孩子转变成情况二的一个孩子:
交换删除节点和后继节点的 key,value,这样就转变成了情况二,递归删除后继节点,直到该节点没有孩子或只剩一个孩子。

失衡情况:

删黑色节点需要考虑平衡,删红色不需要

  • 红色是叶子节点,不会失衡
  • 红色是非叶子节点,他会有两个孩子,针对两个孩子的情况,都会被转换为只有一个孩子的情况,通过交换,进入上面的删除情况二中。

失衡情况一
删的黑色节点,剩下的是一个红色节点:

  • 删了黑色之后,把剩下的这个红节点变黑(缺少的黑色通过红孩子变黑补齐了)

失衡情况二
删的是黑色节点,剩下的也是一个黑色节点(或者是null): 当该节点删除了这个黑色之后,整条路径上对比其他路径就少了一个黑色。

  • 2.1 删除节点的兄弟为红色,此时两个侄子一定是黑色(此种情况转为后续两种处理)


    image.png
  • 2.2 删除节点的兄弟为黑色,侄子都是黑:

    • 有红色父亲的时候:把兄弟变红(平衡删掉的黑色节点),把父亲变黑(平衡该子树与其他子树对比少的黑色)


      image.png
    • 没有红色父亲的时候:以父亲作为起点,触发黑黑的方法,把父亲的兄弟变成红色;
      依次递归处理到根节点,那么每一条路径都把一个黑色变成了红色,实现黑色平衡(当然如果父亲已经是红色了,可以直接变父亲的颜色为黑色(就是上面一种情况)


      image.png
  • 2.3 删除节点的兄弟为黑色,侄子至少有一个红:

    • 兄弟是左孩子,左侄子是红色:LL


      image.png

变色逻辑:
1.通过旋转过来的3变成黑色补齐该路径上少的黑色
2.针对上位节点2,变成原本父亲的颜色,因为这条路径上本身是平衡的,所以上来的要变成原本父亲的颜色
3.针对红色的侄子1,变成黑色,因为原本该路径上,被删除节点的兄弟被当成了父亲,所以原本作为这个路径的黑色兄弟就走了,少了一个黑色,所以让红色侄子变黑。

  • 兄弟是左孩子,右侄子是红色:LR
    这里代码层面可以做简化,直接对比图中1和6,最后侄子上位变成了父亲的颜色,原本的父亲变成黑色


    image.png
    • 兄弟是右孩子,右侄子是红色:RR(此种场景和LL类似)
    • 兄弟是右孩子,左侄子是红色:RL(此种场景和LR类似)
    • 针对左右侄子都是红色的场景,走LL或者RR都是行得通的


      image.png

代码:

/**
 * 删除节点
 * @param deleted
 */
public void doRemove(Node deleted) {
    // 删除节点的后继节点
    Node replace = findReplace(deleted);
    //分三种大情况:没有孩子 一个孩子 两个孩子

    // 被删除节点的父亲
    Node deletedParent = deleted.parent;

    // 一、没有孩子
    if (replace == null) {
        // 1.1 删除的节点是根节点
        if (deleted == root) {
            root = null;
        }
        // 1.2 删除的节点不是根节点
        else {
            // 变色(注意这里要先变色 再删除)
            if (isBlack(deleted)) { // 被删除的是黑色 因为没有孩子(孩子为黑色),所以也属于下面的删除的是黑,剩下的孩子也是黑
                // 旋转 + 变色
                fixBlackBlack(deleted);
            } else { // 被删除的是红色 不需要做处理
                // do nothing
            }
            // 删除操作
            // 删除节点属于左孩子
            if (deleted.isLeftChild()) {
                deletedParent.left = null;
            } else { // 删除节点属于右孩子
                deletedParent.right = null;
            }
            deleted.parent = null;
        }
        return;
    }

    // 二、有一个孩子
    if (deleted.left == null || deleted.right == null) {
        // 2.1 删除的节点是根节点 这个唯一的孩子一定是红色:根节点和孩子互换,删除孩子
        if (deleted == root) {
            // delete和replace交换
            deleted.key = replace.key;
            deleted.value = replace.value;
            deleted.left = null;
            deleted.right = null;
        }
        // 2.2 不是根节点
        else {
            // 删除操作
            // 看被删除的节点是左孩子还是右孩子
            if (deleted.isLeftChild()) {
                deletedParent.left = replace;
            } else {
                deletedParent.right = replace;
            }
            replace.parent = deletedParent;
            // 被删除的节点孩子还父亲置空
            deleted.parent = null;
            deleted.left = null;
            deleted.right = null;

            // 变色操作
            // 删除的是黑色 ,删剩下的也是黑色
            if (isBlack(deleted) && isBlack(replace)) {
                // 旋转 + 变色
                fixBlackBlack(replace);
            } else { // 删除的是黑色,剩下的是红色:
                // 少了一个黑色,把剩下的红色变黑即可
                replace.color = Color.BLACK;
            }
        }
        return;
    }

    // 三、有两个孩子
    /**
     * 这种情况转为只有一个孩子的情况:
     * 将要删除的节点和后继节点的键值交换,那么要删除的节点就变成了后继节点,此时一直递归调用,直到只有一个孩子的情况,进入情况二的分支
     */
    // 交换key
    int tempKey = deleted.key;
    deleted.key = replace.key;
    replace.key = tempKey;

    // 交换value
    Object tempVal = deleted.value;
    deleted.value = replace.value;
    replace.value = tempVal;

    // 要删的就变成了replace 递归直到进入条件二
    doRemove(replace);
}

被删除的节点是黑色,删剩下的也是黑色场景的旋转和变色:

/**
 * 被删除的节点是黑色,删剩下的也是黑色(整体这个路径上少了一个黑色,失衡)
 * - 情况一.被删节点的兄弟是红色,此时侄子一定是黑色:左旋+变色 转为下面两种情况
 * - 情况二.被删节点的兄弟是黑色,侄子(这俩的孩子)都是黑:
 * - 情况三.被删节点的兄弟是黑色,侄子(这俩的孩子)中至少一个红:
 * @param node
 */
private void fixBlackBlack(Node node) {
    if (node == root) {
        //此时处理结束
        return;
    }
    // 节点的兄弟
    Node brother = node.getBrother();
    // 节点的父亲
    Node parent = node.parent;
    // 情况一.被删节点的兄弟是红色,此时侄子一定是黑色:左旋+变色
    if (isRed(brother)) {
        if (node.isLeftChild()) {
            // node属于左节点  父亲左旋
            // 1.1 父亲左旋
            leftRotate(parent);
        } else {
            rightRotate(parent);
        }
        // 1.2 调整平衡
        brother.color = Color.BLACK;
        parent.color = Color.RED;

        // 此时把兄弟变成了黑色了 就符合了情况二和情况三,再次调用方法进行二和三情况的处理
        fixBlackBlack(node);
        return;
    }
    // 情况二:删除节点的兄弟为黑色,侄子都是黑:
    assert brother != null;
    if (isBlack(brother.left) && isBlack(brother.right)) {
        // 2.1兄弟变红
        brother.color = Color.RED;
        // 如果此时父亲是红,则让父亲变黑就维持了平衡了
        if (isRed(parent)) {
            parent.color = Color.BLACK;
        }
        // 如果父亲是黑,触发黑黑调整:让兄弟变红;依次递归,让每一条路径都有一个兄弟变红,就都少了一个黑色,实现黑色平衡
        else {
            fixBlackBlack(parent);
        }
    } else {
        // 情况三:删除的节点兄弟是黑色,侄子至少一个红色(一个红色或者两个都是红色)
        /**
         *  兄弟是左孩子,左侄子是红色:LL :父亲右旋+变色
         *  兄弟是左孩子,右侄子是红色:LR
         *  兄弟是右孩子,右侄子是红色:RR
         *  兄弟是右孩子,左侄子是红色:RL
         */
        // LL: 兄弟是左孩子,左侄子是红色
        if (brother.isLeftChild() && isRed(brother.left)) {
            // 父亲右旋
            rightRotate(parent);
            /**
             * 变色逻辑:
             * 1.通过旋转过来的节点变成黑色补齐该路径上少的黑色
             * 2.针对上位节点,变成原本父亲的颜色,因为这条路径上本身是平衡的,所以上来的要变成原本父亲的颜色
             * 3.针对红色的侄子,变成黑色,因为原本该路径上,被删除节点的兄弟被当成了父亲,所以原本作为这个路径的黑色兄弟就走了,少了一个黑色,所以让红色侄子变黑。
             */
            // 代码变色从后往前,因为从前往后会导致还变色的颜色丢失了
            // 侄子变黑
            brother.left.color = Color.BLACK;
            //上位节点(兄弟节点)变成原来父亲的颜色
            brother.color = parent.color;
            // 原本父亲节点旋转下去了 变成黑色
            parent.color = Color.BLACK;
        }
        // LR:兄弟是左孩子,右侄子是红色
        else if (brother.isLeftChild() && isRed(brother.right)) {
            /**
             * 这里的代码可以做简化,旋转之前和之后分别做一次变色即可
             */
            // 侄子上位,变成父亲的颜色(这里要先变色,因为侄子是要旋下去的,就找不到右孩子了)
            brother.right.color = parent.color;
            // 兄弟左旋
            leftRotate(brother);
            // 父亲右旋
            rightRotate(parent);
            // 父亲最后退位,颜色变黑,补齐删除的黑色
            parent.color = Color.BLACK;
        }
        // RR
        else if (!brother.isLeftChild() && !isRed(brother.left)) {
            // 父亲左旋
            leftRotate(parent);
            // 侄子变黑
            brother.right.color = Color.BLACK;
            //上位节点(兄弟节点)变成原来父亲的颜色
            brother.color = parent.color;
            // 原本父亲节点旋转下去了 变成黑色
            parent.color = Color.BLACK;
        }
        // RL
        else {
            // 侄子上位,变成父亲的颜色(这里要先变色,因为侄子是要旋下去的,就找不到右孩子了)
            brother.left.color = parent.color;
            // 兄弟右旋
            rightRotate(brother);
            // 父亲左旋
            leftRotate(parent);
            // 父亲最后退位,颜色变黑,补齐删除的黑色
            parent.color = Color.BLACK;
        }
    }
}

四、总结

复杂度分析

操作 普通二叉搜索树 AVL树 红黑树
查询 平均O(logn),最坏O(n) O(logn) O(logn)
插入/更新 平均O(logn),最坏O(n) O(logn) O(logn)
删除 平均O(logn),最坏O(n) O(logn) O(logn)

二叉搜索树

二叉搜索树的插入、删除、查询的时间复杂度与树的高度相关,因此在最坏情况下,时间复杂度为O(n),而且容易退化成链表,查找效率低,不平衡。

AVL树

AVL树是一种严格平衡的二叉搜索树,其左右子树的高度差不超过1。因此,它能够在logn的平均时间内完成插入、删除、查询操作,但是在维护平衡的过程中,需要频繁地进行旋转操作,导致插入删除效率较低。

红黑树

红黑树是一种近似平衡的二叉搜索树,它在保持高度平衡的同时,又能够保持较高的插入删除效率。红黑树通过节点着色和旋转操作来维护平衡。红黑树在维护平衡的过程中,能够进行较少的节点旋转操作,因此插入删除效率较高,并且查询效率也较高。

综上所述,红黑树具有较高的综合性能,是一种广泛应用的数据结构。

完整代码链接:https://github.com/GitHongcx/algorithm-demo/blob/main/src/main/java/com/hcx/algorithm/tree/RedBlackTree.java

补充:这篇文章对旋转部分没有描述的很详细,在我上一篇AVL树的实现里有很详细的介绍旋转的逻辑,可以参考。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 227,401评论 6 531
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 98,011评论 3 413
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 175,263评论 0 373
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 62,543评论 1 307
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 71,323评论 6 404
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 54,874评论 1 321
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 42,968评论 3 439
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 42,095评论 0 286
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 48,605评论 1 331
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 40,551评论 3 354
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 42,720评论 1 369
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 38,242评论 5 355
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 43,961评论 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 34,358评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 35,612评论 1 280
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 51,330评论 3 390
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 47,690评论 2 370

推荐阅读更多精彩内容