深入理解左倾红黑树 | 京东物流技术团队

平衡二叉搜索树

平衡二叉搜索树(Balanced Binary Search Tree)的每个节点的左右子树高度差不超过 1,它可以在 O(logn) 时间复杂度内完成插入、查找和删除操作,最早被提出的自平衡二叉搜索树是 AVL 树。

AVL 树在执行插入或删除操作后,会根据节点的平衡因子来判断是否平衡,若非平衡则执行旋转操作来维持树的平衡,本文主要是对红黑树相关的讲解,如果大家感兴趣可以去了解一下 AVL 树相关的知识,在这里不做赘述。

2-3 搜索树

标准二叉树中的节点称为 2-节点(含有一个键和两个指针),为了保证二叉搜索树的平衡性,需要增加一些灵活性,增加 3-节点(含有两个键和三个指针)。2-节点的指针和 3-节点的指针对应的区间大小关系如下:

  • 2-节点:left 指针指向的左子树中所有的键值均小于当前节点键值,right 指针指向的右子树中所有的键值均大于当前节点键值

  • 3-节点:left 指针指向的左子树中所有的键值均小于当前节点键值,mid 指针指向的中子树中所有的键值在当前节点的两个键值之间,right 指针指向的右子树中所有的键值均大于当前节点的键值

我们先来看一下一棵2-3搜索树的样例:

image.png

这里需要注意:2-3搜索树无论如何增删节点,它始终都能维持完美的平衡性,看下文时要牢记这一点。

3-节点的引入是如何保证树平衡的?

我们以插入键值 11 的节点为例,它会查找到该键对应的位置为 12 节点的左子树,如果我们未引入3-节点,那么树高会增加,如下图中(1)所示,而我们引入3-节点后,我们只需将2-节点替换为3-节点,而不会导致树高的增加,2-3搜索树依然完美平衡,如下图中(2)所示:

image.png

我们接着看,如果我们插入的键值为 26,它会查找到该键的位置为 19 和 24 这个3-节点,因为这个节点中已经没有多余的键的位置了,所以26键只能放在右子树的位置,使得树高加一。不过我们可以通过巧妙的方法,先将该节点转换为4-节点,一个4-节点又能转换成 3 个 2-节点, 我们将这 3 个2-节点的根节点(4-节点的中键值)插入到原来的父节点中,那么树高仍能保持不变(完美平衡),如下图所示:

image.png

虽然我们临时借助了4-节点,但是最终变换完毕后,整棵树还是依然是由2-节点和3-节点组成的。

我们再插入键值为4的节点会如何呢?它会查找到1和3这个3-节点的位置,还是采用相同的办法,将其转换成4-节点,但是转换完后准备将它插入到原来的父节点时,会发现父节点也是3-节点,这就导致我们不得不再次使用同样的方法,这样推广到一般情况,需要不断地分解临时的4-节点并将其中键插入到更高的父节点中,直到遇到一个2-节点并将其转换成不需要分解的3-节点或者到达为2-节点的根节点,如下图所示:

2-3树4.jpg

那如果处理到根节点时,根节点仍然为3-节点呢?其实原理是一样的,只不过根节点没有父节点,不需要再将4-节点的中键向上合并,转换成3个2-节点即可,相应地树高会加一。

在以上所述的情况中,2-3树的插入算法使树本身结构发生的改变是局部的,除了相关的节点和引用之外,不必修改和检查树的其他部分,这些局部变换不会影响到树的全局有序性和平衡性,在插入的过程中,2-3树始终是完美平衡二叉树

左倾红黑树(LLRBT)

学会了 2-3 树我们先来学习一种简单的红黑树,左倾红黑树,它是经典红黑树的变体,相对更容易实现。它的基本思想是用标准的二叉搜索树和“颜色信息”来表示2-3树:其中的链接分为两种,红链接将两个2-节点连接起来构成一个3-节点,黑链接是2-3树中的普通链接,如下图所示:

LLRB.jpg

我们规定被红色链接指向的节点为红色节点,被黑色链接指向的节点为黑色节点,从上图中我们也能够发现:如果我们将红链接“横过来”,它和3-节点的表示是一样的,这就是在左倾红黑树中用颜色信息表示3-节点的方式,确切地说,3-节点是由两个2-节点组成,并且其中一个2-节点被另一个2-节点用红链接左引用

可以说左倾红黑树就是一棵2-3搜索树,它满足如下条件:

  • 根节点是黑色的

  • 红链接均为左引用

  • 没有任何一个节点同时和两条红链接相连

  • 任意叶子节点到根节点路径上的黑色节点数量相同,即该树是黑色平衡的

2-3搜索树始终能保持完美平衡,那么任意叶子节点到根节点的距离是相等的。左倾红黑树又是一棵2-3搜索树,其中的黑链接是2-3搜索树中的普通链接,那么左倾红黑树中被黑色链接引用的黑色节点也必然是完美平衡的,所以任意叶子节点到根节点路径上的黑色节点数量必然相同。

性质我们已经理解了,接下来我们看看左倾红黑树的代码实现:

节点定义

上文我们已经规定,被红链接引用的节点为红色,被黑色链接引用的节点为黑色,我们在节点中添加color字段来标记颜色:True表示红色,False表示黑色,定义如下:

public class LeftLeaningRedBlackTree {

    private static final boolean RED = true;

    private static final boolean BLACK = false;

    static class Node {

        int key;

        int value;

        boolean color;

        Node left;

        Node right;

        public Node(int key, int value, boolean color) {
            this.key = key;
            this.value = value;
            this.color = color;
        }
    }

    /**
     * 判断节点是否为红色
     */
    private boolean isRed(Node node) {
        if (node == null) {
            return false;
        } else {
            return node.color == RED;
        }
    }
}

旋转

插入和删除操作可能会使左倾红黑树出现红链接右引用或者左右引用都是红链接的情况,造成黑色不平衡,为了始终满足左倾红黑树的性质,我们需要通过旋转操作进行修复。

左旋

如果有红链接右引用,我们可以通过左旋操作将其调整为红链接左引用,过程如下图所示:

LLRB2.jpg

代码如下:

    /**
     * 左旋
     */
    private Node rotateLeft(Node node) {
        Node newRoot = node.right;
        node.right = newRoot.left;
        newRoot.left = node;

        newRoot.color = node.color;
        node.color = RED;

        return newRoot;
    }

旋转完成后可以发现,左旋是将两个键中较小的作为根节点转变为较大的作为根节点。

右旋

右旋是和左旋完全相反的动作,它能将红链接左引用旋转成红链接右引用,过程如下图所示:

LLRB3.jpg

代码如下:

    /**
     * 右旋
     */
    private Node rotateRight(Node node) {
        Node newRoot = node.left;
        node.left = newRoot.right;
        newRoot.right = node;

        newRoot.color = node.color;
        node.color = RED;

        return newRoot;
    }

右旋是将两个键中较大的作为根节点转变为较小的作为根节点。

左旋和右旋的方法返回值都是Node,旋转完成后父节点对旋转节点的引用会发生改变,在插入节点时旋转操作可以保持左倾红黑树的有序性完美平衡性,也就是说我们在红黑树中进行旋转时无需为它的有序性和完美平衡性担心。

插入节点

插入的每个节点默认红色,之后根据插入后节点颜色情况,使用旋转操作进行修复,我们分情况讨论:

向2-节点中插入

向2-节点中插入节点很简单:如果新键值比老键小,那么将该节点被老节点红链接左引用即可;如果新键值比老键大,那么该节点需要被老节点红链接右引用,因为左倾红黑树中红链接不能左引用,所以我们需要左旋进行调节,过程图示如下:

LLRB4.jpg

向3-节点中插入

向3-节点中插入分三种情况,我们分别进行讨论:

  • 新节点大于3-节点中的两个键值

这种情况该节点被3-节点右引用,前文中我们说过,在3-节点中插入新节点可以临时转化成4-节点进行处理,4-节点又能转换成3个2-节点,那么我们得到的就是一颗树高为 2 的平衡树,此时根节点的左右引用均为红链接,我们可以将它们全部转换成黑链接(反色处理),如下图所示:

LLRB5.jpg

  • 新节点小于3-节点中的两个键值

这种情况该节点被3-节点左引用,这样会出现两条红链接连续的情况,我们只需将上层的红链接右旋,这样便转换成了我们上述的第一种情况,过程如下图所示:

LLRB6.jpg

  • 新节点介于3-节点的两键值中间

这种情况仍然会出现两条红链接连续的情况,我们只需要将下层的红链接左旋,便转换成了上述的第二种情况,过程如下图所示:

LLRB7.jpg

颜色转换

我们需要为上述将某个节点的两条红链接转换成黑链接的动作定义flipColors方法,在图示中能够发现除了将左右节点转换成黑色外,该节点被转换成了红色,颜色转换在本质上和旋转操作同样都是局部的,并不会影响整棵树的黑色平衡性。该节点颜色转换成红色后,相当于将其送入了它的父节点,意味着在父节点插入了一个新键,我们需要按照上述插入节点的对应情况进行处理。

    /**
     * 颜色转换
     */
    private void flipColors(Node node) {
        node.color = RED;
        node.left.color = BLACK;
        node.right.color = BLACK;
    }

不过,这对于根节点来说,并不能完全按照这种规则进行颜色转换,因为如果根节点是红色的,那么说明根节点为3-节点中较小的节点,但实际上并非如此(根节点始终为其中较大的值)。所以我们每次插入节点时需要保证根节点是黑色的,而且每当根节点由红色转变为黑色时,都意味着树高增加了 1

在3-节点中插入节点后,将临时转换成4-节点,并将4-节点的中键作为红色节点插入到它的父节点中(无论是左引用还是右引用),中键作为红色节点插入父节点无异于处理新插入一个红色节点,我们需要不断地重复这个过程,直到遇到根节点或2-节点,这样我们就能始终保持左倾红黑树和2-3搜索树的一一对应关系。

LLRB8.jpg

上图对应了节点插入后,执行旋转和颜色转换的所有情况。这些保持树平衡的操作需要在该插入节点到根节点路径上(回溯)的所有节点执行,规则如下:

  • 如果某节点的右节点是红色,左节点为空或为黑色,需要左旋

  • 如果某节点的左节点是红色,该左节点的左节点仍是红色,需要右旋

  • 如果某节点的左节点和右节点均为红色,需要执行颜色转换

现在我们已经讨论了插入节点时所有可能出现的情况,接下来我们要实现左倾红黑树的插入方法。它的实现是在普通二叉搜索树的插入方法上进行了扩展,添加了保持树平衡性的逻辑,而平衡性的保持又是由下到上的,所以我们需要将旋转和颜色转换的逻辑放在递归逻辑之后,也就是我们在回溯时修复树的平衡,代码如下:

    /**
     * 插入节点
     */
    public void put(Integer key, Integer value) {
        root = put(root, key, value);
        // 根节点永远都是黑色
        root.color = BLACK;
    }

    /**
     * 插入节点的执行逻辑
     */
    private Node put(Node node, Integer key, Integer value) {
        if (node == null) {
            return new Node(key, value, RED);
        }

        if (key > node.key) {
            node.right = put(node.right, key, value);
        } else if (key < node.key) {
            node.left = put(node.left, key, value);
        } else {
            node.value = value;
        }
        // 将3-节点的红链接右引用左旋
        if (isRed(node.right) && !isRed(node.left)) {
            node = rotateLeft(node);
        }
        // 将4-节点两条连续的红链接左引用左旋
        if (isRed(node.left) && isRed(node.left.left)) {
            node = rotateRight(node);
        }
        // 进行颜色转换并将红链接在树中向上传递
        if (isRed(node.left) && isRed(node.right)) {
            flipColor(node);
        }

        return node;
    }

删除节点

删除节点方法是左倾红黑树中最复杂的实现,所以接下来的内容可能会让大家觉得有一些困难,但是千万不要灰心,我在学习的时候也是重复了很多遍才理解了这个过程,也请大家多给自己一些耐心。

左倾红黑树在执行删除的过程中,如果直接删除一个2-节点,会留下空链接,这样会破坏树的完美平衡性,如下图所示:
LLRB_DELETE.jpg

所以,在执行删除之前,需要将被删除节点转换成3-节点或临时的4-节点的一部分,这样才能保证删除节点前后左倾红黑树一直都是完美平衡的,相应地,如果被删除节点为3-节点或4-节点,那么直接移除它是不影响树的完美平衡的。

接下来我们以删除最小节点为例,讲解下节点删除的过程。

删除最小节点

既然我们需要将被删除的2-节点转换成3-或4-节点的一部分,那么我们该如何转换呢?

我们先看第一种情况,要被删除的节点和它的兄弟节点都是2-节点。根据上文中插入节点的逻辑,两个黑色的左右子节点是由4-节点转换颜色后得到的,那么我们可以再对其进行颜色转换,使之再成为4-节点,被删除节点也就成了4-节点的一部分,此时将其删除就不会影响树的平衡性了。不过在删除之后,虽然树的平衡性不被影响,但是红链接右引用不符合左倾红黑树的性质,需要左旋操作来修复,过程图示如下:

LLRB_DELETE3.jpg

图示中由(1)到(2)将根节点转化成了红色,不知道大家还记不记得,在插入节点时4-节点转换成3个2-节点不只将左右节点转换成了黑色,还将该节点染成了红色,表示该节点会插入到父节点中,由于该节点为根节点,所以在(1)中表示为黑色,图(2)将根节点切换回了红色,对应了4-节点转换成3个2-节点的一般情况,图(3)中将这3个节点颜色均取反便得到了4-节点

我们接着看第二种情况,要被删除的节点为2-节点,它的兄弟节点为3-节点,《算法》中介绍了“借键方法”:左子节点将根节点中较小的节点借过来组成3-节点,根节点将右节点中较小的节点借过来保持自身原来节点样式不变,而右节点便成了一个2-节点,借键完毕后再将左子节点(3-节点)中较小的键(红色节点)移除,不会影响树的完美平衡性,图示如下:

LLRB_DELETE2.jpg

现在我们已经整理好了删除最小节点的过程,试着写一下代码:

    /**
     * 删除最小节点
     */
    public void deleteMin() {
        if (!isRed(root.left) && !isRed(root.right)) {
            root.color = RED;
        }
        root = deleteMin(root);
        // 根节点永远都是黑色
        if (root != null) {
            root.color = BLACK;
        }
    }

    /**
     * 删除最小节点
     */
    private Node deleteMin(Node node) {
        if (node.left == null) {
            return null;
        }

        // 判断当前节点和左子节点是不是3-节点,不是的话执行借键逻辑
        if (!isRed(node.left) && !isRed(node.left.left)) {
            node = moveRedLeft(node);
        }
        node.left = deleteMin(node.left);

        return balance(node);
    }

    /**
     * 从右向左借键
     */
    private Node moveRedLeft(Node node) {
        flipColors(node);
        // 兄弟节点为3-节点的情况
        if (isRed(node.right.left)) {
            node.right = rotateRight(node.right);
            node = rotateLeft(node);
        }

        return node;
    }

    /**
     * 从下到上再平衡
     */
    private Node balance(Node node) {
        if (isRed(node.right)) {
            node = rotateLeft(node);
        }
        if (isRed(node.left) && isRed(node.left.left)) {
            node = rotateRight(node);
        }
        if (isRed(node.left) && isRed(node.right)) {
            flipColors(node);
        }

        return node;
    }

需要注意的是,在插入节点定义的flipColors如下,本质上在插入节点时,该操作是将当前节点颜色取反:

    private void flipColors(Node node) {
        node.color = RED;
        node.left.color = BLACK;
        node.right.color = BLACK;
    }

现在将该方法改成了反色处理,为了在删除节点方法中复用,如下:

    private void flipColors(Node node) {
        node.color = !node.color;
        node.left.color = !node.left.color;
        node.right.color = !node.right.color;
    }

本质上,删除最小节点的过程是从上到下不断地将当前节点染红,保证当前节点始终不为2-节点,避免移除节点后发生树的不平衡,因为不知道什么时候能找到最小节点,所以该路径上的每个节点都需要执行该操作。

删除最大节点

删除最大节点我们只讨论被删除节点为2-节点,其兄弟节点为3-节点的情况,另一种被删除节点和它的兄弟节点均为2-节点的情况非常简单,不再赘述。

其兄弟节点为3-节点时,我们仍需要采用“借键”的方法,过程如下图所示:

LLRB_DELETE4.jpg

删除最大节点的代码逻辑如下所示:

    /**
     * 删除最大节点
     */
    public void deleteMax() {
        if (!isRed(root.left) && !isRed(root.right)) {
            root.color = RED;
        }
        root = deleteMax(root);
        // 根节点永远都是黑色
        if (root != null) {
            root.color = BLACK;
        }
    }

    /**
     * 删除最大节点
     */
    private Node deleteMax(Node node) {
        if (isRed(node.left)) {
            node = rotateRight(node);
        }
        if (node.right == null) {
            return null;
        }

        // 当前节点的右引用不是红链接且右节点不是3-节点
        if (!isRed(node.right) && !isRed(node.right.left)) {
            node = moveRedRight(node);
        }
        node.right = deleteMax(node.right);

        return balance(node);
    }

    /**
     * 从左向右借键
     */
    private Node moveRedRight(Node node) {
        flipColors(node);
        if (isRed(node.left.left)) {
            node = rotateRight(node);
        }
        return node;
    }

其中有如下代码是在删除最小节点没有的:

    private Node deleteMax(Node node) {
        // 区别于删除最小节点的操作
        if (isRed(node.left)) {
            node = rotateRight(node);
        }
        // ...
    }

我们来解释一下这段逻辑:在左倾红黑树中,如果当前节点的左节点为红色,那么会有可能存在这种情况:

LLRB_DELETE5.jpg

直接将图示中最大节点移除的话,会导致它的左节点丢失,为了避免,会先执行右旋,将要被删除的节点旋转到右引用的位置。

删除节点

删除节点的方法在弄明白了删除最大和最小节点的方法后,理解起来是非常容易的。它其实是删除最大节点和删除最小节点方法的结合,同样也是需要保证经过的节点为3-节点或临时的4-节点,在找到对应节点时,将该节点值替换为右子树中的最小节点值,再执行删除右子树最小节点,删除完成后执行平衡修复,大家需要关注其中的注释信息,如下:

    /**
     * 删除节点
     */
    public void delete(Integer key) {
        if (!isRed(root.left) && !isRed(root.right)) {
            root.color = RED;
        }
        root = delete(root, key);
        if (root != null) {
            root.color = BLACK;
        }
    }

    /**
     * 删除节点
     */
    private Node delete(Node node, Integer key) {
        if (key < node.key) {
            // 要删除的节点在左子树,保证当前节点为红色
            if (!isRed(node.left) && !isRed(node.left.left)) {
                node = moveRedLeft(node);
            }
            node.left = delete(node.left, key);
        } else {
            if (isRed(node.left)) {
                node = rotateRight(node);
            }
            // node.right == null 可知上面右旋没有发生,右子树为 null,左节点不是红色节点,能证明左子树必然为null(满足黑色平衡)
            if (key.equals(node.key) && node.right == null) {
                return null;
            }
            // 要删除的节点在右子树
            if (!isRed(node.right) && !isRed(node.right.left)) {
                node = moveRedRight(node);
            }

            if (key.equals(node.key)) {
                // 找到右子树中最小的节点替换当前节点的键和值
                Node min = min(node.right);
                node.key = min.key;
                node.value = min.value;
                // 再将该右子树最小的节点移除
                deleteMin(node.right);
            } else {
                node.right = delete(node.right, key);
            }
        }

        return balance(node);
    }

为方便大家学习参考,全量代码如下:

public class LeftLeaningRedBlackTree {

    private static final boolean RED = true;

    private static final boolean BLACK = false;

    static class Node {

        int key;

        int value;

        boolean color;

        Node left;

        Node right;

        public Node(int key, int value, boolean color) {
            this.key = key;
            this.value = value;
            this.color = color;
        }
    }

    private Node root;

    /**
     * 插入节点
     */
    public void put(Integer key, Integer value) {
        root = put(root, key, value);
        // 根节点永远都是黑色
        root.color = BLACK;
    }

    /**
     * 插入节点的执行逻辑
     */
    private Node put(Node node, Integer key, Integer value) {
        if (node == null) {
            return new Node(key, value, RED);
        }

        if (key > node.key) {
            node.right = put(node.right, key, value);
        } else if (key < node.key) {
            node.left = put(node.left, key, value);
        } else {
            node.value = value;
        }
        // 将3-节点的红链接右引用左旋
        if (isRed(node.right) && !isRed(node.left)) {
            node = rotateLeft(node);
        }
        // 将4-节点两条连续的红链接左引用左旋
        if (isRed(node.left) && isRed(node.left.left)) {
            node = rotateRight(node);
        }
        // 进行颜色转换并将红链接在树中向上传递
        if (isRed(node.left) && isRed(node.right)) {
            flipColors(node);
        }

        return node;
    }

    /**
     * 删除最小节点
     */
    public void deleteMin() {
        if (!isRed(root.left) && !isRed(root.right)) {
            root.color = RED;
        }
        root = deleteMin(root);
        // 根节点永远都是黑色
        if (root != null) {
            root.color = BLACK;
        }
    }

    /**
     * 删除最小节点
     */
    private Node deleteMin(Node node) {
        if (node.left == null) {
            return null;
        }

        // 判断当前节点和左子节点是不是3-节点,不是的话执行借键逻辑
        if (!isRed(node.left) && !isRed(node.left.left)) {
            node = moveRedLeft(node);
        }
        node.left = deleteMin(node.left);

        return balance(node);
    }

    /**
     * 从右向左借键
     */
    private Node moveRedLeft(Node node) {
        flipColors(node);
        // 兄弟节点为3-节点的情况
        if (isRed(node.right.left)) {
            node.right = rotateRight(node.right);
            node = rotateLeft(node);
        }

        return node;
    }

    /**
     * 删除最大节点
     */
    public void deleteMax() {
        if (!isRed(root.left) && !isRed(root.right)) {
            root.color = RED;
        }
        root = deleteMax(root);
        // 根节点永远都是黑色
        if (root != null) {
            root.color = BLACK;
        }
    }

    /**
     * 删除最大节点
     */
    private Node deleteMax(Node node) {
        if (isRed(node.left)) {
            node = rotateRight(node);
        }
        if (node.right == null) {
            return null;
        }

        // 当前节点的右引用不是红链接且右节点不是3-节点
        if (!isRed(node.right) && !isRed(node.right.left)) {
            node = moveRedRight(node);
        }
        node.right = deleteMax(node.right);

        return balance(node);
    }

    /**
     * 从左向右借键
     */
    private Node moveRedRight(Node node) {
        flipColors(node);
        if (isRed(node.left.left)) {
            node = rotateRight(node);
        }
        return node;
    }

    /**
     * 删除节点
     */
    public void delete(Integer key) {
        if (!isRed(root.left) && !isRed(root.right)) {
            root.color = RED;
        }
        root = delete(root, key);
        if (root != null) {
            root.color = BLACK;
        }
    }

    /**
     * 删除节点
     */
    private Node delete(Node node, Integer key) {
        if (key < node.key) {
            // 要删除的节点在左子树,保证当前节点为红色
            if (!isRed(node.left) && !isRed(node.left.left)) {
                node = moveRedLeft(node);
            }
            node.left = delete(node.left, key);
        } else {
            if (isRed(node.left)) {
                node = rotateRight(node);
            }
            // node.right == null 可知上面右旋没有发生,右子树为 null,左节点不是红色节点,能证明左子树必然为null(满足黑色平衡)
            if (key.equals(node.key) && node.right == null) {
                return null;
            }
            // 要删除的节点在右子树
            if (!isRed(node.right) && !isRed(node.right.left)) {
                node = moveRedRight(node);
            }

            if (key.equals(node.key)) {
                // 找到右子树中最小的节点替换当前节点的键和值
                Node min = min(node.right);
                node.key = min.key;
                node.value = min.value;
                // 再将该右子树最小的节点移除
                deleteMin(node.right);
            } else {
                node.right = delete(node.right, key);
            }
        }

        return balance(node);
    }

    /**
     * 获取最小节点
     */
    private Node min(Node node) {
        if (node.left == null) {
            return node;
        }
        return min(node.left);
    }

    /**
     * 从下到上再平衡
     */
    private Node balance(Node node) {
        if (isRed(node.right)) {
            node = rotateLeft(node);
        }
        if (isRed(node.left) && isRed(node.left.left)) {
            node = rotateRight(node);
        }
        if (isRed(node.left) && isRed(node.right)) {
            flipColors(node);
        }

        return node;
    }

    /**
     * 左旋
     */
    private Node rotateLeft(Node node) {
        Node newNode = node.right;
        node.right = newNode.left;
        newNode.left = node;

        newNode.color = node.color;
        node.color = RED;

        return newNode;
    }

    /**
     * 右旋
     */
    private Node rotateRight(Node node) {
        Node newNode = node.left;
        node.left = newNode.right;
        newNode.right = node;

        newNode.color = node.color;
        node.color = RED;

        return newNode;
    }

    /**
     * 颜色转换
     */
    private void flipColors(Node node) {
        node.color = !node.color;
        node.left.color = !node.left.color;
        node.right.color = !node.right.color;
    }

    /**
     * 判断节点是否为红色
     */
    private boolean isRed(Node node) {
        if (node == null) {
            return false;
        } else {
            return node.color == RED;
        }
    }
}


巨人的肩膀

  • 《算法 第四版》:第 3.3 章 平衡查找树

  • 维基百科 - 平衡二元搜寻树

  • 维基百科 - AVL树

  • 知乎 - 关于AVL树和红黑树的一点看法

  • 维基百科 - 左倾红黑树

  • LeetCode - 红黑树从入门到看开

  • 知乎 - 有人能讲清楚《Algorithms》中左倾红黑树(LLRB)删除操作的每一行代码吗?

作者:京东物流 王奕龙

来源:京东云开发者社区 自猿其说 Tech 转载请注明来源

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

JavaWeb基础(2)- Web概述、HTTP协议、Servlet、Request与Response

JavaWeb基础&#xff08;2&#xff09;- Web概述、HTTP协议、Servlet、Request与Response 文章目录 JavaWeb基础&#xff08;2&#xff09;- Web概述、HTTP协议、Servlet、Request与Response3 Web概述3.1 Web和JavaWeb的概念3.2 JavaWeb技术栈3.2.1 B/S架构**3.2.2 静态资源**3…

[排序算法] 如何解决快速排序特殊情况效率低的问题------三路划分

前言 在[C/C]排序算法 快速排序 (递归与非递归)一文中,对于快速排序的单趟排序一共讲了三种方法: hoare、挖坑法、双指针法 ,这三种方法实现的快速排序虽然在一般情况下效率很高,但是如果待排序数据存在大量重复数据,那这几种方法的效率就很低,而为了解决快速排序在这样特殊情况…

马蹄集oj赛(双周赛第十八次)

目录 幸运的3 打靶 照亮街道 九次九日九重色 寻找串 竹鼠的白色季节 捉迷藏 好的三连 三角数 买马 可怜的小码哥 花园浇水 高次方程 幸运的3 难度:黄金时间限制: 1秒四占用内存:128M 你有 n 个数&#xff0c;可以将它们两两匹配(即将两数首尾相连)&#xff0c;每个…

element中Table表格控件实现单选功能、多选功能、两种分页方式

目录 1、Table表格控件实现单选功能2、Table控件和Pagination控件实现多选和两种分页方式方法一&#xff1a;使用slice方法方法二&#xff1a;多次调用接口 1、Table表格控件实现单选功能 <template><div><!-- highlight-current-row 是否要高亮当前行 -->…

【史上最细教程】CentOS7 下载安装 RabbitMQ(两种方式:手动安装 / Docker安装)

文章目录 【史上最细教程】CentOS7 下载安装 RabbitMQ方式一&#xff1a;手动安装1.下载安装Erlang、RabbitMQ2.防火墙、安全组端口放行3.启动RabbitMQ服务4.浏览器用户登录5.配置文件查看(可略) 方式二&#xff1a;Docker安装1.安装Docker2.获取RabbitMQ镜像、创建容器3.浏览器…

synchronized锁

synchronized 类锁&#xff1a;给类的静态方法加上synchronized 关键字进行修饰&#xff0c; 锁的是当前类class&#xff0c;一个静态同步方法拿到锁&#xff0c;其他静态同步方法就会等待静态同步方法和普通同步方法间是没有竞争的 对象锁&#xff1a;给类的方法加上synchron…

权威外媒聚焦:Messari强调波场TRON在全球加密支付领域的引领作用

近日,金融时报、费加罗报及美联社等海外权威媒体就波场TRON 在全球加密支付领域的重要进展发布了相关报道。报道引述加密研究机构Messari 《Crypto Theses for 2024》年度报告,重点强调了波场TRON在推动全球加密货币支付尤其是稳定币USDT应用方面的显著成就。 报道提到,波场TR…

你的第一个C/S程序

目录 socket服务端代码客户端代码执行结果 socket socket基础知识 服务端代码 import socket import threading import timeMSG_LENGTH 64 DISCONNECTED !CONNECTION CLOSED connections 0#定义服务器地址 server_ip socket.gethostbyname(socket.gethostname()) server…

三城三奖!苏州金龙助力各地公共交通打造高品质线路

元旦前夕&#xff0c;由中国交通报社主办的绿色运输可持续发展座谈会暨2023年度“新能源公交高品质线路”经验交流会在京举行&#xff0c;来自全国各地的100余名行业管理部门、公交客运企业代表参会。会上同时评选出20条各具特色的“新能源公交高品质线路”及6家“我的公交我的…

后端主流框架-SpringMvc-day2

Java中的文件下载 2 文件下载 文件下载&#xff1a;就是将服务器&#xff08;表现在浏览器中&#xff09;中的资源下载&#xff08;复制&#xff09;到本地磁盘&#xff1b; 2.1 前台代码 前台使用超链接&#xff0c;超链接转到后台控制器&#xff0c;在控制器通过流的方式…

阿里云服务器(ECS云服务器)安装redis

前言&#xff1a; 笔者使用的是云服务器是阿里云的ECS服务器 这个服务器内核是Alibaba Cloud Linux 3。 使用的命令行工具为Alibaba Could Manager 命令行工具连接服务器这里就不多说了&#xff0c;如果没有用过的小伙伴可以去看阿里云的官方文档&#xff0c;很详细。 下面…

【51单片机系列】LCD1602液晶模块

本文是关于液晶显示屏的相关介绍。相对于静态数码管、动态数码管、LED点阵等&#xff0c;LCD1602液晶显示器能够显示更多的字符数字信息&#xff0c;并且也是常用的一种显示装置。 文章目录 一、LCD1602介绍1.1、LCD1602简介1.2、LCD1602常用指令1.3、LCD1602使用 二、LCD1602使…

[雷池WAF]长亭雷池WAF配置基于健康监测的负载均衡,实现故障自动切换上游服务器

为了进一步加强内网安全&#xff0c;在原有硬WAF的基础上&#xff0c;又在内网使用的社区版的雷池WAF&#xff0c;作为应用上层的软WAF。从而实现多WAF防护的架构。 经过进一步了解&#xff0c;发现雷池WAF的上游转发代理是基于Tengine的&#xff0c;所以萌生出了一个想法&…

SpringMVC-获取请求参数

1. 通过ServletAPI获取请求参数 /**** param request HttpServletRequest对象&#xff0c;直接作为形参传入方法&#xff0c;前端处理器就是一个Servlet* 所以前端处理器可以获得HttpServletRequest对象&#xff0c;并根据控制器方法的形参将对象传递给方法* re…

勒索事件急剧增长,亚信安全发布《勒索家族和勒索事件监控报告》

近期(12.15-12.21)态势快速感知 近期全球共发生了247起攻击和勒索事件&#xff0c;勒索事件数量急剧增长。 近期需要重点关注的除了仍然流行的勒索家族lockbit3以外&#xff0c;还有本周top1勒索组织toufan。toufan是一个新兴勒索组织&#xff0c;本周共发起了108起勒索攻击&a…

一文读懂$mash 通证的 “Fair Launch” 规则,将公平发挥极致

Solmash 是Solana生态中由社区主导的铭文资产LaunchPad平台&#xff0c;该平台旨在为Solana原生铭文项目&#xff0c;以及通过其合作伙伴SoBit跨链桥桥接到Solana的Bitcoin生态铭文项目提供更广泛的启动机会。有了Solmash&#xff0c;将会有更多的Solana生态的铭文项目、资产通…

【JUC的四大同步辅助类】

文章目录 一、CountDownLatch二、CyclicBarrier三、Semaphore四、Phaser 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、CountDownLatch CountDownLatch如同火箭发射&#xff0c;计数只能不断减减&#xff0c;当到达0时即发射 场景示例&#xff1…

elect函数可以设置等待时间,

欢迎关注博主 Mindtechnist 或加入【智能科技社区】一起学习和分享Linux、C、C、Python、Matlab&#xff0c;机器人运动控制、多机器人协作&#xff0c;智能优化算法&#xff0c;贝叶斯滤波与Kalman估计、多传感器信息融合&#xff0c;机器学习&#xff0c;人工智能&#xff0c…

ssm基于BS的仓库在线管理系统的设计与实现论文

摘 要 如今的时代&#xff0c;是有史以来最好的时代&#xff0c;随着计算机的发展到现在的移动终端的发展&#xff0c;国内目前信息技术已经在世界上遥遥领先&#xff0c;让人们感觉到处于信息大爆炸的社会。信息时代的信息处理肯定不能用之前的手工处理这样的解决方法&#x…

OpenCV-Python(23):傅里叶变换

原理 傅里叶变换是一种数学变换&#xff0c;用于将一个函数&#xff08;在图像处理中通常是图像&#xff09;从时域&#xff08;空域&#xff09;转换到频域。它将函数表示为一系列正弦和余弦函数的和&#xff0c;用于分析信号的频率和相位信息。 傅里叶变换的原理是将一个连续…