红黑树详解二


  在 红黑树详解一中,我们主要具体分析了 2-3 树与红黑树的等价性。下面然我们来看看怎样向红黑树中添加一个节点,首先能够确定的是,新添加的元素的颜色我们默认设置为红色,这是因为在 2-3 树中插入一个新结点时,一定不会插入在空的位置上,而一定是与一个 2 节点或者一个 3 节点 做融合,既然存在融合,那么势必就有一个红色的节点,因为在红黑树中,我们定义,只有红色的节点才代表它与其父节点在原来的 2-3 树中一个 3 节点的,是融合的。

一、红黑树添加新元素

1. 保持根节点为黑色和左旋转

下面来实现一下添加过程:

(1) 当我们向一颗空的红黑树中添加一个节点 42 时,首先先将该节点的颜色设置为红色。上面已经提到过,永远将新添加的节点的颜色设置为红色。
在这里插入图片描述
  所以,此时我们的根节点就为红色,但是通过红黑树的性质可以知道,红黑树的根节点永远都是黑色的。所以,我们需要将根节点的颜色调整为黑色。
在这里插入图片描述

(2) 如果我们继续往红黑树中添加一个节点 37,并且颜色为红色,那么它应该添加到节点 42 的左子树中,此时,它依然是一颗红黑树。这样就相当与 2-3 树中的一个 3 节点,节点中存在元素 37 和 42。
在这里插入图片描述
  但是也有可能,我们插入的这个节点在根节点的右侧。很显然,这是不满足我们定义的红色节点永远是向左倾斜的。那么,此时我们需要怎么调整呢?在这里插入图片描述
其实,非常简单,做和之前在 AVL 树中的处理方法是一样的,我们只需要进行一次左旋转。变成如下图所示:
在这里插入图片描述
下面,我们继续看一下左旋转的过程是怎样的。其实这个过程和 AVL 树中的左旋转是一致的。

左旋转

(1) 我们简单做一下定义:假设 37 这个节点我们叫做 node,相应的,这个 node 的右孩子我们叫做 x,现在相当于 node 的右孩子是一个红色的节点,我们要进行一下左旋转。这个左旋转在 node 上进行。
在这里插入图片描述
  其实,在 37 和 42 两个节点上还可能会存在左右子树,为了不失一般性,我们将其抽象成下图所示:
在这里插入图片描述
  首先,我们让 node 的右孩子等于 x 的左子树,即 node->right = x->left,然后将 x 的左子树等于 node,即 x->left = node,那么整棵树变成了下图所示:
在这里插入图片描述
  那么,从二分搜索树的角度,已经完成了左旋转的过程,不过在这里,由于我们编写的是红黑树,还需要维护红黑树的节点颜色。在这里,x 的颜色应该是等于 node 的颜色,是因为在左旋转之前的树中,node 为根节点,现在 x 变成了根节点,所以根节点的颜色应该是保持一致的,原来 node 是什么颜色,现在 x 就是什么颜色,即 x->color = node->color; 。那么现在很重要的一点,就是 node 的颜色应该将它变成红色,是因为我们新加入的这个节点在这个例子中是 42,它是和 37 这个节点形成一个 3 节点,那么我们通过左旋转之后,并没有改变 这个 3 节点的两个元素,依然是 37 和 42,我们为了表示 37 和 42 原来是一个 3 节点,我们就需要将 37 标成红色,即 node->color = RED;
在这里插入图片描述
  也许在你这里你可能会有疑问,如果原来 node 为红色,现在将 x 的颜色也变成了红色,而 node 也要变成红色,那不是违反了红黑树的性质吗?答案确实是这样。

  其实,这个左旋转在这里只是一个子过程。在左旋转之后,有可能会产生两个连续的红色节点,然后我们会将左旋转之后的根节点传回去,传回去之后,在我们的添加逻辑中还需要进行更多的后续处理。这个过程将在后面详细介绍,在这里,只需要注意,左旋转的过程并不维持红黑树的性质。

下面是左旋转的代码:

		// 左旋转
        Node *leftRotate(Node *node)
        {
            Node *x = node->right;
            
            // 左旋转
            node->right = x->left;
            x->left = node;
            
            x->color = node->color;
            node->color = RED;
            
            return x;
        }

2. 颜色翻转和右旋转

  上面一部分已经详细阐述了什么时候进行左旋转,下面就是介绍什么情况下我们需要颜色翻转和右旋转。

颜色翻转

  在下图所示情况下,我们需要进行颜色翻转。我们新插入一个新结点 66,它是比原来在 2-3 树中的 3 节点中元素最大值还要大。此时,我们就只需要进行颜色翻转。
在这里插入图片描述
这个颜色翻转就是让节点 42 的两个孩子节点都变成黑节点。
在这里插入图片描述
  变成这个样子之后,根节点要继续向它的父亲节点做融合,既然要做融合,那么这个根节点的颜色就应该是红色。红色的节点才表示它要和它的父亲节点做融合。
在这里插入图片描述
那么,这个过程我们就叫做颜色翻转,fileColors。下面我们就编写 颜色翻转的代码。

		// 颜色翻转
        void  flipColors(Node *node)
        {
            node->left->color = node->right->color = BLACK;
            node->color = RED;
        }

颜色翻转说完了,接下来就是右旋转了。


右旋转

  在下图所示情况下,我们需要进行右翻转。我们新插入一个新结点 12,它是比原来在 2-3 树中的 3 节点中元素最小值还要小。此时,我们就只需要进行右翻转。
在这里插入图片描述
  我们假设根节点是 node 的话,node的左孩子叫做 x,首先我们让 node 的左孩子等于 x 的右孩子,即 node->left = x->right,然后将 x 的右孩子等于 node,即 x->right = node;

在这里插入图片描述
  那么,接下来就是维护红黑树节点的颜色。其实跟左旋转类似,x 的颜色应该与原来的根节点 node 的颜色一直,即 x->color = node->color;,另外一方面,现在 node 变成了 x 的右孩子,反应到 2-3 树中,它们形成的是一个临时的 4 节点,是融合的。所以我们需要将 node 的颜色变成红色。即 node->color = RED; 表示它和它的父节点是融合的。
在这里插入图片描述
  那么,这样就完成了整个的右旋转过程。然后这种情况就变成了上一步进行颜色翻转的那种情况。下面我们实现一下右旋转:

		// 右旋转
        Node *rightRotate(Node *node)
        {
            Node *x = node->left;

            // 右旋转
            node->left = x->right;
            x->right = node;

            x->color = node->color;
            node->color = RED;

            return x;
        }

3. 总结插入元素的三种情况

  如果我们继续向红黑树中添加一个节点 66,那么 66 应该添加在 42 的右子树中。那么,在这种情况下,红黑树应该怎样做操作?
在这里插入图片描述
  可以想象一下,在上面的添加过程中,在 2-3 树,对应下面的过程。将一个 3 节点添加成了一个临时的 4 节点。这是一种添加的节点比原来的 2-3 树中的 3 节点中的最大值还要大。
在这里插入图片描述
  那么相反还有一种情况,就是添加的元素比原来的 2-3 树中的 3 节点的最小值还要小。对应下面这种情况,添加节点 12。
在这里插入图片描述
  在之前的 2-3 树中,我们只分析了上面两种情况,其实还有一种,就是插入的元素是介于原来的 3 节点的两元素大小之间的,比如插入节点为 40。那么,按照二叉搜索树的添加策略,40 应该添加在 37 的右孩子中。
在这里插入图片描述
  那么,对于这种情况,我们应该怎样处理呢?我们处理这种情况,不再需要定义新的子过程了。用我们之前定义的三个子过程就可以实现了。首先,我们需要基于 37 这个节点进行一次左旋转,左旋转之后,我们的红黑树变成了下图所示:
在这里插入图片描述
  那么,它已经变化了我们之前处理过的一种情况。它其实相当于是在 40 和 42 这个 3 节点中插入一个 节点 37。这个处理过程十分容易,我们再针对 42 这个节点进行一次右旋转。得到的这棵树是以 40 为根的树。在这里插入图片描述
  当然,相应节点的颜色还需要进行调整。所以还需要进行一次颜色的翻转。最终变成下图所示:
在这里插入图片描述
  所以,对于这种情况,我们需要进行一次左旋转+右旋转+颜色翻转。
在这里插入图片描述

(1) 添加的元素大小位于两元素之间:
在这里插入图片描述
(2) 添加的元素比两元素还要小:直接就是进行右旋转和颜色翻转。
在这里插入图片描述
(3) 如果添加的元素比原来的两元素还要大:我们直接进行颜色翻转即可。
在这里插入图片描述
其实,对于以上的三种情况,我们都可以用一个逻辑条将其串起来。这是是等价于在原来的 2-3 树中添加一个新结点,其实对于在 1 个 2 节点中添加元素,也可以该逻辑实现。而这个维护的时机,和 AVL 树一样。

下面就来可以实现一个完整的插入过程了。

 		// 向以 node 为根节点的红黑树中插入节点(key,val)
        // 返回插入新节后的红黑树的根
       Node *__insert(Node *node, K key, V val)
        {
            if(node == nullptr)
            {
                count++;
                return new Node(key, val);  // 默认是红色节点
            }
            if(key == node->key)
            {
                node->val = val;
            }
            else if(key < node->key)
                node->left = __insert(node->left, key, val);
            else
                node->right = __insert(node->right, key, val);

            // 维护红黑树的性质
            // 1. 首先判断是否需要进行左旋转:即当前节点的右孩子是红色的节点,并且左孩子不是红色节点
            if(isRed(node->right) && !isRed(ndoe->left))
            {
                //左旋转
                node = leftRotate(node);
            }
            // 2. 判断是否需要进行右旋转:即判断当前节点的左孩子和其左孩子的左孩子都是红色
            if(isRed(node->left) && isRed(node->left->left)))
            {
                node = rightRotate(node);
            }
            // 3. 判断是否需要进行颜色翻转:即当前节点的左右孩子都是红色
            if(isRed(node->left) && isRed(node->right))
            {
                // 颜色翻转
                flipColors(node);
            }
            return node;
        }

二、红黑树的性能总结

  1. 对于完全随机的数据,普通的二分搜索树很好用(二分搜索树不会退化成链表),缺点就是极端情况(几乎有序)退化成链表(或者高度不平衡)
  2. 对于查询较多的使用情况,AVL 树很好用!但是对于红黑树来说,它牺牲了平衡性(2logn的高度)。
  3. 红黑树统计性能更优(综合增删改查所有的操作)。正是因为红黑树统计性能更优,也就是平均情况下会更好一些,所以很多语言内部的容器中所实现的有序的映射就是用红黑树实现的,比如 STL 容器的 set 和 map。这里强调有序,是因为红黑树本身还是一颗二分搜索树。

三、更多和红黑树相关的话题

  1. 上面我们实现的一个红黑树是一个左倾的红黑树。它是等价于一颗 2-3 树。而左倾红黑树是一种比较标准的红黑树的实现方式。
    在这里插入图片描述
      不过,这不是唯一的实现方式。实际上我们也完全可以使用下图的方式实现一颗红黑树。我们叫做右倾红黑树。
    在这里插入图片描述

  2. 我们上面说到,红黑树是一种统计性能更优的树结构,那么说到统计性能更优,那么就想到另一种统计性能优秀的树结构:Splay Tree(伸展树)。伸展树也是一种二叉树,它也可以维持自平衡,不过它对平衡的定义也没有 AVL Tree 那么严格。Splay Tree 更重要的一点是运用了局部性的原理刚被访问的内容下次高概率被再次访问

  3. 基于红黑树的 map 和 set

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值