红黑树的定义和插入操作

本文共分为两篇

红黑树的定义和插入操作

和本篇 红黑树的删除操作

一、红黑树的介绍

红黑树,一种二叉查找树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。
通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。

红黑树,作为一棵二叉查找树,满足二叉查找树的一般性质。下面,来了解下二叉查找树的一般性质。

二叉查找树

二叉查找树,也称有序二叉树(ordered binary tree),或已排序二叉树(sorted binary tree),是指一棵空树或者具有下列性质的二叉树:

1.若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
2.若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
3.任意节点的左、右子树也分别为二叉查找树。
4.没有键值相等的节点(no duplicate nodes)。

因为一棵由n个结点随机构造的二叉查找树的高度为lgn,所以顺理成章,二叉查找树的一般操作的执行时间为O(lgn)。但二叉查找树若退化成了一棵具有n个结点的线性链后,则这些操作最坏情况运行时间为O(n)。点击查看排序和查找算法参考。

如下图:
在这里插入图片描述

为了改变排序二叉树存在的不足,Rudolf Bayer 在 1972 年发明了另一种改进后的排序二叉树:红黑树,他将这种排序二叉树称为“对称二叉 B 树”,而红黑树这个名字则由 Leo J. Guibas 和 Robert Sedgewick 于 1978 年首次提出。

红黑树定义

红黑树虽然本质上是一棵二叉查找树,但它在二叉查找树的基础上增加了着色和相关的性质使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(log n)。

但它是如何保证一棵n个结点的红黑树的高度始终保持在logn的呢?这就引出了红黑树的5个性质:

1.每个结点要么是红的要么是黑的。
2.根结点是黑的。
3.每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的。
4.如果一个结点是红的,那么它的两个儿子都是黑的。
5.对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点。

正是红黑树的这5条性质,使一棵n个结点的红黑树始终保持了logn的高度(红黑树的高度至多为2log(n+1)证明略),从而也就解释了上面所说的“红黑树的查找、插入、删除的时间复杂度最坏为O(log n)”这一结论成立的原因。

在这里插入图片描述

注意:
上文中我们所说的 “叶结点” 或"NULL结点",它不包含数据而只充当树在此结束的指示,这些结点以及它们的父结点,在绘图中都会经常被省略。
性质 4 的意思是:从每个根到节点的路径上不会有两个连续的红色节点,但黑色节点是可以连续的。
因此若给定黑色节点的个数 N,最短路径的情况是连续的 N 个黑色,树的高度为 N - 1;最长路径的情况为节点红黑相间,树的高度为 2(N - 1) 。
性质 5 是成为红黑树最主要的条件,后序的插入、删除操作都是为了遵守这个规定。红黑树并不是标准平衡二叉树,它以性质 5 作为一种平衡方法,使自己的性能得到了提升。

Java中的红黑树

TreeMap 和 TreeSet 是 Java Collection Framework 的两个重要成员,其中 TreeMap 是 Map 接口的常用实现类,而 TreeSet 是 Set 接口的常用实现类。虽然 HashMap 和 HashSet 实现的接口规范不同,但 TreeSet 底层是通过 TreeMap 来实现的,因此二者的实现方式完全一样。而 TreeMap 的实现就是红黑树算法。

对于 TreeMap 而言,由于它底层采用一棵“红黑树”来保存集合中的 Entry,这意味这 TreeMap 添加元素、取出元素的性能都比 HashMap 低:当 TreeMap 添加元素时,需要通过循环找到新增 Entry 的插入位置,因此比较耗性能;当从 TreeMap 中取出元素时,需要通过循环才能找到合适的 Entry,也比较耗性能。

但 TreeMap、TreeSet 比 HashMap、HashSet 的优势在于:TreeMap 中的所有 Entry 总是按 key 根据指定排序规则保持有序状态,TreeSet 中所有元素总是根据指定排序规则保持有序状态。

二、树的旋转知识

在这里插入图片描述

红黑树的左右旋是比较重要的操作,左右旋的目的是调整红黑节点结构,转移黑色节点位置,使其在进行插入、删除后仍能保持红黑树的 5 条性质
比如 X 左旋(右图转成左图)的结果,是让在 Y 左子树的黑色节点跑到 X 右子树去。
我们以 Java 集合框架中的 TreeMap 中的代码来看下左右旋的具体操作方法:
指定节点 x 的左旋 (右图转成左图):

 //这里 p 代表 x
private void rotateLeft(Entry p) {
    if (p != null) {
        Entry r = p.right; // p 是上图中的 x,r 就是 y
        p.right = r.left;       // 左旋后,x 的右子树变成了 y 的左子树 β 
        if (r.left != null)         
            r.left.parent = p;  //β 确认父亲为 x
        r.parent = p.parent;        //y 取代 x 的第一步:认 x 的父亲为爹
        if (p.parent == null)       //要是 x 没有父亲,那 y 就是最老的根节点
            root = r;
        else if (p.parent.left == p) //如果 x 有父亲并且是它父亲的左孩子,x 的父亲现在认 y 为左孩子,不要 x 了
            p.parent.left = r;
        else                            //如果 x 是父亲的右孩子,父亲就认 y 为右孩子,抛弃 x
            p.parent.right = r;
        r.left = p;     //y 逆袭成功,以前的爸爸 x 现在成了它的左孩子
        p.parent = r;
    }
}

可以看到,x 节点的左旋就是把 x 变成右孩子 y 的左孩子,同时把 y 的左孩子送给 x 当右子树。
简单点记就是:左旋把右子树里的一个节点(上图 β)移动到了左子树。
指定节点 y 的右旋(左图转成右图):

private void rotateRight(Entry p) {
    if (p != null) {
        Entry l = p.left;
        p.left = l.right;
        if (l.right != null) l.right.parent = p;
        l.parent = p.parent;
        if (p.parent == null)
            root = l;
        else if (p.parent.right == p)
            p.parent.right = l;
        else p.parent.left = l;
        l.right = p;
        p.parent = l;
    }
}

同理,y 节点的右旋就是把 y 变成左孩子 x 的右孩子,同时把 x 的右孩子送给 y当左子树。
简单点记就是:右旋把左子树里的一个节点(上图 β)移动到了右子树。
了解左旋、右旋的方法及意义后,就可以了解红黑树的主要操作:插入、删除。

三、红黑树的平衡插入

将一个节点插入到红黑树中,需要执行哪些步骤呢?首先,将红黑树当作一颗二叉查找树,将节点插入;然后,将节点着色为红色;最后,通过旋转和重新着色等方法来修正该树,使之重新成为一颗红黑树。详细描述如下:

第一步: 将红黑树当作一颗二叉查找树,将节点插入。

红黑树本身就是一颗二叉查找树,将节点插入后,该树仍然是一颗二叉查找树。
也就意味着,树的键值仍然是有序的。此外,无论是左旋还是右旋,若旋转之前这棵树是二叉查找树,旋转之后它一定还是二叉查找树。这也就意味着,任何的旋转和重新着色操作,都不会改变它仍然是一颗二叉查找树的事实。好吧?那接下来,我们就来想方设法的旋转以及重新着色,使这颗树重新成为红黑树!

第二步:将插入的节点着色为"红色"。

红黑树的第 5 条特征规定,任一节点到它子树的每个叶子节点的路径中都包含同样数量的黑节点。也就是说当我们往红黑树中插入一个黑色节点时,会违背这条特征。同时第 4 条特征规定红色节点的左右孩子一定都是黑色节点,当我们给一个红色节点下插入一个红色节点时,会违背这条特征。因此我们需要在插入黑色节点后进行结构调整,保证红黑树始终满足这 5 条特征。
调整思想
前面说了,插入一个节点后要担心违反特征 4 和 5,数学里最常用的一个解题技巧就是把多个未知数化解成一个未知数。我们这里采用同样的技巧,把插入的节点直接染成红色,这样就不会影响特征 5,只要专心调整满足特征 4 就好了。这样比同时满足 4、5 要简单一些。

第三步: 通过一系列的旋转或着色等操作,使之重新成为一颗红黑树。

那接下来,想办法使之"满足特性(4)",就可以将树重新构造成红黑树了。
首先来看下算法导论的伪代码描述:

RB-INSERT-FIXUP(T, z)
while color[p[z]] = RED                                              // 若“当前节点(z)的父节点是红色”,则进行以下处理。
     do if p[z] = left[p[p[z]]]                                      // 若“z的父节点”是“z的祖父节点的左孩子”,则进行以下处理。
           then y ← right[p[p[z]]]                                   // 将y设置为“z的叔叔节点(z的祖父节点的右孩子)”
                if color[y] = RED                                    // Case 1条件:叔叔是红色
                   then color[p[z]] ← BLACK               ▹ Case 1   //  (01) 将“父节点”设为黑色。
                   color[y] ← BLACK                       ▹ Case 1   //  (02) 将“叔叔节点”设为黑色。
                   color[p[p[z]]] ← RED                   ▹ Case 1   //  (03) 将“祖父节点”设为“红色”。
                   z ← p[p[z]]                            ▹ Case 1   //  (04) 将“祖父节点”设为“当前节点”(红色节点)
                else if z = right[p[z]]                              // Case 2条件:叔叔是黑色,且当前节点是右孩子
                        then z ← p[z]                     ▹ Case 2   //  (01) 将“父节点”作为“新的当前节点”。
                        LEFT-ROTATE(T, z)                 ▹ Case 2   //  (02) 以“新的当前节点”为支点进行左旋。
                        color[p[z]] ← BLACK               ▹ Case 3   // Case 3条件:叔叔是黑色,且当前节点是左孩子。(01) 将“父节点”设为“黑色”。
                        color[p[p[z]]] ← RED              ▹ Case 3   //  (02) 将“祖父节点”设为“红色”。
                        RIGHT-ROTATE(T, p[p[z]])          ▹ Case 3   //  (03) 以“祖父节点”为支点进行右旋。
      else (same as then clause with "right" and "left" exchanged)   // 若“z的父节点”是“z的祖父节点的右孩子”,将上面的操作中“right”和“left”交换位置,然后依次执行。
 color[root[T]] ← BLACK 

根据被插入节点的父节点的情况,可以将"当节点z被着色为红色节点,并插入二叉树"划分为三种情况来处理。

1.情况说明:被插入的节点是根节点。

处理方法:直接把此节点涂为黑色。

2.情况说明:被插入的节点的父节点是黑色。

处理方法:什么也不需要做。节点被插入后,仍然是红黑树。

3.情况说明:被插入的节点的父节点是红色。

处理方法:那么,该情况与红黑树的“特性(5)”相冲突。这种情况下,被插入节点是一定存在非空祖父节点的;进一步的讲,被插入节点也一定存在叔叔节点(即使叔叔节点为空,我们也视之为存在,空节点本身就是黑色节点)。
理解这点之后,我们依据"叔叔节点的情况",将这种情况进一步划分为3种情况(Case)。

1.(Case 1)叔叔是红色

在这里插入图片描述

步骤:

(01) 将“父节点”设为黑色
(02) 将“叔叔节点”设为黑色
(03) 将“祖父节点”设为“红色
(04) 将“祖父节点”设为“当前节点”(红色节点);即,之后继续对“当前节点”进行操作

步骤分析

“当前节点”和“父节点”都是红色,违背“特性(4)”。所以,将“父节点”设置“黑色”以解决这个问题。但是,将“父节点”由“红色”变成“黑色”之后,违背了“特性(5)”:因为,包含“父节点”的分支的黑色节点的总数增加了1。解决这个问题的办法是:将“祖父节点”由“黑色”变成红色,同时,将“叔叔节点”由“红色”变成“黑色”。
关于这里,说明几点:第一,为什么“祖父节点”之前是黑色?这个应该很容易想明白,因为在变换操作之前,该树是红黑树,“父节点”是红色,那么“祖父节点”一定是黑色。 第二,为什么将“祖父节点”由“黑色”变成红色,同时,将“叔叔节点”由“红色”变成“黑色”;能解决“包含‘父节点’的分支的黑色节点的总数增加了1”的问题。这个道理也很简单。“包含‘父节点’的分支的黑色节点的总数增加了1” 同时也意味着 “包含‘祖父节点’的分支的黑色节点的总数增加了1”,既然这样,我们通过将“祖父节点”由“黑色”变成“红色”以解决“包含‘祖父节点’的分支的黑色节点的总数增加了1”的问题; 但是,这样处理之后又会引起另一个问题“包含‘叔叔’节点的分支的黑色节点的总数减少了1”,现在我们已知“叔叔节点”是“红色”,将“叔叔节点”设为“黑色”就能解决这个问题。 所以,将“祖父节点”由“黑色”变成红色,同时,将“叔叔节点”由“红色”变成“黑色”;就解决了该问题。按照上面的步骤处理之后:当前节点、父节点、叔叔节点之间都不会违背红黑树特性,但祖父节点却不一定。若此时,祖父节点是根节点,直接将祖父节点设为“黑色”,那就完全解决这个问题了;若祖父节点不是根节点,那我们需要将“祖父节点”设为“新的当前节点”,接着对“新的当前节点”进行分析。

示意图

进行case 1变换,下面两张是后续
在这里插入图片描述

2.(Case 2)叔叔是黑色,且当前节点是右孩子

case1中我们不考虑当前节点是左孩子还是右孩子,因为情况都相同。但是当叔叔节点为黑色时,则要考虑节点是左孩子还是右孩子。

在这里插入图片描述

步骤

(01) 将“父节点”作为“新的当前节点”。
(02) 以“新的当前节点”为支点进行左旋。

步骤分析

首先,将“父节点”作为“新的当前节点”;
接着,以“新的当前节点”为支点进行左旋。
为了便于理解,我们先说明第(02)步,再说明第(01)步;
为什么要“以P为支点进行左旋”呢?根据已知条件可知:N是P的右孩子。而之前我们说过,我们处理红黑树的核心思想:将红色的节点移到根节点;然后,将根节点设为黑色。既然是“将红色的节点移到根节点”,那就是说要不断的将破坏红黑树特性的红色节点上移(即向根方向移动)。 而N又是一个右孩子,因此,我们可以通过“左旋”来将S上移! 按照上面的步骤(以P为支点进行左旋)处理之后:若N变成了根节点,那么直接将其设为“黑色”,就完全解决问题了;
若N不是根节点,那我们需要执行步骤(01),即“将P设为‘新的当前节点’”。
那为什么不继续以N为新的当前节点继续处理,而需要以P为新的当前节点来进行处理呢?这是因为“左旋”之后,P变成了N的“子节点”,即N变成了P的父节点;而我们处理问题的时候,需要从下至上(由叶到根)方向进行处理;也就是说,必须先解决“孩子”的问题,再解决“父亲”的问题;所以,我们执行步骤(01):将“父节点”作为“新的当前节点”。

示意图

本张图是对上一张图的case2变换
在这里插入图片描述

3.(Case 3)叔叔是黑色,且当前节点是左孩子

在这里插入图片描述

步骤

(01) 将“父节点”设为“黑色”
(02) 将“祖父节点”设为“红色”
(03) 以“祖父节点”为支点进行右旋

步骤分析

N和P都是红色,违背了红黑树的“特性(4)”,我们可以将F由“红色”变为“黑色”,就解决了“违背‘特性(4)’”的问题;但却引起了其它问题:违背特性(5),因为将P由红色改为黑色之后,所有经过P的分支的黑色节点的个数增加了1。那我们如何解决“所有经过F的分支的黑色节点的个数增加了1”的问题呢? 我们可以通过“将G由黑色变成红色”,同时“以G为支点进行右旋”来解决。

示意图

本张图是对上一张图的case3变换,生成完整红黑树。
在这里插入图片描述

总结:

红黑树的插入操作,当父节点为黑时,很好理解。主要是当父节点是红色时,需要区分成3种case。
case1时,发生一次着色操作,然后不断循环,每次完成case1操作后,把G赋给N,直到循环到根节点或者父节点为黑,跳出case1的情况。由于红黑树的高度至多为2log(n+1)。所以case1至多发生log(n+1)次。
case2时,发生一次旋转操作,跳到case3情形。
case3时,发生一次旋转操作,再一次着色操作。完成操作。
所以红黑树的旋转操作很少。局部至多2次(插入最多两次旋转,删除最多三次旋转)。大部分都是着色操作。
少量的旋转操作使得再添加节点时,大部分节点是可以被查询/修改的(因为旋转时为了数据安全,会锁住某些节点不能被修改,而着色操作并不影响这些)。在很多底层的实现上,有大量红黑树的实现。

推荐一个演示的网站

https://sandbox.runjs.cn/show/2nngvn8w

---------未完待续--------

发布了206 篇原创文章 · 获赞 68 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/u013728021/article/details/84303748