1. 什么是红黑树

(1) 简介

    上一篇我们介绍了基本动态集合操作时间复杂度均为O(h)的二叉搜索树。但遗憾的是,只有当二叉搜索树高度较低时,这些集合操作才会较快;即当树的高度较高(甚至一种极端情况是树变成了1条链)时,这些集合操作并不比在链表上执行的快。

    于是我们需要构建出一种“平衡”的二叉搜索树。

    红黑树(red-black tree)正是其中的一种。它可以保证在最坏的情况下,基本集合操作的时间复杂度是O(lgn)。

(2) 性质

    与普通二叉搜索树不同的是,红黑树在每个结点上增加了一个存储位来表示该结点的颜色(只能是Black或Red中的一种),因此此时一个结点包含5个属性:color,key,left,right和p。通过对各个结点的颜色进行约束,可以保证任何一条从根到叶子的简单路径上不会比其他路径长2倍(这就保证了“平衡”)。

    这个约束(性质)是:

① 根结点和叶结点是黑色的;

② 红色结点的子结点必是黑色的;

③ 任何一个结点到其所有后代叶结点的简单路径包含相同数目的黑色结点,并称这个黑色结点的数目(不包含出发结点)为黑高(black-height,用bh(x)表示,红黑树的黑高为根结点的黑高)。

下图是一棵红黑树:

 

    也许你会奇怪上面的红黑树并没有满足叶结点必须是黑色这条性质呀。事实上它是满足的,因为上图画出的其实是树的内部结点,我们在真正处理时,会把上图中的叶结点的左右孩子指向一个值为NIL,颜色为黑色的结点(外部节点),即真正的叶结点是这个黑色的值为NIL的结点,这样就满足红黑树性质了。如下图所示:

    但是如果采用上面的方法处理,无形中加入了这么多“无用”的结点,势必会浪费大量的存储空间。其实我们可以把这些NIL结点合并为一个,就像下图做的那样:

 但为了方便起见,我们之后的讨论将忽略这个值为NIL的结点。

(3) 为什么红黑树是一种好的搜索树

因为

一棵有n个结点的红黑树的高度至多为2lg(n+1)。

我们可以先用数学归纳法证明:以任一结点x为根的子树至少包含2bh(x)-1个内部结点。然后根据约束③便可以得出上述结论(具体证明略)。

    因此可知,动态集合操作Search、Minimum、Maximum、Successor和Predecessor在红黑树上可在O(lgn)时间内完成。

    由于红黑树其实是一种“平衡”的二叉搜索树,因此我们只需要研究它的插入和删除操作,其他操作和二叉搜索树一致。

2. 旋转

    在研究插入操作之前,我们先来介绍旋转操作。

    我们在红黑树上进行Insert、Delete操作时,会因为修改了树的结构而导致违背上述约束。这时就需要修改树中某些结点的颜色和指针结构来维护红黑树的性质。

    指针结构的修改是通过旋转完成的。下图是两种旋转操作:左旋和右旋的示意图。

下面给出左旋操作的伪代码:

可以看出,左旋(右旋也类似)操作可以在O(1)时间内完成。

下图是一个左旋操作的实际例子:

3. 插入

(1) 算法

    我们可以用类似于二叉搜索树的方法来向树中插入一个元素。它可以在O(lgn)时间内完成。插入算法的描述如下:

可以看出我们默认把新插入的结点着为红色插入(原因之后给出)。与二叉搜索树的插入算法最为不同的是,在最后一步我们调用了RB-INSERT-FIXUP方法来维护红黑树的性质。下面给出它的具体描述:

下图是一个范例:

(2) 分析

上述过程可能有些复杂,我们来仔细分析一下,从两个方面入手。

    第一,我们应当明确RB-INSERT-FIXUP方法是来维护红黑树的性质的,因此我们要搞清楚插入一个结点(红色)将会打破哪些性质;

    第二,我们要具体分析上述的三种情况究竟在做什么有什么影响

① 哪些性质会被破坏

    很明显,只有性质①——根结点必须是黑色(当且仅当插入时树为空会发生这种情形)和性质②——红色结点的孩子必为黑色会被破坏。

② 三种情况

    首先,循环的大前提是z的父结点是红色。然后,我们分析的三种情况建立在z的父结点是左孩子基础上(相反情况类似,不做分析)。

    我们还容易看出:在每次迭代前,z结点总是红色的;y结点为z结点的“叔叔”(y = z.p.p.right,下面称y为z的叔结点)。

    Case 1:z的叔结点y为红色(同时也说明了z的“爷爷”结点是黑色的)。在做什么:把z的“爷爷”结点着为红色;而把“爷爷”结点的子结点都着为黑色;z上升2级,指向它的“爷爷”结点。有什么影响:以上操作对任何一条简单路径的黑高都不会产生影响;操作其实并没有让情况得到“改善”,只是使z上升了2级。

    Case 2:z的叔结点y为黑色且z为右孩子。在做什么:z指向自己的父结点;对z结点进行左旋操作。有什么影响:以上操作对任何一条简单路径的黑高都不会产生影响,只是将Case 2变为了Case 3。

    Case 3:z的叔结点y为黑色且z为左孩子。在做什么:把z的父结点置为黑色;把z的“爷爷”结点置为红色;对z的“爷爷”结点进行右旋操作。

有什么影响:在两次修改颜色后,会导致从根结点向左出发的所有路径的黑高加1,而向右出发的所有路径的黑高不变;而右旋操作会使黑高回归平衡。

(3) 证明

下面我们再用循环不变式(关于循环不变式见算法基础——算法导论(1))来分析(证明)上述过程:

这个不变式是:

1) 结点z是红色的;

2) 最多仅有1条红黑性质被打破,要么是性质①——根结点为黑色被打破;要么是性质②——红结点的孩子必须是黑色被打破。

初始化

    由于我们默认把新插入的结点置为红色,因此初始时,结点z是红色显然成立。

    在迭代之前,如果只有一个结点,即只有根结点,性质①被打破;否则,z和z.p都为红色,性质②被打破;

    综上所述,不变式在初始时成立。

保持:

    从上面对三种情况的分析我们可以看出:结点z始终是红色的;三种操作都没有改变任何路径上的黑高,即性质③始终是满足的。显然每次完成case 1后,性质①或性质②是被打破的。完成case 2一样。当完成case 3后,循环就终止了(因为在case 3中我们把z.p置为了黑色),此时满足红黑性质。

    不变式始终成立。

终止:

    迭代终止的条件是:z.p为黑色。通过保持性分析我们看出,终止只可能发生在执行完case 1和case 3情形后。而在执行完case 1后z为红色,z.p为黑色,因此性质②不可能被打破,那么只可能是性质①被打破;在执行完case 3后,就已经满足所有红黑性质,即已经是一棵合法的红黑树。

    由上述分析,我们可以得出:在循环结束后,要么二叉树已经是一棵合法的红黑树;要么只有性质①——根结点为黑色被打破。

    于是在循环结束后,我们只需要做一次将根结点置为黑色,那么便修正了红黑树的合法性。

(4) 说明

    由于一棵有n个结点的红黑树的高度为O(lg n),因此执行RB—INSERT的前16行需要O(lg n)时间;在RB-INSERT-FIXUP中,仅当case 1发生时,while循环才会执行下去;而每次执行完case 1,指针z都会上升2层。因此while循环最多执行lg n次;所以RB-INSERT-FIXUP时间复杂度为O(lg n),因此整个插入操作的时间复杂度为O(lg n)。

4. 删除

(1) 算法

    同二叉搜索树一样,我们先给出TRANSPLANT方法,该方法会用以v为根结点的子树替换以u为根结点的子树:

    下面给出删除操作的算法描述:

    可以看出,以上删除操作与普通二叉搜索树相比没有太大差别。其中最大的差别是以上操作在22行加了一个维护红黑性质的过程,RB-DELETE_FINXUP,该操作的过程如下:

(2) 分析

RB-DELETE也分了三种不同的情况,其中第三种情况又分了两小种,分别依次对应如下图所示的情形:

 

    同样我们还是要分析各种情况什么有什么影响(对红黑性质而言)。

    可以发现上图其实就是普通二叉搜索树在删除时的分类情况,事实上,以上的代码完全包含了普通二叉搜索树删除操作的代码,只是在其基础上加上了维护红黑性质的代码。因此对于这些重复的代码做了什么我们不再分析。

    我们在每次删除前,对于上面前两幅图中的情况,我们会记录下被删除结点z的颜色,因为它决定了我们最后是否需要修正红黑性质(若z为红色,其父结点和孩子必为黑色,删除z将不会违背性质③,用z的孩子去替代z也不会影响性质②),y指向z,x指向z的右孩子;对于后两种情况,我们记录的是被删除结点z右子树中关键字最小的结点y的颜色,同样,如果它是红色,不管z是什么颜色,统一将y的颜色修改为z的颜色,并按照图中的方式去置换掉z,都不会对红黑性质产生影响。综合上述分析,我们发现四种情况的“输出”(处理后的结果)是一致的:若记录的颜色是红色,说明红黑性质未改变;如果是黑色,说明红黑性质一定被打破了。具体的说,如果结点y是黑色,如下图,将会造成3种影响:

1)如果y原来是根结点,而y的一个红孩子成为了新的根结点,将会违背性质①。如上图①中左侧情况。

2)如果x和x的父结点都为红色,将违背性质②。如上图③中的左侧,x为红色的情况。

3)所有的情况(除了y原来是根结点外)都将导致之前包含y结点的简单路径上的黑结点数少1,将违背性质③。修正这一问题的方式是我们将现在占据y结点位置的x结点“再涂上一层黑色”,当然,“涂色”操作并不反映在代码上,即我们不会修改x的color属性,我们只是“在心中记住”,适当的时候会把x的这层黑色涂到某个红色结点上以达到目的。“涂了两层色”的x结点可能是双层黑色或红黑色,它们分别会“贡献”2或1个黑色结点数。

 

下面我们再分析RB-DELETE-FIXUP修正过程:

修正过程分了4种情况,我们先给出每种情况对应的示意图:

然后对每种情况给出分析(建立在x是左孩子的基础上):

Case 1:x的右兄弟w是红色,说明x的父结点一定是黑色。所作的操作是:交换w和其父结点的颜色,即把w换为黑色,其父结点换位红色;然后对父结点左旋,w重新指向x的右兄弟(该结点原本是w的左孩子,所以一定为黑色)。这是Case 1过度到Case 2。

Case 2:w的孩子都为黑色(w也是黑色)。所作的操作是:将w换为红色,x指向其父结点。

Case 3:w的左孩子是红色,右孩子是黑色(w也是黑色)。所作的操作是:交换w和其左孩子的颜色,即把w换位红色,其左孩子换为黑色;然后对w右旋,w重新指向x的右兄弟。

Case 4:w的右孩子是黑色(w是黑色)。w与x的父结点交换颜色;并把w的右孩子设为黑色,对x的父结点左旋,x直接指向根结点,循环结束。

做完以上的while循环,我们还要做的一步操作是将根结点置为黑色。这样就能保证满足性质①。

可以证明做完上述操作,所有的红黑性质便满足了。具体证明过程和插入操作时的证明类似,这里省略。

我们也不难分析出RB-DELETE的时间复杂度是O(lg n)。

5. 小结

    像上面那样,我们就可以构造出一棵红黑树了。它能保证基本集合操作的时间复杂度为O(lg n)。

    先记录到这里,以后再给出普通二叉搜索树和红黑树的Java实现代码。

红黑树——算法导论(15)的更多相关文章

  1. 通过分析 JDK 源代码研究 TreeMap 红黑树算法实现

    本文转载自http://www.ibm.com/developerworks/cn/java/j-lo-tree/ 目录: TreeSet 和 TreeMap 的关系 TreeMap 的添加节点 Tr ...

  2. 通过分析 JDK 源代码研究 TreeMap 红黑树算法实现--转

    TreeMap 和 TreeSet 是 Java Collection Framework 的两个重要成员,其中 TreeMap 是 Map 接口的常用实现类,而 TreeSet 是 Set 接口的常 ...

  3. 通过分析 JDK 源代码研究 TreeMap 红黑树算法实

    TreeMap和TreeSet是Java Collection Framework的两个重要成员,其中TreeMap是Map接口的常用实现类,而TreeSet是Set接口的常用实现类.虽然HashMa ...

  4. 研究jdk关于TreeMap 红黑树算法实现

    因为TreeMap的实现方式是用红黑树这种数据结构进行存储的,所以呢我主要通过分析红黑树的实现在看待TreeMap,侧重点也在于如何实现红黑树,因为网上已经有非常都的关于红黑树的实现.我也看了些,但是 ...

  5. 红黑树-算法大神的博客-以及java多线程酷炫的知识

    http://www.cnblogs.com/skywang12345/p/3245399.html 解释第5条:从 ->根节点(或者任意个结点)到->所有的末端节点的路径中 ->黑 ...

  6. 红黑树之 原理和算法详细介绍(阿里面试-treemap使用了红黑树) 红黑树的时间复杂度是O(lgn) 高度<=2log(n+1)1、X节点左旋-将X右边的子节点变成 父节点 2、X节点右旋-将X左边的子节点变成父节点

    红黑树插入删除 具体参考:红黑树原理以及插入.删除算法 附图例说明   (阿里的高德一直追着问) 或者插入的情况参考:红黑树原理以及插入.删除算法 附图例说明 红黑树与AVL树 红黑树 的时间复杂度 ...

  7. 红黑树与AVL树

    概述:本文从排序二叉树作为引子,讲解了红黑树,最后把红黑树和AVL树做了一个比较全面的对比. 1 排序二叉树 排序二叉树是一种特殊结构的二叉树,可以非常方便地对树中所有节点进行排序和检索. 排序二叉树 ...

  8. Red Black Tree(红黑树)

    (修改于 2018-05-06 15:53:22 还差删除维护操作.层序遍历没完成.维护操作没完成不想写层序遍历怎么办...) 今天下午完成了红黑树的插入的维护操作,但删除的维护操作还没有解决,删除的 ...

  9. Java集合详解6:这次,从头到尾带你解读Java中的红黑树

    <Java集合详解系列>是我在完成夯实Java基础篇的系列博客后准备开始写的新系列. 这些文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查 ...

随机推荐

  1. ABP文档 - 通知系统

    文档目录 本节内容: 简介 发送模式 通知类型 通知数据 通知重要性 关于通知持久化 订阅通知 发布通知 用户通知管理器 实时通知 客户端 通知存储 通知定义 简介 通知用来告知用户系统里特定的事件发 ...

  2. 运行执行sql文件脚本的例子

    sqlcmd -s -d db_test -r -i G:\test.sql 黑色字体为关键命令,其他颜色(从左至右):服务器名称,用户名,密码,数据库,文件路径 通过select @@servern ...

  3. Mysql事务探索及其在Django中的实践(二)

    继上一篇<Mysql事务探索及其在Django中的实践(一)>交代完问题的背景和Mysql事务基础后,这一篇主要想介绍一下事务在Django中的使用以及实际应用给我们带来的效率提升. 首先 ...

  4. (JS+CSS)实现图片放大效果

    代码很简单,在这里就不过多阐述,先上示例图: 实现过程: html部分代码很简单 <div id="outer"> <p>点击图片</p> &l ...

  5. 神马玩意,EntityFramework Core 1.1又更新了?走,赶紧去围观

    前言 哦,不搞SQL了么,当然会继续,周末会继续更新,估计写完还得几十篇,但是我会坚持把SQL更新完毕,绝不会烂尾,后续很长一段时间没更新的话,不要想我,那说明我是学习新的技能去了,那就是学习英语,本 ...

  6. Android学习路线总结,绝对干货

    title: Android学习路线总结,绝对干货 tags: Android学习路线,Android学习资料,怎么学习android grammar_cjkRuby: true --- 一.前言 不 ...

  7. [C#] 进阶 - LINQ 标准查询操作概述

    LINQ 标准查询操作概述 序 “标准查询运算符”是组成语言集成查询 (LINQ) 模式的方法.大多数这些方法都在序列上运行,其中的序列是一个对象,其类型实现了IEnumerable<T> ...

  8. Mac OS 使用 Vagrant 管理虚拟机(VirtualBox)

    Vagrant(官网.github)是一款构建虚拟开发环境的工具,支持 Window,Linux,Mac OS,Vagrant 中的 Boxes 概念类似于 Docker(实质是不同的),你可以把它看 ...

  9. P2V之后的磁盘扩容新思路

    背景: 原先的物理机环境多是若干块物理磁盘经过RAID卡进行了RAID5之后的虚拟磁盘组,这样我们在操作系统内看到的也就是一块完整的磁盘.我们会在上面进行分区,然后格式化后以便使用. Figure 1 ...

  10. 读书笔记汇总 - SQL必知必会(第4版)

    本系列记录并分享学习SQL的过程,主要内容为SQL的基础概念及练习过程. 书目信息 中文名:<SQL必知必会(第4版)> 英文名:<Sams Teach Yourself SQL i ...