红黑树这个数据结构,让你又爱又恨?看了这篇,妥妥的征服它

红黑树是一个比较复杂的数据结构,相信不少人也只知其名而不知其意,由于理解它的原理确实须要花费必定的功夫。之因此写这篇文章,也是为了更好的理解 Java 中 TreeMap 的源码。html

写以前,搜了下网上的文章,说实话,看完有点懵,大部分一上来就给你它的五大性质,而后就是一顿插入、删除、旋转操做,就完事了,理解起来至关吃力。node

本文将结合 2-3-4 树,按部就班地介绍红黑树的由来原理,相信看完以后,你对它会有更清晰的认识。此外,这里描述的是普通红黑树,而不是它的变体左倾红黑树(LLRB),这一点须要注意。算法

红黑树的引入

二叉查找树

红黑树的由来要从二叉查找树提及。二叉查找树是一颗二叉树,它每一个结点的值都大于其左子树的任意结点而小于右子树的任意结点,它结合了链表插入的灵活性有序数组查找的高效性(二分查找)编程

bst.png

对于使用二叉查找树的算法,它的运行时间取决于树的形状,而树的形状又取决于结点插入的前后顺序。如上图所示,最好状况下N 个结点的树是彻底平衡的,每条空连接到根结点的距离都为 ~lgN;而在最坏的状况下,搜索路径上可能有 N 个结点,退化成了链表数组

因此,为了保证运行时间始终在对数级别,在动态构建二叉查找树时,但愿保持其平衡性,也就是下降树的高度,使其尽量为 ~lgN,这样就能保证全部的查找都能在 ~lgN 次比较内结束,就像二分查找那样,这样的树被称为平衡二叉查找树数据结构

AVL 树

第一个自平衡二叉查找树就是AVL 树,它规定,每一个结点的左右子树的高度之差不超过 1。在插入或删除结点,打破平衡后,就会经过一次或屡次树旋转来从新平衡。编程语言

AVL 树是严格平衡的,适用于查找密集型应用程序,由于在频繁插入或删除结点的场景下,它花费在树旋转的代价过高。源码分析

红黑树就是一种折中方案,它不追求完美平衡,只求部分达到平衡,从而下降在调整时树旋转次数。网站

2-3-4 树

说到红黑树,就不得不提 2-3-4 树,由于,红黑树能够说就是它的一种特殊实现,对它有所了解,很是有助于理解红黑树。3d

保持平衡,无非是为了下降树的高度,若是把二叉查找树通常化,容许一个结点保存多个值,变成多叉树,也可认为是下降了高度。

确切地说,标准二叉查找树中的结点称为2-结点(一个值两个子结点),如今引入3-结点(两个值三个子结点)和4-结点(三个值四个子结点),这样就能获得一颗 2-3-4 树(也称为 2-4 树)。

node.png

2-3-4 树是 4 阶 B 树,全部数据按排序顺序保存,全部叶子结点都在相同的深度。对于大多数编程语言,直接实现 2-3-4 树比较困难,而红黑树的实现相对要简单容易,这也是红黑树应用普遍的一部分缘由。

2-3-4-rb.png

红黑树是二叉树,全部的结点都是2-结点,因此为了可以表示3-结点和4-结点,为结点引入了颜色属性

  • 黑色,表示普通结点
  • 红色,表示可与父结点合并看做多值结点

如上图所示,若是把红黑树的红色结点和其父结点放平,它的结构就和左边的 2-3-4 树同样。

红黑树

如今,来看下红黑树的性质:

  1. 每一个结点都是红色或黑色的
  2. 根结点是黑色的**(是红色最终也会转黑色)**
  3. 全部叶子结点都是黑色的,这里的叶子结点指的是空结点,经常使用 NIL 表示
  4. 若是结点为红色,则其子结点均为黑色**(红色表示可与父结点合并,子结点凑什么热闹)**
  5. 从给定结点到其任何后代 NIL 结点的每条路径都包含相同数量的黑色节点**(转成 2-4 树,全部叶子节点均在最底层)**

这些性质没必要去背,就算记住后也绝对会忘,应该结合着 2-3-4 树理解性记忆。

另外,红黑树中的旋转颜色翻转,就至关于 2-3-4 树中的拆分合并,而且 2-3-4 树结点的拆分和合并,理解起来至关简单。对比分析和理解红黑树的操做,绝对让你眼前一亮。

树旋转

在分析插入和删除以前,先了解下什么是树旋转。树旋转是二叉树中调整子树的一种操做,经常使用于调整树的局部平衡性,它包含两种方式,左旋转右旋转

rotate.png

其实旋转操做很容易理解:左旋转就是将用两个结点中的较小者做为根结点变为将较大者做为根结点,右旋转恰好于此相反,如上图所示:

  • 右旋转,就是将较小者 L 做为根结点,而后调整 L 和 P 的子树
  • 左旋转,就是将较大者 P 做为根结点,而后调整 P 和 L 的子树

红黑树的旋转其实就是为了确保和其结构相同的 2-3-4 树的一一对应关系,同时保证红黑树的有序性和平衡性。

插入

接下来,就结合 2-4 树分析结点的插入,首先 2-4 树的插入逻辑是这样的:

  • 若是是 2-结点,直接插入变成 3-结点
  • 若是是 3-结点,直接插入变成 4-结点
  • 若是是 4-结点,首先进行分裂,变成 2-结点,再插入

2-4 树插入的都是叶子结点红黑树插入的结点都是红色的,由于在 2-4 树中,待插入结点都认为能够插入到一个多值结点中。

这里假设待插入结点为 NPN 的父结点,GN 的祖父结点,UN 的叔叔结点(即父结点的兄弟结点),那么红黑树有如下几种插入状况:

  1. N 是根结点,即红黑树的第一个结点
  2. N 的父结点(P)为黑色
  3. P红色的(不是根结点),它的兄弟结点 U 也是红色
  4. P红色,而 U黑色

状况 1,2,3

这三种状况比较简单,就放在一块儿说明了,它们都不涉及旋转只涉及颜色翻转,换句话说就是只是结点合并无拆分

状况 1 和 2,不影响红黑树的性质,不会打破平衡,直接插入便可:

  • 对于 2-4 树来讲,空树插入就是一个 2-结点->3-结点->4-结点转换的过程
  • 红黑树就是创建一个根结点为黑色的标准 2-结点

状况 3P红色(不是根结点),U 也是红色,两个树插入状况以下:

  • 在 2-4 树中,就意味着这是个 4-结点,它首先拆分红 2-结点,而后再进行插入
  • 对于红黑树,它至关于已经拆分,直接变色即:PU 变成黑色,G 变成红色,若 G根结点,直接变黑,不然递归向上检查是否形成不平衡

[7, 5, 9, 3] 输入序列为例,两个树构建过程以下:

case-1-2-3.png

状况 4

状况 4P红色,而 U黑色,此时,在 2-4 树看来这个结点就是一个 3-结点,直接插入变成 4-结点;而对于红黑树,它为了和这个 2-4 树结构保持一致,会根据不一样的状况作旋转,分别有如下四种可能:

  • PG左孩子,若 NP左孩子,那么将祖父结点 G 右旋转 便可
  • PG左孩子,若 NP右孩子,那么 P 先左旋转,而后再将祖父结点 G 右旋转

相反的:

  • PG右孩子,若 NP右孩子,那么将祖父结点 G 左旋转 便可
  • PG右孩子,若 NP左孩子,那么 P 先右旋转,而后再将祖父结点 G 左旋转

[7, 5, 9, 3, 4] 输入序列为例,也就是在上图的基础上,插入 4,演示 P 为左,N 为右,树的旋转过程:

case-4.png

其余状况,左右互换便可,可自行尝试分析。这里给出最开始提供的红黑树2-3-4 树它们的动态构建过程,输入序列为 [7, 5, 9, 3, 4, 8, 10, 11, 12, 13, 14, 15],首先是 2-3-4 树的构建:

2-3-4-btree.gif

红黑树构建时会有一次根结点调整,可注意一下:

rbtree-20.gif

删除

二叉查找树的结点无非是有两个子结点有一个子结点叶子结点三种,其中有两个子结点M 结点的删除逻辑是:

  1. 首先寻找 M 结点左子树最大右子树最小的结点 X
  2. 而后把 X 结点的值复制到 M 结点
  3. 最后删除 X 结点,而这个结点要么是叶子结点,要么就只有一个孩子

因此,删除任一结点的问题就简化成了:删除一个最多只有一个孩子的结点的状况,而且全部的删除操做都在叶子结点完成,只不过删除的结点再也不是一开始想删除的结点,但结点的值最终是删除了,而树结构的变化与简化问题相比,并不重要。

在分析红黑树的删除以前,简单来看下 2-3-4 树的删除状况。

2-3-4 树结点删除

它相似二叉查找树的删除,实际的删除操做也是在叶子结点完成,只不过在删除的过程当中涉及到结点的合并,主要有 3 种不一样的状况:

  1. 若是元素 K 是内部结点,而且在一个至少有 2 个 key 的多值叶子结点内部,则只需从结点中删除 K
  2. 若是元素 K 是内部结点,且有左孩子和右孩子,那么: 2.1 若是左孩子至少有 2 个 key,那么找一个最大值替换 K,而后删除这个最大值 2.2 若是右孩子至少有 2 个 key,那么找一个最小值替换 K,而后删除这个最小值 2.3 若是两个孩子都只有 1 个 key,那么将 K 下沉,与其子女合并,造成一个至少有 2 个 key 的结点,最后再删除 K
  3. 若是元素 K 不是内部结点,所在结点只有它 1 个 key,那么根据如下状况,最终会转成状况1状况2: 3.1 若是它的兄弟结点至少有 2 个 key,那么选择一个推到父节点中,再把旧的父节点下沉和 K 合并 3.2 若是它的兄弟结点也只有 1 个 key,那么将父结点下沉,与其子女合并,再删除 K因此,此时须要父结点至少有 2 个 key,若是没有那么在父结点上递归按状况 3 处理

上面这些状况,有一个前提就是,在遍历查找待删除结点时,必须保证路过的结点都至少有 2 个 key,不是的话就须要合并结点。这点比较难理解,在插入时,会把遍历过程当中遇到的4-结点 进行拆分,相对的,在删除时,就要保证遍历的结点至少有 2 个 key,也就至关于把以前拆分的进行了合并

如下图示演示了上述的每种可能的删除状况:

2-3-4-delete.png

简单来讲,理解 2-3-4 树删除的重点就是:

  • 若是删除的结点是多值结点,直接删除便可
  • 不然从兄弟结点获取一个多余的结点填补空缺
  • 再不然就从父结点获取一个结点填补空缺,若是父结点没有多余结点,将问题递归到父结点处理。

红黑树的删除

红黑树的删除也一样相似二叉查找树,不过要考虑平衡,也就是结点颜色问题,要麻烦一点。

首先声明一点,接下来讲的红黑树叶子结点二叉查找树叶子结点相同,若是要强调红黑树结点是空的叶子结点 NIL 会特殊说明,画图会使用黑色方框表示。

假设待删除结点为 M,若是有非叶子结点,称为 C,那么有两种比较简单的删除状况:

  1. M 为红色结点,那么它必是叶子结点,直接删除便可,由于若是它有一个黑色的非叶子结点,那么就违反了性质5,经过 M 向左或向右的路径黑色结点不等
  2. M 是黑色而 C 是红色,只须要让 C 替换到 M 的位置,并变成黑色便可,或者说交换 CM 的值,并删除 C(就是第一个简单的状况)。

注意M 有且仅有一个非叶子的左或右孩子结点,至关于 2-3-4 树删除的状况 1

这两个状况,本质都是删除了一个红色结点,不影响总体平衡。以 [7, 5, 9, 3, 4] 输入序列构建的红黑树为例,演示以上两种比较简单的状况:

delete-1.png

删除比较复杂的是 MC 都是黑色的状况,此时 M 确定是叶子节点,而 C 确定是 NIL 结点,若是不是这样的状况将违反性质5

一个黑色结点被删除会打破平衡,须要找一个结点填补这个空缺,假设待删除结点为 M,删除后它的位置上就变成了 NIL 结点,为了方便描述,这个结点记为 NP 表示 N 的父结点,S 表示 N 兄弟结点,S 若是存在左右孩子,分别使用 SLSR 表示,那么删除就有如下几种状况:

delete-2-case.png

状况 1 - N 是根结点

删除后,N 变成了根结点,也就是说删除前只有 M 这一个结点,直接删除便可。

状况 2 - P 黑 S 红

S 是红色,那么它必有两个孩子结点,且都为黑色,并且 P 也确定是黑色。此时,交换 PS 的颜色,而后对 P 左旋转,以下:

delete-2-2-case.png

如今,结点 N 的父结点变成了红色,兄弟结点变成了 SL,此时就能够按照状况 四、五、6继续处理。

状况 3 - P 黑 S 黑

P 是黑色,S 也是黑色,而且 S 也没有非空的孩子结点。此时,直接将 S 变成红色,那么通过 S 的路径也就少了一个黑色结点,总体上就致使通过 P 的路径比原来少了一个黑色结点,把不平衡状态从结点 N 转移到告终点 P,能够把 P状况1 处理,直到遇到根结点,以此造成递归

delete-2-3-case.png

状况 4 - P 红 S 黑

P 是红色,S 是黑色,而且 S 也没有非空的孩子结点。此时,只要交换 PS 的颜色,正好填补了少一个黑色结点的空缺,也就是恢复了平衡的状态:

delete-2-4-case.png

状况 5 - P 任意 S 黑 SL 红

P 任意颜色,S 黑色,S 的左孩子红色,(S 有右孩子也是红色)。此时,对 S 右旋转,并交换 SSL 的颜色:

delete-2-5-case.png

其实就是把这种状况,转成了 状况 6 进行处理。

状况 6 - P 任意 S 黑,SR 红

P 任意颜色,S 黑色,S 的右孩子红色,(S 有左孩子也是红色)。此时,对 P 左旋转,交换 PS 的颜色,并将 SR 变成黑色:

delete-2-6-case.png

此时恢复平衡的状态,不管 P 以前是什么颜色,N 都比以前多了一个黑色父结点。假设 P 原先是红色的,如今变成了黑色;假设原先是黑色的,如今 P 又多了一个黑色的父结点 S,因此,不管怎样,通过结点 N 路径增长了一个黑色结点。

以上 6 种状况,结点 N 都是左孩子,若是是右孩子,只需把左右对调便可。类比 2-3-4 树的删除,理解黑色结点删除后的关键就是:

  • 若是儿子结点中有红色的则从儿子结点中选一结点填补被删除后的空缺
  • 不然,从兄弟结点中选择一个结点填补空缺
  • 再不然,从父结点中选择一个结点填补空缺,将问题递归到父结点处理

最后来看下 2-3-4 树和红黑树动态删除的过程,输入序列为 [7, 5, 9, 3, 4, 8, 10, 11, 12, 13, 14, 15, 16],删除顺序是 [16, 13, 11, 7, 15, 14, 8, 4, 9, 10, 5, 3, 12],首先是 2-3-4 树的动态删除过程:

2-3-4-delete.gif

红黑树动态删除的过程:

rb-tree-delete.gif

小结

红黑树确实比较复杂,单纯的分析性质和旋转,意义不大,而 2-3-4 树就不同了,它的插入和删除简单多了,而红黑树的旋转和变色最终也是为了和同构的 2-3-4 树保持一致,本文就是相互结合分析,互相印证,相信会相对容易理解一点。

动图来自网站:www.cs.usfca.edu/~galles/vis… 它支持单步调试,有兴趣能够试一下。

搜索公众号「顿悟源码」获取更多源码分析和造的轮子。

相关文章
相关标签/搜索