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. 【疯狂造轮子-iOS】JSON转Model系列之一

    [疯狂造轮子-iOS]JSON转Model系列之一 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 之前一直看别人的源码,虽然对自己提升比较大,但毕竟不是自己写的,很容易遗 ...

  2. 如何一步一步用DDD设计一个电商网站(六)—— 给购物车加点料,集成售价上下文

    阅读目录 前言 如何在一个项目中实现多个上下文的业务 售价上下文与购买上下文的集成 结语 一.前言 前几篇已经实现了一个最简单的购买过程,这次开始往这个过程中增加一些东西.比如促销.会员价等,在我们的 ...

  3. OpenCASCADE Job - dimue

  4. 流程开发Activiti 与SpringMVC整合实例

    流程(Activiti) 流程是完成一系列有序动作的概述.每一个节点动作的结果将对后面的具体操作步骤产生影响.信息化系统中流程的功能完全等同于纸上办公的层级审批,尤其在oa系统中各类电子流提现较为明显 ...

  5. 快速了解微信小程序的使用,一个根据小程序的框架开发的todos app

    微信官方已经开放微信小程序的官方文档和开发者工具.前两天都是在看相关的新闻来了解小程序该如何开发,这两天官方的文档出来之后,赶紧翻看了几眼,重点了解了一下文档中框架与组件这两个部分,然后根据简易教程, ...

  6. setCapture、releasCapture 浅析

    1. setCapture 简介 setCapture可以将鼠标事件锁定在指定的元素上,当元素捕获了鼠标事件后,该事件只能作用在当前元素上. 以下情况会导致事件锁定失败: 当窗口失去焦点时,锁定的事件 ...

  7. SharePoint 2013: A feature with ID has already been installed in this farm

    使用Visual Studio 2013创建一个可视web 部件,当右击项目选择"部署"时报错: "Error occurred in deployment step ' ...

  8. mac下生成ssh keys 并上传github仓储

    使用github仓储需要本机生成一个公钥key 添加到自己的git账户SSH keys中   mac 生成方法:   1. 打开终端 输入   ssh-keygen 然后系统提示输入文件保存位置等信息 ...

  9. 萌新笔记——linux下查看内存的使用情况

    windows上有各种软件可以进行"一键加速"之类的操作,释放掉一些内存(虽然我暂时不知道是怎么办到的,有待后续学习).而任务管理器也可以很方便地查看各进程使用的内存情况,如下图: ...

  10. 2DToolkit官方文档中文版打地鼠教程(一):初始设置

    这是2DToolkit官方文档中 Whack a Mole 打地鼠教程的译文,为了减少文中过多重复操作的翻译,以及一些无必要的句子,这里我假设你有Unity的基础知识(例如了解如何新建Sprite等) ...