2-3树 与 红黑树

前言

红黑树直接看有点懵,涂上颜色,颜色转换,状态调整,说实话,一上来这么弄,我都不想看,有些东西你会知道是这么回事,但是你不清楚为什么这么做?为什么要涂上颜色?我自己也不太喜欢死记硬背,感觉很伤脑子,而且过一段时间就忘记了,这不是我的风格。

2-3 树

2-3 树 同2-3-4树 是差不多的概念,这也是一种平衡树,但有不一样的地方:

  • 一般平衡树一个节点只能存一个key,这种树的节点可以有两个key,有两个key的节点称为3节点,有一个key的节点称为2节点,这里不是说 2个节点,就叫2节点。下图左边是2节点,右边是3节点。
    2-3树 与 红黑树_第1张图片2-3树 与 红黑树_第2张图片
  • 2节点只有一个左子树、一个右子树,3节点有三个子树,分别是左子树、中间子树、一个右子树。
  • 平衡树都有 节点旋转操作,因为要保证左子树、右子树 高度不能相差1,2-3树也有,但是它的旋转会复杂一些

同样 2-3树的插入、删除也会更复杂,大致的思路是:

  • 如果是2节点,就变成3结点
  • 如果是3结点,3结点成4节点,4节点重新分配节点

删除的话就复杂很多,当然被篇文章就不仔细讨论这些了,他的优点包括平衡二叉树的所有优点:

  • 查找时间复杂度: l o g ( n ) log(n) log(n),最坏的情况下降低了树的高度,比二叉平衡树可以容纳更多的key

当然缺点也非常致命:

  • 查找和插入操作的实现需要大量的代码,而且它们所产生的额外开销可能会使算法比标准的二叉查找树更慢

所以2-3树在实际中很少使用。由于其需要大量的节点变换(从2-节点到3-节点到4-节点甚至到5-节点…),这些变换在实际代码中是很复杂的。所以现在几乎没有2-3树的具体实现。
但是由于2-3树的变化十分直观,因此前人在2-3树的理论基础上发明了红黑树。

2-3-4树与红黑树

2-3-4树 与 2-3树唯一的差别就在于多了一个4结点,容纳3个key,其他差别不太大,红黑树其实更类似2-3-4树:

  • 一个黑结点 如果有 两个红结点,就类似一个4结点
  • 一个黑结点如果有一个红结点,就类似一个3结点
    2-3树 和 2-3-4树 虽然很方便,但是上一节中也提到代码会非常复杂,于是改善了一下,提出了红黑树,红黑树用红链接表示2-3树中另类的3-结点,统一了树中的结点类型,使代码实现简单化,又不破坏其高效性,用颜色来做一个标识。

有一些博客里讲解了红黑树的几个特征性质,当然我们也提一下,虽然这些特征比较麻烦:

  • 1、根结点是黑色
  • 2、没有相邻的红结点,意思就是父结点是红结点,子结点就不能是红结点,红结点的子结点必须是黑结点
  • 3、从结点(包括根)到其任何后代NULL节点(叶子结点下方挂的两个空节点,并且认为他们是黑色的)的每条路径都具有相同数量的黑色节点

这是约束一颗红黑树的条件,也就是符合这些条件就是红黑树,其实还有一点:新插入的结点都是红色节点。红色节点插入树中之后,根据约束条件来更改颜色、调整树结构。

红黑树插入结点

插入相对还是比较简单的。同样结点数目构成的红黑树有很多种,这些不同的情况都可以通过染色、平衡来实现,那么问题来了?你依据什么样的准则来染色、调整?因为很多种染色、平衡操作都可以使得不平衡的红黑树变得平衡,那么你设计的调整准则是什么?

看到一篇文章讲解插入操作,讲解的还是比较好的,插入主要关注三种情况,这三种情况会因为一开始插入,或者是因为调整之后出现的情况,并且并不是调整一次就结束,会反复调整,我们先看下伪代码:


RB-INSERT-FIXUP(T,z)
 1  while color[p[z]] = RED  
 2      if p[z] = left[p[p[z]]]  
 3                y ← right[p[p[z]]]  
 4                if color[y] = RED  
 5                   color[p[z]] ← BLACK                    ▹ Case 1  
 6                   color[y] ← BLACK                       ▹ Case 1  
 7                   color[p[p[z]]] ← RED                   ▹ Case 1  
 8                   z ← p[p[z]]                            ▹ Case 1  
 9                 else  if z = right[p[z]]  
10                           z ← p[z]                       ▹ Case 2  
11                           LEFT-ROTATE(T, z)              ▹ Case 2  
12                 else   
13                     color[p[z]] ← BLACK                 ▹ Case 3  
14                     color[p[p[z]]] ← RED                ▹ Case 3  
15                     RIGHT-ROTATE(T, p[p[z]])            ▹ Case 3  
16      else (same as then clause   with "right" and "left" exchanged)  
17  color[root[T]]  ←  BLACK  

代码可能有点难懂,参数说明:z是插入节点的指针,T是树。总结一下代码流程:

while  ( z节点父节点 是红色 ):
         if  z节点的父节点  ==  z节点 祖父节点的左节点:
                y = z节点 祖父节点右节点
                if y 颜色 == red
                       第一种情况调整                                     ▹ 注意
                       z = z节点的祖父节点
                 else if z是右孩子    #  这也说明 y 颜色是黑色
                           z = z节点的父节点
                           第二种情况调整                                 ▹ 注意
                 else  # z是左孩子 ,y 颜色是黑色
                        第三种情况调整                                    ▹ 注意
         else # 隐藏含义: z节点的父节点  ==  z节点 祖父节点的右节点
              # 剩下的操作跟上面一样,只不过 左右互换一下

最后将 根节点 颜色 改为 黑色

注意几点:

  • 调整一共有三种情况
  • 这是一个while 循环,三种情况 反复调整,一直都 z节点 的父节点不是红色
  • 最后一步:将根节点置位黑色

那么调整的三种情况是怎么样的呢?图解如下:
2-3树 与 红黑树_第3张图片
讲解:

  • 红色节点是插入节点:这句话是说 红色圈起来的节点,节点的圆圈是红色笔画的,
  • 调整后, z z z被我用红色字体标出来了,这就是下一个循环中用的 z z z节点。
  • 第一种情况:变色
  • 第二种情况:左旋转
  • 第三种情况:父节点变黑,祖父节点变红,变色后 右旋转

看的时候注意几个问题:

  • 第一种情况下:为什么根节点是红色?这不就不符合红黑树定义了嘛?
    因为有最后的一个步骤将根节点置位黑色,所以其实也不算是一种失误

  • 第二种情况、第三种情况 下 原有结构也是有问题的?
    因为我们看到了的只是一部分结构,单纯的这种树结构是不会出现的,因为根本不符合红黑树的定义,建议看下教你透彻了解红黑树。里面有完整的树结构,第二种、第三种其中一部分结构,并且也有可能是调整之后出现的情况

  • 第二种情况、第三种情况的联系
    如果你仔细看,你会发现第二种情况跟第三种情况是连续的,第二种情况之后就开始执行第三种情况,

  • 第三种情况之后好像出现了不平衡的情况
    第三种情况是先变色,再旋转,但其实第三种情况的初始状态就不会存在,也就是不存在单纯的第三种情况的初始状态,它只是红黑树的一部分。完整的树下它是平衡的。

  • 出现连续的两个红色节点的情况,比如第三种情况,有点像2-3-4树
    红-红-黑 就像 一个 4结点,只不过在2-3-4树中是一个结点,在红黑树中要变色、调整,使得平衡。

  • 第三种情况之后,似乎就结束调整了
    没错,我们看图中第三种情况调整之后, z z z的节点位置,发现它不满足while循环的条件,也就会跳出循环,所以可以认为这是最后的一个步骤。

  • 为什么是这三种情况?
    很奇怪的一点就是为什么是这三种情况?这三种情况似乎是可以连续起来的,2、3、1这样的顺序,不过也只是猜测,各种情况分析之后其实也就只有这几种情况需要调整了,具体的还要继续研究一下。

插入的操作大致是这样,一些不需要调整的情况如下:

  • 插入的是根结点,因为原树是空树,此情况只会违反性质2,所以直接把此结点涂为黑色。
  • 如果插入的结点的父结点是黑色,由于此不会违反性质2和性质4,红黑树没有被破坏,所以此时也是什么也不做。

三种情况的另一种换言之,可以加强大家认识:

  • 情况1:如果当前结点的父结点是红色且祖父结点的另一个子结点(叔叔结点)是红色
  • 情况2:当前结点的父结点是红色,叔叔结点是黑色,当前结点是其父结点的右子
  • 情况3:当前结点的父结点是红色,叔叔结点是黑色,当前结点是其父结点的左子

左右反过来的情况是一样的,处理过程也是类似的,这里不赘述。

		TreeSet treeSet = new TreeSet();
        treeSet.add(4);
        treeSet.add(1);
        treeSet.add(2);
        treeSet.add(3);
        treeSet.add(5);
        treeSet.add(6);
        treeSet.add(7);

以上代码是我用来测试的代码,通过debug来查看了每个节点的颜色,得到了最终树的形状:

我比较奇怪,为啥 这个不是其他结构,因为4也可以作为根节点,得到一个更加平衡的二叉树,于是我猜想是不是跟节点插入顺序有关,于是我重新弄了一个代码:

       TreeSet treeSet = new TreeSet();
        treeSet.add(4);
        treeSet.add(1);
        treeSet.add(5);
        treeSet.add(2);
        treeSet.add(6);
        treeSet.add(3);
        treeSet.add(7);

我debug看了一下,还是结构发生了变化,

插入的顺序跟最终的结果关系很大,有可能本来就很平衡,不需要调整。

红黑树 删除结点

删除结点 一共有6种情况,真是复杂,慢慢来分析,这里会先讲解删除树节点的大致操作,然后来了解一下二叉树删除节点之后的调整过程,定一个基调,用来分析红黑树的删除:

  • 1、叶子节点删除,直接删除
  • 2、只有一个子节点的节点,删除该节点,子节点替代上去
  • 3、有两个子节点的节点删除,选择左子节点的最大节点来替代,默认这么做,也可以选择右边最小子节点。
    不过红黑树是先删除再调整树结构,调整树结构时会以删除节点的位置出发来做调整操作。
    所以如果是红黑树的话,应该怎么做呢?
  • 当前结点是红节点
  • 删除后,替代节点如果是黑色节点,且是根节点,则直接删除

这两种情况比较好弄,但是不太好理解,第一种比较好理解,第二种不太好理解,因为极有可能会造成左子树黑色结点少了一个,这样,根节点到叶子节点的路径上黑色节点数目不太一致,这一点先保留意见,之后来测试一下场景。
还有另外四种情况,代码先上:

 1 while x ≠ root[T] and color[x] = BLACK  
 2     do if x = left[p[x]]  
 3           then w ← right[p[x]]  
 4                if color[w] = RED  
 5                   then color[w] ← BLACK                        ▹  Case 1  
 6                        color[p[x]] ← RED                       ▹  Case 1  
 7                        LEFT-ROTATE(T, p[x])                    ▹  Case 1  
 8                        w ← right[p[x]]                         ▹  Case 1  
 9                if color[left[w]] = BLACK and color[right[w]] = BLACK  
10                   then color[w] ← RED                          ▹  Case 2  
11                        x ← p[x]                                ▹  Case 2  
12               else if color[right[w]] = BLACK  
13                           then color[left[w]] ← BLACK          ▹  Case 3  
14                                color[w] ← RED                  ▹  Case 3  
15                                RIGHT-ROTATE(T, w)              ▹  Case 3  
16                                w ← right[p[x]]                 ▹  Case 3  
17                         color[w] ← color[p[x]]                 ▹  Case 4  
18                         color[p[x]] ← BLACK                    ▹  Case 4  
19                         color[right[w]] ← BLACK                ▹  Case 4  
20                         LEFT-ROTATE(T, p[x])                   ▹  Case 4  
21                         x ← root[T]                            ▹  Case 4  
22        else (same as then clause with "right" and "left" exchanged)  
23 color[x] ← BLACK  

4种情况主要有:

  • Case 1:当前结点是黑色,且兄弟结点为红色(此时父结点和兄弟结点的子结点分为黑)
    下图中A节点就是当前节点,把父结点染成红色,把兄弟结点染成黑色,之后重新进入算法。此变换前、后 原红黑树性质5不变,而把问题转化为兄弟结点为黑色的情况。2-3树 与 红黑树_第4张图片
  • Case 2:当前结点是黑色,且兄弟是黑色且兄弟结点的两个子结点全为黑色
    这里要注意,调整后会将B节点作为当前节点,然后进行插入调整。所以下面的图可以看出其实是不平衡的,删除调整之后还需要插入调整
    2-3树 与 红黑树_第5张图片
  • Case 3:当前结点颜色是黑,兄弟结点是黑色,兄弟的左子是红色,右子是黑色
    把兄弟结点染红,兄弟左子结点染黑,之后再在兄弟结点为支点解右旋,之后重新进入算法,这主要是把当前情况转化为情况4,然后通过插入调整来使得满足性质:根节点到所有叶子节点路径中黑色节点数目一致。这里有个疑问?插入调整的当前节点是哪个?
    2-3树 与 红黑树_第6张图片
  • Case 4:当前结点颜色是黑,它的兄弟结点是黑色,但是兄弟结点的右子是红色,兄弟结点左子的颜色任意
    把兄弟结点染成当前结点父结点的颜色,把当前结点父结点染成黑色,兄弟结点右子染成黑色,之后以当前结点的父结点为支点进行左旋,然后进行插入调整
    2-3树 与 红黑树_第7张图片

其实通过上述的情况来看,这里的删除调整不是一下子就结束了,也要进行插入调整,两个调整之后还要进行多次调整,最终可以得到一颗符合条件的平衡树。
来看一个代码实现:

        TreeSet treeSet = new TreeSet();
        treeSet.add(4);
        treeSet.add(1);
        treeSet.add(5);
        treeSet.add(2);
        treeSet.add(6);
        treeSet.add(3);
        treeSet.add(7);


        treeSet.remove(2);

通过debug发现删除前后的差别是,删除前:

删除后:

红黑树 业务总结

目前树结构的有:

  • 二叉查找树(BST)
    英文Binary Sort Tree,查找、插入、删除的时间复杂度为 O ( l o g N ) O(logN) O(logN),时间复杂度就变味了 O ( N ) O(N) O(N)
  • AVL 树
    平衡二叉树全称平衡二叉搜索树,也叫AVL树。是一种自平衡的树。它要求左子树、右子树的高度相差不超过1,如果超过1了就会执行调整算法来调整数结构。

红黑树也是一种AVL树,红黑树是用非严格的平衡来换取增删节点时候旋转次数的降低,任何不平衡都会在三次旋转之内解决,而AVL是严格平衡树,因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多。

实际应用中,根据业务特点来选择:

  • 若搜索的次数远远大于插入和删除,那么选择AVL
  • 如果搜索,插入删除次数几乎差不多,应该选择RB

参考博客

漫画算法:什么是红黑树?(通俗易懂)
2-3树到红黑树
红黑树从头至尾插入和删除结点的全程演示图
RedBlack.pdf

你可能感兴趣的:(java基本知识,红黑树,redblackTree)