红黑树,先理解2-3树

67 阅读13分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第7篇文章,点击查看活动详情

首先,当然是直接摆出红黑树大名鼎鼎的五条基本性质了

  1. 节点是能被标记红色或者黑色。
  2. 根节点是黑色。
  3. 所有叶子节点都是黑色。(叶子节点是空节点)
  4. 不存在能够和两条红链接相连的节点。(每个红色的节点的两个子节点都是黑色)
  5. 从任意一个节点到叶子节点,经过的黑色节点数量是一样的。(黑平衡)

只要同时满足以上五个性质的二叉搜索树就可以称之为红黑树。

温馨提示:本文比较长,开始之前还请你耐下心来,一步一步仔细看完,我相信这篇文章会带给你一些收获。请继续加油!:)

红黑树其实也是一种二叉搜索树,只不过这是一种性能更好的二叉搜索树。二叉搜索树的结构决定了增删改查这些操作对于二叉搜索树而言都是 log(n) 级别的时间复杂度,所以这是一种高效的数据结构,但它有一个致命的缺陷,就是有序插入元素的情况下,它会严重的向一边倾斜退化成一个链表,也就是失衡。

所以,我们希望我们的二叉搜索树在任何情况下都能保持下面左图的样子,或者说是尽量保持成左图的样子,而永远不要退化成右图的样子。于是我们为二叉搜索树加入了平衡机制,其中红黑树就是这种机制之一。对于二叉搜索树的有序性而言,红黑树在这基础上又添加了平衡性。 以上红黑树的五条基本性质就是维持平衡的手段。

红黑树-1.png

像左图这样的树我们称之为平衡的,而红黑树仅仅只是维持平衡的手段而已,红黑树在本质上依然是一棵二叉搜索树。

像红黑树这样的数据结构,也不是一朝一夕的,而是有一个演化的过程,从二叉搜索树到红黑树之间就有一个不可缺少的过渡:2-3查找树

2-3查找树

定义:

  • 一棵 2-3查找树 要么是一棵空树,要么由2-节点3-节点组成。
  • 2-节点:含有一个节点本身的值和两条链接,左连接指向的2-3树值都小于该节点,右链接指向的2-3树值都大于该节点。
  • 3-节点:含有两个节点本身的值和三条链接,左连接指向的2-3树值都小于该节点,中链接指向的2-3树值都位于节点的两个值之间,右链接指向的2-3树值都大于该节点。

注意: 2-3查找树是一种完美平衡的树结构,也就是说一棵2-3树中的任意节点的左子树、中子树、右子树高度相等。

由完美平衡可知: 2-节点 要么没有两条链接都指向空,要么两条链接都不为空;3-节点 要么没有三条链接都指向空,要么三条链接都不为空。

如图:

红黑树-2.png

接下来将分析2-3树中的各种插入情况:

2-节点添加元素

首先,需要在2-3树中搜索到一个可以添加节点的位置,如果搜索结束于一个2-节点,那么很好办,只需要将这个2-节点替换成一个3-节点。像这样:

红黑树-3.png

其实向一个2-节点插入元素是非常简单的,只需要将一2-节点变换成一个3-节点即可。但是这很简单的一步却是保持 2-3树 完美平衡的最关键的一步

试想一个问题:一棵完美的平衡二叉树,也就是满二叉树,它的节点个数是奇数还是偶数? 答案是奇数,根节点的左子树和右子树的节点数是相同的,于是整棵树的节点数就是2 * 左子树的节点数 + 1。整棵树无论对于哪个节点求节点个数得出的答案都是奇数。

于是: 如果一棵二叉树的节点个数是偶数,那么这棵二叉树绝对不是完美平衡,想要得到一棵完美平衡的二叉树必备的一个条件就是树的节点个数为奇数。实际上一棵完美平衡的二叉树的节点数是等于2 ^ h - 1的。(其中h为树的高度,根节点的高度为1)

一棵二叉树在生长的过程中,依次将元素插入其中,这不可能保证整棵树的节点个数在任何情况下都是奇数。所以:在节点只能存放一个元素的情况下,不可能实现完美平衡的树结构。2-3树中的3-节点正是为了解决这一情况而存在的。

如果向二叉树中的一个左右子树都为空的节点插入元素无论插入在左子树还是右子树中肯定会造成这个节点的不完美平衡,但是现在引入了3-节点这样的一个过渡的节点,说白了,这其实是在暂存一下这种不平衡的情况,一旦在这个3-节点中再次插入一个元素之后立马会分裂成为一棵左右子树都不为空的二叉树。

3-节点添加元素

3-节点 添加元素有以下三种情况:

  • 整棵树只有一个 3-节点
  • 向一个父节点为 2-节点3-节点 添加元素
  • 向一个父节点为 3-节点3-节点 添加元素
  • 向 “从插入节点出发直到根节点全部都是 3-节点” 的节点添加元素

整棵树只有一个 3-节点

单纯的向一棵仅有一个三节点的2-3树添加元素是比较简单的,这里简单讲一下思路即可。

  1. 将新节点插入3-节点中,组成一个临时的4-节点。此时这个4-节点中包含三个值和四条链接。

  2. 将这个4-节点的三个值都抽取出来成为三个2-节点,中间的值抽取出来的2-节点作为根,根的左链接指向三个节点中的最小值,右链接指向三个节点中的最大值。

红黑树-4.png

你可能会注意到:将4-节点分裂为三个2-节点的过程中,树的高度 +1 了。这也是2-3树的生长方式,当根节点已经是一个3-节点的时候插入一个新的节点,此时根节点分裂为三个2-节点,树高度 +1

与普通的二叉查找树不同的是2-3树的生长方向是由下至上生长的,而二叉查找树的生长方向是由上至下生长的。

父节点为 2-节点3-节点

  1. 插入到 3-节点 中,组成一个临时的 4-节点

  2. 4-节点 中间的值抽取出来的根节点插入至父节点,使父节点从原来的2-节点变成3-节点, 此处插入过程与 “向2-节点添加元素” 相同。

  3. 4-节点分裂出来的两个2-节点移动至父节点中,成为3-节点的两个子树。

红黑树-5.png

父节点为 3-节点3-节点

  1. 插入到 3-节点 中,组成一个临时的 4-节点

  2. 把临时的 4-节点 中间的值抽取出来的根节点插入至父节点,使父节点从原来的 3-节点 变成新的临时 4-节点

  3. 4-节点 分裂出来的两个 2-节点 移动到父节点也就是新的临时 4-节点 中,成为新的临时 4-节点 的两个子树。

  4. 将新的临时 4-节点 分裂出来的根继续往上插入,插入的过程前面相同。

红黑树-6.png

从插入节点出发直到根节点全部都是 3-节点

  1. 一直按照 “父节点为3-节点3-节点” 的方式插入元素,那么直到最后根节点也会变成一个临时的 4-节点

  2. 将根节点转换成的临时 4-节点 分裂开来,按照 “向3-节点添加元素” 的方式插入,临时 4-节点 中间的值成为整颗 2-3树 新的根,此时树高 +1

红黑树-7.png


小结

  1. 2-节点 插入元素:直接插入至2-节点中生成一个三节点。

  2. 3-节点 插入元素:

    1. 父节点为 2-节点:组成临时4-节点,分裂后的根插入父节点组成3-节点

    2. 父节点为 3-节点:组成临时4-节点,分裂后的根插入父节点,再次组成临时4-节点。 不断重复此过程,直到找到一个2-节点为止。如果直到根节点还找不到,则将根节点分裂成三个2-节点,此时树高加一。

红黑树

我认为应该这么来理解红黑树:使用二叉树来表现2-3树。换句话说,2-3树和红黑树是等价的。

其实一般的红黑树是等价于 2-3-4树 的,也就是我们前面说的 2-3树 中再加了一个4-节点。但那样的红黑树太复杂,用 2-3树 来表示红黑树更方便理解。

2-3树 表示的红黑树被分为左倾红黑树和右倾红黑树,这里只要先记住左倾红黑树的红连接是左连接,右倾红黑树的红连接是右连接。

二叉树是怎样表现 2-3树 的

  • 二叉树怎样表现 2-节点

    2-3树 中的 2-节点 跟二叉树中的普通节点是相同的,也就是说普通的节点就可以表示 2-3树 中的 2-节点

  • 二叉树怎样表现 3-节点

    3-节点 中的两个值分别当作是两个 2-节点 分裂开,中间使用一条连接将这两个 2-节点 相连接起来,这条连接称为红连接,这样两个 2-节点 就代表了一个 3-节点

    二叉树中两个 2-节点 之间的连接会存在父子关系。左倾红黑树就是在 3-节点 的表示中,红连接一定是指向左子树的,右倾红黑树的红连接则一定是指向了右子树。

红黑树-8.png

红黑树的红和黑其实是用来表示指向该节点链接的颜色,为了方便表示红链接我们注意到每个红色链接都会指向唯一的一个节点,所以我们将节点标记为红色来表示指向此节点的链接是红链接,相反的没有被红链接指向的节点全部就被标记为黑色。

本文一开始就提出的红黑树的五条基本性质中的第1点、第2点、第3点也就是这么来的,性质4和性质5稍微有一些抽象:

  1. 节点是红色或者黑色 (每个节点都会被标记为红色或者黑色)

  2. 根节点是黑色 (二叉树中没有链接指向根节点,所以根节点被标记为黑色)

  3. 所有叶子节点都是黑色 (叶子节点都是空节点,没有被红链接所指向,所以标记为黑色)

  4. 不存在能够和两条红链接相连的节点(红连接是 3-节点 的内部连接)

  5. 从任意一个节点到叶子节点,经过的黑色节点数量是一样的(试试把红链接画平,下图粗链接表示为红链接)

红黑树-9.png

着色和旋转

用二叉树表示 2-3树 显然还需要一些手段来处理把 2-节点 升级为 3-节点、把 3-节点 分裂成3个 2-节点、以及分裂过程中临时 4-节点 等问题。

处理这些问题的手段就是着色和旋转,其中旋转又分为左旋转和右旋转。

着色: 着色操作比较简单,只需要改变对节点颜色的标记即可。

左旋转和右旋转: 下图中粗链接表示为红链接

红黑树-10.png

注意:旋转过后应该保持根节点颜色相同,以及这里的旋转过程是在插入节点时调用的,在2-3树中插入操作总是和树中的节点相互融合的,所以要将旋转后的节点标记为红色。(图中黄颜色的标记就是旋转着色过程)

具体的旋转过程在AVL 树这篇文章中已经有具体的说明,这里不再过多的赘述。


在正式开始插入操作之前,有一点需要先注意:新插入的节点默认是红色的节点,因为在插入的时候除非是插入在根节点的位置之外,所有的插入首先都是与原有的节点做融合操作,而融合进其他节点的节点在红黑树中被标记为红色。

向红黑树中的“2-节点”插入元素

向一个2-节点中插入元素之后,会与原来的2-节点一起形成一个3-节点。这会出现两种情况:

  1. 插入节点小于2-节点,插入在2-节点的左边,成为2-节点的左孩子。
    符合左倾红黑树的定义,即红链接在左边,此时无需修改。

  2. 插入节点大于2-节点,插入在2-节点的右边,成为2-节点的右孩子。
    不符合左倾红黑树的定义,即红链接在右边,此时需要对被插入节点做一次左旋转的操作,以将右红链接纠正为左红链接。

红黑树-11.png

向红黑树中的“3-节点”插入元素

向一个3-节点中插入元素之后,与原来的3-节点之间会产生三种情况:

  1. 插入的元素小于 3-节点 的两个元素。
  2. 插入的元素位于 3-节点 的两个元素之间。
  3. 插入的元素大于 3-节点 的两个元素。

红黑树-12.png

插入代码实现

public class RBTree {

    private static final boolean RED = true;
    private static final boolean BLACK = false;

    private static class Node {
        int e;
        Node left, right;
        boolean color;

        public Node(int e) {
            this.e = e;
            this.left = null;
            this.right = null;
            this.color = RED;
        }
    }

    private Node root;
    private int size;

    private Node leftRotate(Node node) {
        Node x = node.right;  // 暂存
        
        node.right = x.left;  // 旋转
        x.left = node;
        
        x.color = node.color; // 着色
        node.color = RED;

        return x;
    }

    private Node rightRotate(Node node) {
        Node x = node.left;   // 暂存

        node.left = x.right;  // 旋转
        x.right = node;

        x.color = node.color; // 着色
        node.color = RED;

        return x;
    }

    private boolean isRed(Node node) {
        return node == null ? BLACK : node.color;
    }

    public void add(int e) {
        add(root, e);
        root.color = BLACK;
    }

    private Node add(Node node, int e) {
        if (node == null) { // 插入整棵树的根节点
            size++;
            return new Node(e);
        }

        if (node.e > e) { // 向右插入
            node.left = add(node.left, e);
        }
        if (node.e < e) { // 向左插入
            node.right = add(node.right, e);
        }
        
        // 把右红连接纠正为左红连接
        if (isRed(node.right) && !isRed(node.left))
            node = leftRotate(node);

        // 新元素小于 3-节点 的两个元素。
        if (isRed(node.left) && isRed(node.left.left))
            node = rightRotate(node);

        // 3-节点裂变为3个 2-节点
        if (isRed(node.left) && isRed(node.right)) {
            node.color = RED;
            node.left.color = BLACK;
            node.right.color = BLACK;
        }
        
        return node;
    }
}

巨人的肩膀:

  • 《算法:第4版》