图解红黑树与Java实现

文章目录

      • 红黑树引出
        • 1、二叉搜索树BST
        • 2、2-3-4树
        • 3、红黑树RBT
        • 5、红黑树的一些问题
      • 红黑树查找
        • 1、效率分析
      • 红黑树插入
        • 1、红黑树为空树
        • 2、插入点的key已存在
        • 3、插入结点的父结点是黑结点
        • 4、插入结点的父结点是红结点
          • 4.1:叔叔结点存在并且为红结点(叔父双红)
          • 4.2:叔叔结点不存在或为黑结点,且插入结点的父亲结点是祖父结点的左子结点
            • 4.2.1:新插入结点为其父结点的左子结点(LL双红)
            • 4.2.2:新插入结点为其父结点的右子结点(LR双红)
          • 4.3:叔叔结点不存在或为黑结点,并且插入结点的父亲结点是祖父结点的右子结点
            • 4.3.1:新插入结点为其父结点的右子结点(RR双红)
            • 4.3.2:新插入结点为其父结点的左子结点(RL双红)
        • 5、综合案例
        • 6、失衡解决原因
          • 6.1、叔父双红
          • 6.2、LL、RR、LR、RL
        • 7、效率分析
      • 红黑树删除
        • 1、前置知识
        • 2、删除单分支结点
        • 3、删除双分支结点
        • 4、删除单个红结点
        • 5、删除单个黑结点
          • 5.1、黑兄弟,右红侄
          • 5.2、黑兄弟,左红侄
          • 5.3、黑兄弟,双黑侄
          • 5.4、红兄弟
        • 6、失衡解决原因
          • 6.1、黑兄弟,红右侄
          • 6.2、黑兄弟,红左侄
          • 6.3、黑兄弟,双黑侄
          • 6.4、红兄弟
        • 7、效率分析
      • 红黑树与AVL树的比较
      • 红黑树Java实现

红黑树引出


1、二叉搜索树BST

如果我们给二叉树加一个额外的条件,就可以得到一种被称作二叉搜索树(Binary Search Tree)的特殊二叉树

二叉搜索树要求:

  1. 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值
  2. 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值
  3. 它的左、右子树也分别为二叉排序树

图解红黑树与Java实现_第1张图片

虽然二叉搜索树在最优情况下(完全二叉树)平均查找比较次数为log2(n)(二分查找)

但是在极端情况下二叉树退化为线性链表查找时间复杂度就退化为O(log2n),这一切都是因为不够平衡导致的,所以需要平衡二叉查找树

2、2-3-4树

2-3-4树即4阶B树,是一种完美平衡的树形结构,保证其查找的效率为O(log2n)级别

根据4阶B树的定义,可以知晓每个树结点最少有一个关键字,最多有三个关键字,所以可以把结点根据关键字个数分为:

  • 2结点:含有一个关键字和两条链接
  • 3结点:含有两个关键字和三条链接
  • 4结点:含有三个关键字和四条链接

图解红黑树与Java实现_第2张图片

2-3-4树虽然从理论上来说能解决二叉查找树的缺点,但是实现非常麻烦,要同时维护三种数据结构 2-结点 3-结点 4-结点。在三种数据结构之间转换复制信息不仅仅代码复杂而且需要额外的时间和空间开销,最后的结果也没理论那么好

虽然2-3-4树实现比较复杂,但我们仍然想通过二叉树结构描述2-3-4树,所以提出了红黑树这种数据结构,红黑树的背后逻辑就是2-3-4树的逻辑,是2-3-4树的二叉表示

那怎么用二叉树表示2-3-4树呢?

2结点用二叉树结构表示当然没问题,3结点、4结点怎么办?

多个关键字在二叉树中只能形成多个结点,因为二叉树中一个结点最多有两个孩子,加起来刚好三个结点,所以我们可以用红边(红链接)来描述3结点与4结点,即红边连接的结点中的关键字,在2-3-4树中是一个结点内的,这样就解决了34结点的表示问题

但是实际怎么存储呢?我们将红边指向的结点作为红结点,否则为黑结点,存在结点属性中

图解红黑树与Java实现_第3张图片

好了既然有了上面的结点对应关系我们来看一个实际等价转换例子

图解红黑树与Java实现_第4张图片

经这样二叉表示的2-3-4树性质很明显,简单推敲一下就是下方红黑树的五大性质

3、红黑树RBT

红黑树是每个结点都带有颜色属性的二叉查找树,颜色或红色或黑色,在二叉查找树强制一般要求以外,增加了如下的额外要求:

  • 性质1:每个结点或者是黑色,或者是红色

  • 性质2:根结点是黑色

  • 性质3:所有叶子结点(NIL)是黑色

  • 性质4:红色结点子结点一定是黑色,不可以同时存在两个红色结点相连

  • 性质5:任意结点到其每个叶子结点都包含相同数量的黑结点
    图解红黑树与Java实现_第5张图片

5、红黑树的一些问题

红黑树是AVL树吗?

红黑树并不是一颗平衡二叉查找树,从图上可以看到,根结点的左子树显然比右子树高

但左右子树黑结点的层数是相等的,即任意一个结点到到其每个叶子结点的路径都包含数量相同的黑结点(性质5)

所以我们叫红黑树这种平衡为黑色完美平衡

只要一棵二叉查找树树满足红黑树的五条性质,这棵树就是趋近于平衡状态

对红黑树是 “近似平衡” 的理解

平衡二叉查找树的初衷是为了解决二叉查找树因为动态更新导致的性能退化问题。所以"平衡"的意思可以等价为性能不退化,”近似平衡"就等价为性能不会退化的太严重

一棵极其平衡的二叉树(满二叉树或完全叉树)的高度大约是 log2n,所以如果要证明红黑树是近似平衡的,只需要分析,红黑树的高度是否比较稳定地趋近 log2n 就好了

红黑树能自平衡,它靠的是什么?

靠的是三种操作:左旋、右旋和变色

1.变色:结点的颜色由红变黑或由黑变红

2.左旋:以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变

图解红黑树与Java实现_第6张图片

3.右旋:以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变

图解红黑树与Java实现_第7张图片

红黑树查找


1、效率分析

树的查找都取决于其高度差,红黑树相比于平衡二叉树是不严格的,因为它不是严格控制左、右子树高度差小于等于1,但红黑树高度依然是平均log2n,且最坏情况高度不会超过2log2n,即当一棵子树全为黑结点而另一颗红黑交替时。所以其查找时间复杂度:

T ( n ) = O ( l o g 2 n ) T(n)=O(log_2n) T(n)=O(log2n)

红黑树插入


插入操作包括两部分操作

  1. 查找插入的位置
  2. 插入后自平衡

注意:插入结点,必须为红色,理由很简单,红色在父结点(如果存在)为黑色结点时,红黑树的黑色平衡没被破坏,不需要做自平衡操作。但如果插入结点是黑色,那么插入位置所在的子树黑色结点总是多1,每次都必须做自平衡

插入的所有情况分析

图解红黑树与Java实现_第8张图片

所有只有在插入的结点的父结点为红结点的情况下才需要平衡调整

1、红黑树为空树

最简单的一种情景,直接把插入结点作为根结点就行

注意:根据红黑树性质2根结点是黑色。还需要把插入结点设为黑色

2、插入点的key已存在

新结点的value值替换原结点的value值

图解红黑树与Java实现_第9张图片

3、插入结点的父结点是黑结点

直接插入,不会影响自平衡

图解红黑树与Java实现_第10张图片

4、插入结点的父结点是红结点

再次回想下红黑树的性质2:根结点是黑色

如果插入结点的父结点为红结点,那么该父结点不可能为根结点,所以插入结点总是存在祖父结点且为黑色

这一点很关键,因为后续的旋转操作肯定需要祖父结点的参与

4.1:叔叔结点存在并且为红结点(叔父双红)

此时该插入子树的红黑层数的情况是:黑红红。显然最简单的处理方式是把其改为:红黑红

处理:

  1. 叔父染黑
  2. 爷爷染红
  3. 将爷爷结点设置为当前结点,进行后续处理

图解红黑树与Java实现_第11张图片

可以看到,我们把PP结点设为红色了,如果PP的父结点是黑色,那么无需再做任何处理;

但如果PP的父结点是红色,则违反红黑树性质了。所以需要将PP设置为当前结点,继续做插入自平衡处理,直到平衡为止

4.2:叔叔结点不存在或为黑结点,且插入结点的父亲结点是祖父结点的左子结点

注:单纯从插入前来看,叔叔结点非红即空(NIL结点),否则的话破坏了红黑树性质5,此路径会比其它路径多一个黑色结点。

4.2.1:新插入结点为其父结点的左子结点(LL双红)

图解红黑树与Java实现_第12张图片

处理:

  1. 父染黑,爷爷染红
  2. 爷爷结点右旋

图解红黑树与Java实现_第13张图片

4.2.2:新插入结点为其父结点的右子结点(LR双红)

图解红黑树与Java实现_第14张图片

处理:

  1. 父左旋
  2. 将父结点设为当前结点,得到LL双红情况
  3. 按照LL红色情况处理(1.变颜色2.右旋PP)

图解红黑树与Java实现_第15张图片

4.3:叔叔结点不存在或为黑结点,并且插入结点的父亲结点是祖父结点的右子结点

该情况是对应情景4.2的镜像情况

4.3.1:新插入结点为其父结点的右子结点(RR双红)

图解红黑树与Java实现_第16张图片

处理:

  1. 父染黑,爷爷染红
  2. 爷爷左旋

图解红黑树与Java实现_第17张图片

4.3.2:新插入结点为其父结点的左子结点(RL双红)

图解红黑树与Java实现_第18张图片

处理:

  1. 父右旋
  2. 将父节点设置为当前结点,得到RR双红情况
  3. 按照RR红色情况处理(1.变颜色2.左旋PP)

图解红黑树与Java实现_第19张图片

注:情景4.2与4.3的叔叔为黑结点的情况在第一次调整是不会出现的

5、综合案例

图解红黑树与Java实现_第20张图片

图解红黑树与Java实现_第21张图片

6、失衡解决原因

6.1、叔父双红

的情况对应的是向2-3-4树中的4结点再次插入关键字导致的结点分裂的情况

按照4阶B树结点分裂情况来看,会从中间位置 ⌈ m / 2 ⌉ \lceil m/2 \rceil m/2处将其关键字分为两部分,左部分包含的关键字放在原结点中,右部分包含关键字放在新结点中,中间关键字插入其父节点:即一个4结点会分裂为一个3结点和一个2结点

图解红黑树与Java实现_第22张图片

6.2、LL、RR、LR、RL

这四种插入后失衡情况其实都是一个原理,其对应的是2-3-4树中的3结点变为4结点,只不过位置可能插的不到位,没有插入到3结点空的那一颗子树上去,所以需要变色旋转内部调整平衡即可

7、效率分析

插入操作包括两部分操作

  1. 查找插入的位置
  2. 插入后自平衡

查找操作的时间复杂度为O(log2n),而由于自平衡操作最坏情况下也只是需要两次旋转操作即时间复杂度为O(1),所以效率:
T ( n ) = O ( l o g 2 n ) T(n)=O(log_2n) T(n)=O(log2n)

红黑树删除


1、前置知识

我们先来简单回顾下二叉搜索树的删除操作。假如我们有一颗二叉搜索树如下:

图解红黑树与Java实现_第23张图片

二叉搜索树中删除结点有3种场景:

  1. 删除结点无子结点,可以直接删除。如删除6、22、27
  2. 删除结点只有一个子结点,则将父子内容替换然后转为删除子结点即可。如删除8
  3. 删除结点有两个子结点,可以用前驱(后继)结点替换删除结点,然后转而删除其前驱(后继)结点即可。如删除17,25

结论:所有删除最终都会退化为删除单个结点

红黑树删除的所有情况如下:

图解红黑树与Java实现_第24张图片

2、删除单分支结点

将key互换然后再删除红子结点即可

图解红黑树与Java实现_第25张图片

3、删除双分支结点

转换为删除单个结点

图解红黑树与Java实现_第26张图片

转换为删除带有一个子树的结点 => 转换为删除单个结点

图解红黑树与Java实现_第27张图片

注:以上为了好画图,转而删除的单个结点都是红结点

4、删除单个红结点

直接做掉就vans了,也并不会影响平衡

图解红黑树与Java实现_第28张图片

5、删除单个黑结点

注:以下删除结点都是以被删除结点为左孩子的情况,被删除结点为右孩子的情况镜像处理即可

5.1、黑兄弟,右红侄

此时无论父亲是红色还是黑色都是一样的

解决:左旋父,爷染父色,父叔黑

图解红黑树与Java实现_第29张图片

5.2、黑兄弟,左红侄

此时无论父亲是红色还是黑色都是一样的

解决:右旋兄,交换兄弟与其右子颜色,变成情况5.1

图解红黑树与Java实现_第30张图片

5.3、黑兄弟,双黑侄

此时,两个侄子结点一定不存在(Nil)(因为存在就属于前两种情况)

解决:兄弟红,向上找,遇根或红结点,染黑即解决

图解红黑树与Java实现_第31张图片

如果父结点是非根黑结点则递归处理父节点

  • 首先将兄弟结点B染成红色,此时视角在D、B层
  • 然后向上找,视角上移到P层,P为黑色,需要以P为新的处理节点按照对应情况递归处理

图解红黑树与Java实现_第32张图片

5.4、红兄弟

此时,父结点一定是黑结点

解决:左旋父,父祖换色,变成前三种情况

图解红黑树与Java实现_第33张图片

此时,以目标结点D为参照,根据黑兄弟BL以及侄子结点情况可能变成前三种情况

这其实是黑兄弟双黑侄的镜像情况,受的是3结点父亲的二叉表示不唯一的影响

至此,删除结点已分析完毕。待删除结点是父结点的右孩子的情况为以上的镜像情况

6、失衡解决原因

在解释删除失衡能解决的原理之前我们很有必要来复习一下B树的删除:

众所周知B树中删除就是找兄弟结点借关键字:

  1. 兄弟够借。若被删除关键字所在结点删除前的关键字个数低于下限,且与此结点左/右兄弟结点的关键字个数还很宽裕,则需要调整该结点、左/右兄弟结点及其双亲结点(父子换位法)
  2. 兄弟不够借。若被删除关键字所在结点删除前的关键字个数低于下限,且此时与该结点相邻的左、右兄弟结点的关键字个数均为[m/2]-1(4阶B树中即1),则将关键字删除后与 左/右 兄弟结点及双亲结点中的关键字进行合并,如果该合并造成了父节点关键字不够了还需要递归处理父结点

6.1、黑兄弟,红右侄

此情况的失衡处理过程相当于2-3-4中的删除2结点,当然存在红右侄说明兄弟节点肯定不是2结点(暗示关键字够够的能借)

图解红黑树与Java实现_第34张图片

6.2、黑兄弟,红左侄

此情况其实是和黑兄弟,红右侄是一样的,只不过我们的2结点兄弟结点的二叉表示可能不利于我们调整

此情况的失衡处理过程相当于2-3-4中的删除2结点,当然存在红右侄说明兄弟节点肯定不是2结点(暗示关键字够够的能借)

图解红黑树与Java实现_第35张图片

6.3、黑兄弟,双黑侄

这种情况说明兄弟也是2结点不够借了,只能将关键字删除后与兄弟结点及双亲结点中的关键字进行合并

当向上找遇到根结点时将根节点与不够借的兄弟结点合并为一个3节点即可,当遇到红结点说明父节点非2结点(合并完之后父亲关键字 数量还够),则将父节点的红子接点与兄弟结点合并为一个2结点即可

图解红黑树与Java实现_第36张图片

当遇到非根黑结点说明父节点是一个2结点,父节点与兄弟结点合并为一个三结点之后关键字数量的话,则又要与其自己的兄弟节点进行调整或者合并操作,直至平衡为止。下图是一个例子:

图解红黑树与Java实现_第37张图片

6.4、红兄弟

这其实是黑兄弟双黑侄的镜像情况,受的是3结点父亲的二叉表示不唯一的影响

调整相当于在2-3-4中没变但是3结点父亲的二叉表示变了

图解红黑树与Java实现_第38张图片

注:在平衡性调整中首次就是红兄弟这种情况调整后只能转换到黑兄弟双黑侄,其它情况会在黑兄弟双黑侄视角上移中体现

7、效率分析

删除操作包括两部分

  1. 查找待删结点位置
  2. 删除后自平衡

查找操作的时间复杂度为O(log2n),且当失衡时RBTree最多只需3次旋转就可调整平衡,只需要O(1)的复杂度。所以效率:
T ( n ) = O ( l o g 2 n ) T(n)=O(log_2n) T(n)=O(log2n)

红黑树与AVL树的比较


查找 插入 删除
AVL树 O(log2n) O(log2n) O(log2n)
红黑树 O(log2n) O(log2n) O(log2n)
  1. AVL树的从查找时间复杂度虽然略优于红黑树,但是对于现在的计算机,cpu太快,可以忽略性能差异

  2. 红黑树整体统计性能略优于AVL树(红黑树旋转情况少于AVL树)

详细分析如下

查找:红黑树属于不严格的平衡二叉树。因为它不是严格控制左、右子树高度或结点数之差小于等于1,但红黑树高度依然是平均log(n),且最坏情况高度不会超过2log2n,所以查找效率依然是log(n)级别

插入:插入node引起了树的失衡,AVL和RBTree都是最多只需要2次旋转操作,即两者都是O(1)

删除:在删除node引起树的不平衡时,最坏情况下,AVL需要维护从被删node到root路径上所有node的平衡性,因此需要旋转的量级O(log2n),而RBTree最多只需3次旋转,只需要O(1)的复杂度

总结:AVL的结构相较RBTree来说更为平衡,在插入和删除node更容易引起Tree的不平衡,因此在大量数据需要插入或者删除时,AVL需要rebalance的频率会更高。因此,RBTree在需要大量插入和删除node的场景下效率更高。自然,由于AVL高度平衡,因此AVL的搜索效率更高

若想了解红黑树时间复杂度证明可以看一下这篇文章:RBTree时间复杂度分析

PS:其实文章还少些东西没怎么写完,过两天修补下

红黑树Java实现

传送门:Gayhub地址

你可能感兴趣的:(数据结构,java,java,b树,数据结构,二叉树,红黑树)