【数据结构】红黑树

1,113 阅读6分钟

1. 前身2-3-4树

  学习红黑树之间,我们需要了解其前身,也就是2-3-4树。该树结构是一种绝对平衡的多叉树,有3种节点类型:

  • 2节点:这种节点有2个子节点
  • 3节点:这种节点有3个子节点
  • 4节点:这种节点有4个子节点

2-3-4树节点类型.png

1.1 添加操作

  以下是往一棵2-3-4树添加元素1-10的过程,该过程很好地体现出2-3-4树的绝对平衡性质。

2-3-4树添加操作.png

1.2 与红黑树的比较

  我们先了解一下红黑树的以下几个性质:

  1. 每个节点是红色或者黑色
  2. 根节点是黑色
  3. 红色节点的父节点或子节点都是黑色,即不存在连续两个红色节点
  4. 叶子节点都是不存放数据的空节点,称为NIL节点(其他文章里可能有其他的称呼,但意思一样),该节点也是黑色
  5. 从任一节点出发,到任一NIL叶子节点都会经过相同数量的黑色节点,即黑平衡

如下图就是一棵红黑树,从根节点15出发,到任一NIL叶子节点都经过了相同数量的黑色节点

红黑树.png

  那么红黑树与上述提到的2-3-4树有什么关系呢?当我们把红色节点"移动到"与其父节点同一高度时,我们就能发现它就变成了一棵2-3-4树,如下图所示。红黑树的平衡性质也就能直观地体现出来。

2-3-4树演变为红黑树.png

1.3 总结

  1. 红黑树中黑色节点个数 等于 2-3-4树的节点个数
  2. 如果一个节点是红色,那么它对应的2-3-4树的节点位置,是在两边
  3. 否则如果一个节点是黑色,那么它对应的2-3-4树的节点位置,是在中间

2. 红黑树的操作

  通过章节1的内容,我们已经掌握了红黑树的前身和性质,下面介绍红黑树的操作。

2.1 添加

  红黑树的添加操作共有12种场景,如下图所示:8种场景是在红色节点下添加,4种场景是在黑色节点下添加。注意:新添加的节点默认为红色,因为红色节点并不影响红黑树的高度/黑平衡,以红色节点添加进来更能满足红黑树的性质,越满足红黑树的性质,后续的修复操作就越少。

红黑树的添加场景.png

2.1.1 CASE 9-12

  这4种场景最简单,因为是在黑色节点下添加,相当于在2-3-4树中,往2节点或者3节点添加一个节点,这并不影响整棵树的高度,且不违背红黑树的性质,因此这4种场景没有修复操作。

  剩下的场景都是在红色节点下进行添加,由于红黑树不能有连续两个红色节点,因此后续的8种场景都会有对应的修复操作。

2.1.2 CASE 1-4

  这4种场景相当于在2-3-4树的4节点进行添加操作,修复操作操作如下:

  1. 把父节点、叔叔节点变为黑色,祖父节点变为红色
  2. 祖父节点变为红色后,把它看成一个新添加的节点,向上进行递归添加操作。对应在2-3-4树的4节点进行添加时的上溢
  3. 最终,如果根节点无法"消化"递归传递上来的的红色节点,则整棵树的黑色高度会加一

红黑树的添加场景1.png

2.1.3 CASE 6和7

  这两种场景是对称的,添加位置分别是RR(父节点是祖父节点的孩子,新节点是父节点的孩子)和LL,以CASE 6为例进行介绍。对应在2-3-4树的3节点添加数据变成4节点,但由于违背不存在连续两个红色节点的性质,因此需要修复,修复操作如下:

  1. 父节点变为黑色、祖父节点变为红色
  2. 左旋祖父节点

操作完成后,即可结束,不需要向上递归操作。

红黑树的添加场景2.png

2.1.4 CASE 5和8

  这两种场景是对称的,添加位置分别是RL(父节点是祖父节点的孩子,新节点是父节点的孩子)和LR,以CASE 5为例进行介绍。对应在2-3-4树的3节点添加数据变成4节点,但由于违背不存在连续两个红色节点的性质,因此需要修复,修复操作如下:

  1. 右旋父节点
  2. 把原来的父节点看成是新添加的节点,此时变成与CASE 6一样的场景
  3. 重复CASE 6的修复操作

操作完成后,即可结束,不需要向上递归操作。

红黑树的添加场景3.png

2.2 删除

挖坑待更新。。。

3. 代码实现

  以下代码逻辑与JAVA的TreeMap实现一致。

public class RedBlackTree<K> {

    private static final int RED = 1;
    private static final int BLACK = 0;

    private final Comparator<K> comparator;
    private final Node<K> NIL;
    private Node<K> root;

    private static class Node<K> {
        private K data;
        private Node<K> left;
        private Node<K> right;
        private Node<K> parent;
        private int color;
    }

    public RedBlackTree(Comparator<K> comparator) {
        this.comparator = comparator;

        NIL = new Node<>();
        NIL.color = BLACK;
        NIL.left = null;
        NIL.right = null;
        root = NIL;
    }

    private Node<K> minimum(Node<K> node) {
        while (node.left != NIL) {
            node = node.left;
        }
        return node;
    }

    private Node<K> maximum(Node<K> node) {
        while (node.right != NIL) {
            node = node.right;
        }
        return node;
    }

    /**
     * 对X进行左旋
     *
     *     p                  p
     *     |                  |
     *     X      ----->      Y
     *    / \                / \
     *   a   Y              X   c
     *      / \            / \
     *     b   c          a   b
     */
    private void leftRotate(Node<K> x) {
        Node<K> y = x.right;
        x.right = y.left;
        if (y.left != NIL) {
            y.left.parent = x;
        }
        Node<K> p = x.parent;
        y.parent = p;
        if (p == null) {
            this.root = y;
        } else if (x == p.left) {
            // 如果x是p的左子节点,那么y就应该变为左子节点
            p.left = y;
        } else {
            p.right = y;
        }
        y.left = x;
        x.parent = y;
    }


    /**
     * 对X进行右旋
     *
     *       p               p
     *       |               |
     *       X    ----->     Y
     *      / \             / \
     *     Y   c           a   X
     *    / \                 / \
     *   a   b               b   c
     */
    private void rightRotate(Node<K> x) {
        Node<K> y = x.left;
        x.left = y.right;
        if (y.right != NIL) {
            y.right.parent = x;
        }
        Node<K> p = x.parent;
        y.parent = p;
        if (p == null) {
            this.root = y;
        } else if (x == p.right) {
            p.right = y;
        } else {
            p.left = y;
        }
        y.right = x;
        x.parent = y;
    }

    /**
     * 插入一个新元素
     */
    public void insert(K data) {
        // 创建一个新节点
        Node<K> node = new Node<>();
        node.parent = null;
        node.data = data;
        node.left = NIL;
        node.right = NIL;
        // 新节点默认红色
        node.color = RED;

        Node<K> p = null;
        Node<K> x = this.root;
        while (x != NIL) {
            p = x;
            if (comparator.compare(data, x.data) < 0) {
                x = x.left;
            } else {
                x = x.right;
            }
        }
        // 此时x是最下层的NIL空节点,p是新节点的父节点
        node.parent = p;
        if (p == null) {
            this.root = node;
        } else if (comparator.compare(data, p.data) < 0) {
            p.left = node;
        } else {
            p.right = node;
        }

        // 如果新节点就是根节点,变为黑色,直接返回
        if (node.parent == null) {
            node.color = BLACK;
            return;
        }
        // 如果新节点是根节点的左/右子节点,直接返回
        if (node.parent.parent == null) {
            return;
        }

        fixInsert(node);
    }

    /**
     * 插入新节点后的修复
     * @param k 插入后的新节点
     */
    private void fixInsert(Node<K> k) {
        // 叔叔节点,即k的父节点的兄弟节点
        Node<K> uncle;
        // 当父节点是红色时,才需要修复,即前8个CASE
        while (k.parent.color == RED) {
            if (k.parent == k.parent.parent.right) {
                // 如果父节点是右节点,对应CASE 3、4、5、6
                uncle = k.parent.parent.left;
                if (uncle.color == RED) {
                    // 如果叔叔节点是红色,对应CASE 3、4
                    // 此时把叔叔节点、父节点变为黑色、祖父节点变为红色
                    uncle.color = BLACK;
                    k.parent.color = BLACK;
                    k.parent.parent.color = RED;
                    // 把祖父节点赋给k(即把祖父节点看成新节点),向上循环调整
                    k = k.parent.parent;
                } else {
                    // 否则叔叔为黑色时,对应CASE 5、6 (此时叔叔节点是黑色的NIL)
                    if (k == k.parent.left) {
                        // 如果是CASE 5,对父节点进行右旋,旋转后的箭头方向与CASE 6相同
                        k = k.parent;
                        rightRotate(k);
                    }
                    // 父节点变为黑色、祖父节点变为红色,然后左旋祖父节点,修复完成
                    k.parent.color = BLACK;
                    k.parent.parent.color = RED;
                    leftRotate(k.parent.parent);
                }
            } else {
                // 否则父节点是左节点,对应CASE 1、2、7、8
                uncle = k.parent.parent.right;
                if (uncle.color == RED) {
                    // 如果叔叔节点是红色,对应CASE 1、2
                    // 此时把叔叔节点、父节点变为黑色、祖父节点变为红色
                    uncle.color = BLACK;
                    k.parent.color = BLACK;
                    k.parent.parent.color = RED;
                    // 把祖父节点赋给k(即把祖父节点看成新节点),向上循环调整
                    k = k.parent.parent;
                } else {
                    // 否则叔叔为黑色时,对应CASE 7、8 (此时叔叔节点是黑色的NIL)
                    if (k == k.parent.right) {
                        // 如果是CASE 8,对父节点进行左旋,旋转后的箭头方向与CASE 7相同
                        k = k.parent;
                        leftRotate(k);
                    }
                    // 父节点变为黑色、祖父节点变为红色,然后右旋祖父节点,修复完成
                    k.parent.color = BLACK;
                    k.parent.parent.color = RED;
                    rightRotate(k.parent.parent);
                }
            }
            if (k == root) {
                break;
            }
        }
        root.color = BLACK;
    }

}

4. 参考资料