红黑树的理解与学习+伪代码

在看HashMap源码的时候,涉及到红黑树,这个数据结构早已听闻大名,而且在学校的教材中没有讲这个数据结构,所以花了点时间去学习和理解这个数据结构。(比我想象中的复杂的多……)

Red-Black Tree的简介

首先这是个二叉查找树,它属于但又不严格属于平衡二叉树(AVL),因为它没有像平衡二叉树一样,严格规定平衡因子的绝对值要小于等于1,而是靠他的颜色规定来达到高性能。

一棵拥有n个元素的RB树,树的高度最多为2log(n + 1),所以操作的时间复杂度是O(logN)级别的。——所以它其实是一棵平衡查找树。

RB-Tree的properties

首先,红黑树看名称我们都知道,这是个带颜色的树。

红黑树有五个设定,或者说是规矩/性质:

  1. 每个结点,要么是红色,要么是黑色。
  2. 根节点是黑色的。
  3. 每个叶节点,或者说是NIL节点(也可以说是null空结点)都是黑色的。
  4. 如果一个结点是红色的,那么它的爸爸不能是红色,它的子女也不能是红色,总之红黑树中不允许连续的两个红色结点出现。
  5. 对于每个结点,从该结点到其后代的所有叶节点的简单路径上,均包含相同数目的黑色节点。

还有两个小概念:

  1. 为了方便处理边界条件以及方便描述,我们把不需要关注的叶子节点和null节点统一用一个特殊节点表示,我们称它为哨兵节点,记为T.NIL。
  2. 第五点性质也能用一个叫做黑高——Black height的概念来描述——从某个节点x出发(不含该节点)达到一个叶节点的任意一条简单路径上的黑色节点个数称为该节点的黑高。

这里有时候作图方便,用两个圈圈来表示红色的节点,一个圈来表示黑色的节点。

下面看个红黑图的例子:

8节点是个红色节点,它的黑高为1;3的黑高也是1;10的黑高也是1。

就是因为这么苛刻看起来很奇怪的需求,让红黑树有了相当不错的性能。

红黑树的插入

首先,红黑树是一棵二叉查找树,所以首先的插入操作和一般的插入查找树一样,根据查找树的规矩将节点插入到正确的位置。

插入后,该新新节点便出现在叶子的位置,然后我们要上色——上成红色。

为什么是红色呢?红黑树的插入的难点就是要维持住那五个性质,如果你插入的是黑色,毫无意义第5条规矩肯定会被破坏掉。而如果插入的是红色,则存在规矩仍然成立的情况。

这个时候,我们很可能破坏了红黑树的规矩了,比如规矩4——连续两个红色节点。(其实好像也只可能破坏规矩4)

然后我们要做的,是如果发现冲突,则把这个冲突往上移动。

往上移动是总体的一个思路,相关的做法是:我们需要通过对一些节点进行从新上色,从而将破坏规矩的冲突位置往上移动,直至可以通过旋转来解决,旋转解决的过程中很可能伴随着再要重新上色。

说明下,伪代码中的节点里的P代表是父亲,然后left和right就分别代表左孩子和右孩子。

看下伪代码:

RB-INSERT(T, x)
  y = T.NIL;
  z = T.root;
  // 一直循环直到找到z的合适位置
  while z != T.NIL
    y = z;
    if x.key < z.key
       z = z.left;
    else z = z.right;
  x.p = y;
  if y == T.NIL
     T.root = x;
  elseif x.key < y.key
     y.left = x;
  else y.right = x;
  x.left = T.NIL;
  x.right = T.NIL;

  //这上面都是二叉查找树的插入过程,重点看下面

  //为新插入的节点上色成红色
  x.color = RED;

  //如果红色的新插入节点x的爸爸也是红色,就和规矩四冲突了,就要调整恢复红黑树的五大性质
  if(x.p.color == RED)RB-INSERT-FIXUP(T, x);

插入上色后,因为x为红色,所以只是可能与规矩4冲突,所以只要检测到x的爸爸是红色,就要调用恢复函数。

恢复函数的大致思路:

因为爸爸已经是红色了嘛,然后思路就是主要研究爷爷,还有爸爸的兄弟。爷爷这里其实已经知道了,一定是黑色的,因为爸爸是红色的话,爷爷只能是Black。

代码的思路是,不断地从新节点将冲突往上走,所以首先有个循环,只要x不是root根结点,x还是红色,循环就继续。

然后对于每个x,我们分为两个categoryA还有categoryB两个大情况,其实就是x的父亲是爷爷的左孩子还是右孩子,这两个category的操作是刚好相反的,然后这里的伪代码就只是写出一个。

然后每个category里面又有三种case。

case1:

  爷爷是Black(肯定),然后叔叔是红色的。这个时候我们只要把爷爷由黑色变成红色,然后把叔叔和爸爸都变成黑色,这样局部也就是从爷爷开始看下来,是没有冲突的,但爷爷由黑色变成红色肯定会造成上面的冲突,这里就把冲突往上移动了。所以,下一步就是把x赋值为爷爷,继续循环。

(图片是借https://blog.csdn.net/lm2009200/article/details/70148565的,所以上面说的case2不关事hh)

case2:

  叔叔是黑色的,x是爸爸的右孩子,先假设爸爸是爷爷的左孩子(就假设某个category)。然后视觉上,x和爸爸的红色冲突是“z”型的,这个时候需要的操作是对x进行左旋,然后就会把冲突变到一条直线上哈哈。

然后就可以进入case3.

(图片是借的,所以上面的case是不一样的hh)

case3:

  叔叔是黑色的,x是爸爸的左孩子,假设爸爸是爷爷的左孩子。这就冲突在一条线上了,这里涉及的操作是既要旋转也要上色,这里也借一下大佬的图片:

(同样无视里面的case的字)

要做的是,对x的爸爸进行右旋转,然后并对x的爸爸z重上色;还有对x的爷爷a重上色。

case4是终结情况,然后x要移动到它爸爸也就是z的位置,然z不是红色,循环结束。

这是别人写的一个解析:

我们一步步的分析如何从左边的图调整为右边的图,首先还是回到我们的指导思想,把x指针指向节点的父节点染黑,染黑后发现改变了子树Q和W的黑高,那么一个做法就是右旋转a节点,右旋节点a后发现子树F和G的黑高加了1,破坏了性质5,那么把节点a染红,正好就把黑高调整回来了,经过这样的调整,也就变成了上面右边的红黑树图案了。至此,性质4恢复了,红黑树的插入调整也正常结束。

然后看调整算法的伪代码:

while(x != T.root && x.color == RED) {
    if(x.p == x.p.p.left) {
    //x的爸爸是爷爷的左孩子,categoryA

        y == x.p.p.right;//y是爷爷的右孩子,也就是x的叔叔
        if(y.color == red){
            //case1的情况

            x.p.color = black;
                  y.color = black;
                  x.p.p.color = red;
                  x = x.p.p;
        } else {
          //x的叔叔不是红色,分成case2和case3

            if(x == x.p.right) {
            //case2——冲突成z型

                x = x.p;
                LEFT-ROTATE(T, x);//左旋转操作

                //然后就变成case3
            }

            //case3 选择加变色
            x.p.color = black;
                x.p.p.color = red;
                RIGHT-ROTATE(T, x.p.p);
        }

    } else(x.p为右子树,也就是x的爸爸是爷爷的右孩子,和爸爸是左孩子的操作相反即可);

}
T.root.color = BLACK;//如果一查入就是根节点,就直接到这里但根节点还是红色,所以要变成黑色。

红黑树的删除

删除就特么复杂了。

首先先来复习一下,二叉查找树的删除操作:

  如果要删除的那个结点没有孩子,直接删除;如果要删除的节点有一个左孩子或者右孩子,那么就由这个孩子来代替它;如果要删除的节点有两个孩子,那么要找它的直接前驱,它可以是左子树的最右边(比它小的最大值),也可以是右子树的最左边(比它大的最小值),找到直接前驱后,将直接前驱覆盖到要删除的节点的位置,然后删除直接前驱——问题转换到情况2甚至情况1。

红黑树的删除的大致流程也和这个差不多,但它要恢复红黑树的五条性质。

首先我们为红黑树定义一个覆盖函数:

//替换函数,用v节点替代u,只负责更改父节点的指向,左右孩子需要自己更改
RB-TRANSPLANT(T, u, v) {
    if(u.p == null) {
        T.root = v;
    } else if(u == u.p.left) {
        u.p.left = v;
    } else u.p.right = v;

    v.p = u.p;
}

然后是红黑树的删除流程函数的伪代码:

RB-DELETE(T, z)
  y = z;
  y-original-color = y.color;
  if z.left == T.NIL
    x = z.right;
    RB-TRANSPLANT(T, z, z.right);
  else if z.right == T.NIL
    x = z.left;
    RB-TRANSPLANT(T, z, z.left);
  else y = TREE-MINMUM(z.right)
    y-original-color = y.color;
    x = y.right;
    if y.p = z;
      x.p = y;
    else RB-TRANSPLANT(T, y, y.right)
         y.right = z.right;
         y.right.p = y;
    RB-TRANSPLANT(T, z, y)
    y.left = z.left;
    y.left.p = y;
    y.color = z.color;//更改y的颜色,这样的话从y以上红黑树的性质都不会违反
  if y-original-color == black
    RB-DELETE-FIXUP(T, x)

一开始看这个有点绕,因为以前写二叉查找树的删除,涉及替换是把要删除的那个点的值用直接前驱的值覆盖上去,然后改为删除直接前驱,而这里是直接通过指针的移动,反正如果不拿着笔仔细画指针很容易懵。

z指的一直是要删除的那个点,通过指针的移动后,z指的点会不可达(这里少了free节点z的操作),也就是被删除了;

y指的是,理论上真正要删除的这个点,这里就是懵的地方,后面才看清楚这里的指针,比如本来要删除z,然后找了z的直接前驱,理论上,直接前驱的值被覆盖到z上,然后删除直接前驱,所以y指向这个直接前驱。但这里指针的操作是,直接把y指向的直接前驱变到z的位置上,z变成没有爸爸,即不可达。所以这种情况删除完毕,y所指的节点还在树上;

x指向的,节点被删除后,补上那个空位的节点。

上面的几种删除情况用借一个大佬的示意图来理解:

首先,如果被删除的那个,也就是上图左边y指向的那个,或者说理论上要删除的那个节点是红色,那么对那五条性质不会有影响,只有这个被删掉的y是黑色的,才需要调用下面的恢复函数。

恢复思路:

首先,被删除的那个是黑色,那么百分之白含有这个节点的路径的黑高会见一,也就是肯定违背性质5,在已知这个的情况下,再分下面几种情况(下面的情况都是已经违背了性质5):

1. 违背性质2,如果被被删除的那个是根节点,而它的唯一一个孩子是红色的节点,那么就违背性质2了,这个很容易解决,直接把根节点染黑就行了。

2. 违背性质4,也就是x为红色,他爸也是红色,这种情况也容易解决,因为我们黑高是少了一的嘛,所以我们可以直接染黑x,这样刚好解决问题。

3. 剩下的情况,就是只是违背了性质5了,只要调整好性质5就行了。

这里有一个技巧,就是把x节点视为还有一层黑色,问题就变成了解决违反性质1了,也就是把x看成既红又黑,我们只要把这层额外的黑色不断往上推,直到推给了一个红色节点,那么子树的黑高就恢复了。和插入一样,有个关键思想是,转换过程中千万不能破坏其他任何的性质。经过分析,破坏性质1(本质上是破坏性质5)有以下五种情况:

只是违背性质5的情况下的五种情况:

以下内容全来自博客:https://blog.csdn.net/lm2009200/article/details/70162811

case 1 x是红色的

case 2. x的兄弟节点w是红色的

case 3 x的兄弟节点w是黑色的,而且w的两个子节点都是黑色的

case 4 x的兄弟节点w是黑色的,w的左儿子是红色的,w的右孩子是黑色的

case 5 x的兄弟节点w是黑色的,且w的右孩子是红色的。

case 1是最容易解决的,直接染黑就是了。也就是说,违背性质4的情况可以和这里归为一类,都是直接染黑x。

case 2的话,改变w和x.p的颜色,左旋转x.p,这样子不改变任何性质的同时,把case 2转变为case 3,4,5。不做详细讨论
伪代码为:

w.color = black;

x.p.color = red;

LEFT-ROTATE(T, x.p)

w = x.p.right;

case 3的话,可以认为从x和w去掉一层黑色给x.p,如果x.p为原本为红色的话,那么x的子树黑高加一,w子树黑高不变,性质就恢复好了,如果x.p原来为黑色的,那么认为x.p的整个子树黑高都少了1,多了的一层黑色就给了x.p,case3就转为case 2,3,4,5了。

伪代码如下:

w.color = red;
x = x.p

case 4的情况左侄儿为红,右侄儿为黑,这种情况统一转case 5来处理。

这里右旋w并且没有改变红黑树的五大性质,转为了case5。伪代码如下:

w.left.color = black;
w.color = red;
RIGHT-ROTATE(T, w)
w = x.p.right;

case 5的情况是红黑树调整的出口,只要到达了case 5,调整完就能恢复所有性质了。调整如下图所示:

接下来分析case5的转换过程,这里的思路是这样的:首先我们要让x子树黑高加一,那么就左旋转a,左旋转后d的左子树没有任何问题,但是右子树黑高可能减少了1(如果a原来是黑色的情况),为了解决这个问题,可以把a和d颜色交换,然后染黑c,这样左旋转后的d的右子树的黑高也就不会有任何改变了。伪代码如下:

w.color = x.p.color;
x.p.color = black;
w.right.color = black;
LEFT-ROTATE(T, x.p);
x = T.root;

最后是整个删除调整的伪代码:

RB-DELETE-FIXUP(T, x)
 while x != T.root && x.color = black
   if x == x.p.left
     w = x.p.right
     // case 2
     if w.color = red
       w.color = black;
       x.p.color = red;
       LEFT-ROTATE(T, x.p)
       w = x.p.right;
     // case 3
     if w.left.color == black && w.right.color == black
       w.color = red;
       x = x.p;
     // case 4
     else if w.right.color == black
       w.left.color = black;
       w.color = red;
       RIGHT-ROTATE(T, w)
       w = x.p.right;
     // case 5
     w.color = x.p.color;
     x.p.color = black;
     w.right.color = black;
     LEFT-ROTATE(T, x.p);
     x = T.root;

参考文章与资料:

  网易云公开课的算法导论红黑树部分。

  《必须要把红黑树讲清楚,看完还不明白请直接找我之》

    系列2——https://blog.csdn.net/lm2009200/article/details/70148565

    系列3——https://blog.csdn.net/lm2009200/article/details/70162811

原文地址:https://www.cnblogs.com/wangshen31/p/10393317.html

时间: 2024-11-13 09:11:00

红黑树的理解与学习+伪代码的相关文章

红黑树的理解与Java实现

前言 前段时间在研究 JDK1.8 的 hashmap 源码,看到 put 方法的插入环节,遇到了红黑树,不得不停止阅读源码的过程,因为还没掌握红黑树是无法完全读透 hashmap 源码的.红黑树作为一种数据结构,它被应用得非常多,可能很多人不认识它,但其实它已经在默默为我们的代码在发光发热.例如,你只要在 Java 中用到 map,基本上就是在用红黑树(当元素个数到达八个时链表转红黑树). PS:在看这篇文章前,必须先了解普通的二叉查找树和平衡查找树(AVL)树.2-3-4树.不然看起来会非常

对B+树,B树,红黑树的理解

出处:https://www.jianshu.com/p/86a1fd2d7406 写在前面,好像不同的教材对b树,b-树的定义不一样.我就不纠结这个到底是叫b-树还是b-树了. 如图所示,区别有以下两点: B+树中只有叶子节点会带有指向记录的指针,而B树则所有节点都带有,在内部节点出现的索引项不会再出现在叶子节点中. B+树中所有叶子节点都是通过指针连接在一起,而B树不会. B+树的优点: 非叶子节点不会带上指向记录的指针,这样,一个块中可以容纳更多的索引项,一是可以降低树的高度.二是一个内部

《红黑树》学习心得

R-B Tree简介 R-B Tree,全称是Red-Black Tree,又称为"红黑树",它一种特殊的二叉查找树.红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black). 红黑树的特性:(1)每个节点或者是黑色,或者是红色.(2)根节点是黑色.(3)每个叶子节点(NIL)是黑色. [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!](4)如果一个节点是红色的,则它的子节点必须是黑色的.(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的

30张图带你彻底理解红黑树

本文转自安卓大叔 写在前面 当在10亿数据中只需要进行10几次比较就能查找到目标时,不禁感叹编程之魅力!人类之伟大呀! —— 学红黑树有感. 终于,在学习了几天的红黑树相关的知识后,我想把我所学所想和所感分享给大家.红黑树是一种比较难的数据结构,要完全搞懂非常耗时耗力,红黑树怎么自平衡?什么时候需要左旋或右旋?插入和删除破坏了树的平衡后怎么处理?等等一连串的问题在学习前困扰着我.如果你在学习过程中也会存在我的疑问,那么本文对你会有帮助,本文帮助你全面.彻底地理解红黑树! 本文将通过图文的方式讲解

说说红黑树——不谈操作,只讲理解

一.前言 ??这几天想学一学红黑树这种数据结构,于是上网找了很多篇博客,初看吓了一跳,红黑树竟然如此复杂.连续看了几篇博客后,算是对红黑树有了一些了解,但是它的原理却并不是特别理解.网上的博客,千篇一律的都是在叙述红黑树的操作,如何插入节点.删除节点,旋转.变色等,只关注如何正确构建一棵红黑树,但是却很少提及为什么这么做.这篇博客我就来记录一些我所知道的红黑树中比较重要的东西,以及谈一谈我的理解. ??我不会描述红黑树的具体实现,因为阅读红黑树具体实现的过程中,我发现这真的不是很重要,没有太大的

通过2-3-4树理解红黑树

前言 红黑树是数据结构中比较复杂的一种,最近与它交集颇多,于是花了一周的空闲时间跟它死磕,终于弄明白并实现了红黑树.写文总结一下,希望能给试图理解红黑树的同学一些灵感,也让我能记得更深刻. 在研究红黑树时吃了不少苦头,原因有二: 红黑树的插入和删除非常复杂,很多人并没有理解或完全实现,或实现了的没有任何注释,让人很难参考: 网络上红黑树的理解方式较为单一,一般是 双黑.caseN 法,而插入和删除的情况很多,每种都有对应的处理方式,如果死记硬背的话,再过一段时间再回忆各种情况可能就一头雾水了.

通过2-3树理解红黑树

一.简介 前面的文章我们循序渐进的讲解了<二叉树><二分搜索树><AVL-平衡二叉树>,从左至右互为基础.尤其是二分搜索树给了我们如何将数据组织成为搜索树的思想,当然二分搜索树存在的天然问题--在极端情况下回退化为链表.所以引出了AVL-平衡二叉树,通过再平衡即LL,LR,RR,RL四个旋转操作维护了一棵平衡的二分搜索树.本章节我们继续梳理一个高阶的树结构即:红黑树.想必大家都知道,红黑树如何维持平衡,如何进行颜色反转让人很难理解,虽然很多博文很多书对红黑树都有讲解,但

红黑树(一)之 原理和算法详细介绍---转帖

目录1 红黑树的介绍2 红黑树的应用3 红黑树的时间复杂度和相关证明4 红黑树的基本操作(一) 左旋和右旋5 红黑树的基本操作(二) 添加6 红黑树的基本操作(三) 删除 作者:Sky Wang    于 2013-08-08 概述:R-B Tree,又称为"红黑树".本文参考了<算法导论>中红黑树相关知识,加之自己的理解,然后以图文的形式对红黑树进行说明.本文的主要内容包括:红黑树的特性,红黑树的时间复杂度和它的证明,红黑树的左旋.右旋.插入.删除等操作. 请尊重版权,转

算法---红黑树实现介绍(一)

一.概述 红黑树是一种经典的存储结构,就其本身来说是一个二叉查找树,只是在这个基础上,树的节点增加了一个属性用于表示颜色(红或黑).通过限制从根节点到叶子的各个路径的节点着色的限制,来保证不会有哪个路径会比其它的路径长度超过2倍,从而红黑树是接近平衡的. 一直以来没有把红黑树完全理解,总觉得太难,望而生畏,最近下决心要弄清楚,也是花了很长时间,不过总算是明白了.记录下来以便更好的理解. 二.红黑树的特点 作为红黑树,需要有这5个限制,如下: 1)树中的每个节点,要么是红色,要么是黑色 2)树的根