源码阅读(17):红黑树在Java中的实现和应用

(接上文《源码阅读(16):Java中主要的Map结构——HashMap容器(上)》)

3.2、HashMap准备知识:红黑树

红黑树又称自平衡二叉查找树,由于其稳定的查找特性,红黑树在Java中有广泛的应用——例如我们将要讲解的TreeMap容器和当前正在讲解的HashMap容器都有红黑树的具体应用。红黑树的操作原理相对于我们已经讲解过的堆树要复杂一些,但也并不是说无法理解,读者只需要跟随本节的介绍思路进行理解,并自行动手对其中提到的关键点进行验证,即可掌握红黑树的基本原理。

在网络上也有一些介绍红黑树的资料,但部分资料生涩难懂,甚至有一部分资料存在明显的错误,无法实现红黑树的构造还原。本文将通过图文结合的方式,介绍红黑树构造的本质。

3.2.1、二叉查找树(二叉搜索树)

要讲解红黑树,就必须首先介绍二叉查找树,因为红黑树是二叉查找树在极端情况下,稳定其时间复杂度的一种优化结构。所谓二叉查找树是指这样一颗树:

  1. 首先它是一颗二叉树
  2. 如果当前树的根结点存在左子树,则左子树上的任意结点的权值均小于当前根结点的权值
  3. 如果当前树的根节点存在右子树,则右子树上的任意结点的权值均大于当前根结点的权值
  4. 以此类推,以当前树上任何结点作为子树的根结点,则其左子树和右子树上的结点权值特点均满足以上第2点和第3点的描述
  5. 二叉查找树上的结点,没有权值相等的两个结点。

请注意二叉查找树的定义和我们之前文章已经介绍过的堆树定义的区别,简单来说前者的定义更严格。针对以上对二叉查找树的定义,以下的树结构都是二叉查找树:
在这里插入图片描述
那么以下定义不是二叉查找树:
在这里插入图片描述
上图的二叉树不是二叉查找树,是因为权值为32的结点,错误的存在于权值为35的根结点的右子树上。

3.2.2、二叉查找树的查找和添加

  • 二叉查找树的查找操作

如果需要在某个二叉查找树上查找权值为x的节点,那么从二叉查找树的根节点开始,递归进行以下是查找过程:

  1. 如果当前搜索的结点为空(即是说遇到空树或空子树),则表明要查找的权值为x的结点不在树上,查找失败;
  2. 如果以上步骤发现当前结点的权值等于要查找的权值,说明当前结点就是要查找的结点,查找成功;
  3. 若要查找的结点权值,小于当前结点的权值,说明要查找的结点如果存在,也只可能存在于以当前节点为根节点的左子树上,所以取得当前节点的左儿子结点,并递归进行第1步处理。
  4. 若要查找的结点权值,大于当前结点的权值,说明要查找的结点如果存在,也只可能存在于以当前节点为根节点的右子树上,所以去的当前节点的右儿子节点,并递归进行第1步处理。

那么这样的查找过程,可以用以下步骤示意图进行表达:
在这里插入图片描述

  • 二叉查找树的添加操作

当向一个二叉查找树添加一个权值为x的结点时,首先应该查找到可以添加新结点的位置,在查找过程中还需要检查二叉查找树中是否已有权值相同的结点,如果有则不应该添加新结点。

  1. 如果当前要添加的新节点的权值大于当前结点的权值,则向当前节点的右子树寻找添加位置。如果当前结点没有右儿子,则说明可以在这个位置添加新结点;如果当前结点存在右儿子,则以当前右儿子结点为准,开始递归进行本步骤1的处理。
  2. 如果当前要添加的新节点的权值小于当前节点的权值,则向向前节点的左子树寻找添加位置。如果当前节点没有左儿子,则说明可以在这个位置添加新的结点;如果当前结点存在左儿子,则以当前左儿子节点为准,开始递归进行步骤1的处理。

以上描述的添加操作步骤,可以用以下步骤示意图进行表达:
在这里插入图片描述

3.2.3、为什么需要红黑树

二叉查找树具有良好的查找性能,其查找操作的平均时间复杂度为log(n),但是也会出现一些极端情况,使得二叉查找树的查找操作性能明显下降,请考虑如下图所示的极端树结构(极端二叉查找树结构):
在这里插入图片描述
很明显如上图所示的树结构也满足二叉查找树的定义,但是上图的二叉查找树实际上就是一个结点权值有序的链表结构,其查找操作的时间复杂度为O(n)。出现这种情况的根本原因是因为二叉查找树并不是二叉平衡树,造成树上的所有节点明显“右倾”。

二叉平衡树要么是空树,要么树上任意非叶子结点的左右子树高度差不大于1

为了避免二叉查找树的极端情况出现,我们需要找到一种方式使得二叉查找树能够引入类似二叉平衡树的约束,且无论如何对二叉查找树进行写操作,二叉查找树都可以自行调整并稳定这个平衡结构——这就是我们将要引入的红黑树概念。

请注意虽然红黑树和二叉平衡树都属于二叉树,但是红黑树并不是二叉平衡树,两者最大的区别是红黑树放弃了绝对的子树平衡,转而追求一种大致平衡,这保证了在与平衡二叉树的时间复杂度相差不大的情况下,每次新增的结点最多只需要三次旋转就能满足结构要求

3.2.4、红黑树的基本定义

红黑树是在二叉查找树的基础上,增加了类似二叉平衡树的约束条件。本小节我们就进行详细介绍。为了简化红黑树的处理结构,我们需要引入外部结点的概念——注意外部结点只是一种记号,并不存储真实数据。

虚拟结点只是方便程序员在设计和编程时理解结点的旋转操作,在实际应用中并没有太大的意义。例如算法在具体编码时,程序员可以认定权值为null的结点是虚拟结点。红黑树除了带有外部结点,还需要满足下列条件的二叉查找树:

  1. 包括外部结点在内的所有结点都带有颜色,要么是黑色要么是红色
  2. 根节点为黑色
  3. 外部结点为黑色
  4. 每个红结点的儿子结点必须为黑色;但是黑结点的儿子既可以为红色也可以为黑色——这个规则可以理解为(红不相邻)
  5. 根结点到任意叶结点(或者外部结点)的路径上,标记为黑色的结点数量相同——这个规则可以理解为(黑平衡)
  6. 进行结点添加时,都默认进行添加的结点是红色结点。

以上红黑树规则中,第4点红不相邻和第5点黑平衡是最重要的规则。下图中,我们使用“圆形”表示树的真实节点,使用“正方形”表示树的外部结点,使用“深灰色”代表黑节点,使用“白色”代表红结点。那么按照以上对红黑树的规则定义,以下树结构都满足红黑树的定义:
在这里插入图片描述
以上结构满足红黑树的定义
在这里插入图片描述
以上结构也满足红黑树的定义

3.2.5、红黑树的操作规则

当红黑树收到某些写操作的影响(例如添加了一个结点),而变得不再满足红黑树的结构要求时,红黑树就会自行修正当前的树结构使自己重新满足红黑树结构要求。红黑树中有三种操作规则,它们使得红黑树随时满足结构要求——无论红黑树在写操作中出现任何状态,都可以通过这三种规则的联合使用让红黑树重新满足结构要求。

这三种规则是改色操作、结点左旋操作和结点右旋操作。改色操作自不必说,但只是修改颜色并不能调整可能已被打乱的红黑树的所有结构(例如红不相邻原则、黑平衡原则),所以还需要借助结点左旋操作和结点右旋操作进行调整,下文中我们重点介绍这三种操作。

将要介绍的三种操作都只是对红黑树的局部进行调整,调整过程所涉及的结点包括当前结点、它的父结点、它的祖父结点、它的叔结点(父结点的兄弟结点)。

3.2.3.1、红黑树结点的改色

当左右子树的平衡性将要改变时,就需要改色——这句话可能读者暂时不明白,没关系,这里先记下来继续往后面阅读即可。当红黑树需要改色时,其一定具备父结点和祖父结点-——不同时具备父结点和祖父结点的操作结点不允许改色,除非它是整个红黑树的根结点,而改色操作是将其父结点和祖父节点进行颜色更改

3.2.3.2、红黑树结点的左旋

以某个结点作为旋转结点P,将结点P向其左儿子(记为LP结点)的位置移动,结点P原来的右儿子结点(记为RP结点)将替代原来结点P的位置,成为结点P的父节点;RP结点的左儿子结点将成为P结点的右儿子。如下图所示:
在这里插入图片描述
红黑树的左旋操作也再次证明,无论是改色操作、左旋操作还是右旋操作,这些操作都将在红黑树的局部进行。所以无论红黑树的结点规模有多大,单一操作都可以将时间复杂度控制在一个常量范围内

左旋操作将有效减少以当前结点为根节点的左右子树的平衡度,并且不会变更当前子树的结点规模。 严格来说就是增加左子树的深度(左子树深度+1),减少右子树的深度(右子树深度-1)。

3.2.3.3、红黑树结点的右旋

以某个结点作为旋转结点P,将结点P向其右儿子(记为RP结点)的位置移动,结点P原来的左儿子结点(记为LP结点)将替代原来结点P的位置,成为结点P的父节点;LP结点的右儿子结点将成为P结点的左儿子。如下图所示:
在这里插入图片描述
从以上的介绍我们发现,基于结点P的左旋操作和基于P结点的右旋操作,两者是可逆的。右旋操作也将有效减少以当前结点为根节点的左右子树的平衡度,并且也不会变更当前子树的结点规模。 严格来说就是减少左子树的深度(左子树深度-1),增加右子树的深度(右子树深度+1)。

3.2.4、红黑树的结点添加操作

红黑树的添加操作实际上分为两个动作:第一个动作是将拥有权值的新节点正确添加到这个二叉查找树的正确位置,这个动作相对简单,如果读者不清楚可以参考上文中对二叉查找树添加操作的介绍。第二个动作是重新调整红黑树的平衡性,这是由于添加操作可能已经打破了红黑树的构造结构,而第二个动作可能遇到的各种场景才是本文介绍的重点。

当向红黑树添加一个新的结点时,结点都表示为红色,且添加操作存在几种情况需要分开考虑,首先我们先描述那些处理并不复杂的情况,作为红黑树结点添加时的基本约束

3.2.4.1、简单的处理场景
  • 情况1:添加前红黑树没有任何结点:

这时就是向红黑树添加第一个结点,这个结点将作为红黑树的根结点。由于我们默认当前添加的结点为红色,而红黑树结构要求中规定根结点必须为红色,所以这种情况下只需要更改新增结点的颜色即可。这是一个必要的过程,无论满足以下将提到的哪种添加处理场景,都需要在每次完成处理后,保证红黑树的根节点一直是黑色。

  • 情况2:新增结点的父结点是黑色:

这种情况下新增的红色结点不会影响红黑树的红不相邻特性以及黑稳定特性,所以不需要做任何额外处理。将新结点关联到正确的黑结点下即完成添加操作

  • 情况3:新增结点的父结点和叔结点都是红色:

这种情况下新增结点只需要进行改色操作,既是更改当前父结点和叔结点的颜色为红色,更改当前祖父结点的颜色为黑色,并以当前祖父结点作为下一次递归处理的依据结点继续处理。操作情况如下图所示:
在这里插入图片描述

3.2.4.2、新增结点的父结点和叔结点不同色

这种情况下,新增结点的父结点只可能为红色(因为如果为黑色,就只需要简单的添加新结点,不需要做其它任何处理),言下之意就是说叔结点的颜色为黑色。这种情况下,又要分为四种分支情况进行讨论:

  • 情况4:当前结点是父结点的右儿子,父结点是祖父结点的右儿子:
    在这里插入图片描述

这种情况下将以当前结点的祖父结点为支点,进行左旋操作。由于左旋后以祖父结点为根结点的左右子树的平衡性将被修改,所以需要在操作前先进行变色操作——当前结点的父结点变为黑色,祖父结点变为红色。操作过程如下图所示:
在这里插入图片描述
左旋操作后,将以当前结点的父结点(P结点)为新的操作结点,继续递归进行调整操作

  • 情况5:当前结点是父结点的左儿子,父结点是祖父结点的左儿子:
    在这里插入图片描述

这种情况下将以当前结点的祖父结点为支点,进行右旋操作。由于右旋后以祖父结点为根结点的左右子树的平衡性也将会改变,所以需要在操作前先进行变色操作——当前结点的父结点变为黑色,祖父结点变为红色。操作过程如下图所示:
在这里插入图片描述
右旋操作后,将以当前结点的父结点(P节点)为新的操作结点,继续递归进行调整操作

  • 情况6:当前节点是父结点的右儿子,父结点是祖父节点的左儿子:
    在这里插入图片描述

这种情况下,需要首先通过旋转(左旋)将4个结点的组织结构转换为情况5的结构,由于这样的旋转不会改变以结点G作为根结点的左右子树的平衡性,所以这次旋转操作前不需要进行改色操作。旋转后将以当前节点的父结点(P节点)为新的操作结点,继续进行递归操作。

在完成以上左旋操作后,当前操作结点相关的4个结点的组织结构,就变成了情况5所示的结构了,然后在按照情况5的处理要求接着进行递归处理,如下图所示:
在这里插入图片描述

  • 情况7:当前节点是父结点的左儿子,父结点是祖父节点的右儿子:
    在这里插入图片描述

这种情况下,需要首先通过旋转(右旋)将4个结点的组织结构转换为情况4的结构,由于这样的旋转不会改变以结点G作为根结点的左右子树的平衡性,所以这次旋转操作前不需要进行改色操作。旋转后将以当前节点的父结点(P节点)为新的操作结点,继续进行递归操作。

在完成以上右旋操作后,当前操作结点相关的4个结点的组织结构,就变成了情况4所示的结构了,然后在按照情况4的处理要求接着进行递归处理,如下图所示:
在这里插入图片描述

3.2.5、添加操作实例

下面我们按照以上介绍的添加过程中遇到的各种情况的处理方式,来构造一个红黑树。以下示例中,我们将会把以下多个带有不同权值的结点,通过添加结点的方式构造一个红黑树:
在这里插入图片描述

========
(接下文《源码阅读(18):Java中主要的Map结构——HashMap容器(中)》)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

说好不能打脸

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值