博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
红黑树算法思悟
阅读量:4108 次
发布时间:2019-05-25

本文共 10154 字,大约阅读时间需要 33 分钟。

红黑树算法思悟

  1. 红黑树是什么?

    红黑树是一种“平衡树索树”,可以保正在最坏情况下基本动态集合操作的时间复杂度为O(log n);

    红黑树中的结点有一个颜色属性:黑的或者红的;红黑树通过对任何一条从根到叶子结点的简单路径上各结点的颜色对平衡树进行约束,因而是近似于平衡的;

  2. 红黑树有什么性质?

    1. 每个结点或者是红色的,或者是黑色的;
    2. 根结点是黑色的;
    3. 虚拟叶子结点(虚拟叶子结点,该结点逻辑上没有值,也就是其父结点是传统意义上的叶子结点,我把父结点叫做实体叶子结点)是黑色的;虚拟叶子结点也叫外部结点,其余结点称为内部结点;
    4. 如果一个结点是红色的,则它的两个子结点都是黑色的;
    5. 对于每个结点,从该结点到其所有后代叶子结点的简单路径上,均包含相同数目的黑色结点;
    6. 一棵有n个内部结点的红黑树,其高度最高为2log(n+1);
  3. 红黑树如何实现?

    对于红黑树的常见操作包括:查找、插入、删除等;

    大体来看:

    查找的方式同普通二叉搜索树一样;

    删除同二叉平衡树树基本一致,前半部分先做以一次查找,找到要删除结点B后,按该结点左右子结点的数目分类处理:如果有两个结点(实体子结点),寻找其中序后驱(该结点只有一个结点)A,然后对A执行删除操作(从物理结构上删除结点),并把A结点的值赋给B(从逻辑上删除);如果B只有一个结点,那么直接对B进行删除操作;后半部分就是对树的性质的维护操作了,对于二叉平衡树来说,需要保证平衡因子,对于红黑树来说就是保证红黑树性质中的2、4、5条,具体怎么做见下文详细分析;

    对X结点(此时X结点最多只有一个结点)执行删除操作是说,使用X的子结点替代X成为其父结点的孩子结点;

    插入同删除处理过程类似;前半部分把结点插入到合适的地方,后半部分维护红黑树的性质,同样,具体算法见下文详细分析;

    其实,上文对于删除操作的介绍在另一篇中也记录过,这里又叙述一遍并不是白费口舌,而是在学习和理解红黑树相关算法时,对于以上操作有了新的感悟,所以借此记录下来:删除操作中很明显采用了分类讨论的思想:对删除结点B的子结点数目进行分类讨论,然后分别处理,以前自己只看到“分”,但是没看到其中的“合”(额,我不是要说“天下大势分久必合,合久必分”:-));“分”,比较好理解,分类讨论嘛,常见的数学思想,但讨论并不是终止;而“合”体现在不管结点B的子结点数目是什么样的,最后真正执行删除操作时,所处理的结点都最多只有一个子结点!也就是经过前面的“分”,我们的最终目的是“合”:将不同的操作化为统一的处理!这一点在红黑树的相关算法(插入、删除)里体现尤为突出,且看下文一一道来~

    在进入本文最重要(精彩)的部分之前,还有一些重要操作要提及(磨刀不误砍柴工嘛);

    左右旋

    1. 左旋:

      对p左旋是说,p的右子结点q作为p的父结点;p结点作为q的左子结点;原来q的左子结点作为p的右子节点;

    2. 右旋:

      对q右旋是说,q的左子结点p作为q的父结点;q结点作为p的右子节点;原来p的右子节点作为q的左子结点;

    可以看出的是,对x执行左旋或者右旋,那么x就会进入其原来的左子树或者右子树,而对应的x的子结点将离开所在的子树;

    以对结点p进行左旋为例:

    根结点到a中叶子结点的路径上将:增加一个q(p原来就在);

    根结点到b中叶子结点的路径上将:保持不变;

    根结点到c中叶子结点的路径上将:少一个p(原来就在);

    这样来看,执行一次左旋操作,将对a子树和c子树造成影响,从图中来看,a和c子树都处在树的外边;

    对于右旋的分析也是一样的~

    具体算法实现:

    1. 红黑树插入算法

      //RedBlackTreeNode
      中的insert()方法public void insert(T newValue) { if(newValue==null){ throw new NullPointerException(); }//空值检查 RedBlackTreeNode
      fatherNode=this; RedBlackTreeNode
      checkNode;// int compareResult; while(true){ compareResult=newValue.compareTo(fatherNode.value);//比较fatherNode和待插入值的大小 if(compareResult<=0){ if(fatherNode.leftTree!=null){ fatherNode=(RedBlackTreeNode
      ) fatherNode.leftTree;//左子结点为实体结点,进入查找 }else{
      //左子结点为虚拟叶子结点,创建新的实体结点并建立联系 checkNode=new RedBlackTreeNode<>(fatherNode,newValue);//默认为红色结点以保持路径上黑结点数目不变,至于能不能为红色,后半部分会进行维护; fatherNode.leftTree=checkNode; break;//结束循环 } }else{ if(fatherNode.rightTree!=null){ fatherNode=(RedBlackTreeNode
      )fatherNode.rightTree;//右子结点为实体结点,进入查找 }else{
      //右子结点为虚拟叶子结点,创建新的实体结点并建立联系 checkNode=new RedBlackTreeNode<>(fatherNode,newValue); fatherNode.rightTree=checkNode; break;//结束循环 } } } //checkNode为新增结点,默认红色以保证黑平衡(通过构造函数实现~),接下来进入维护代码 RedBlackTreeNode
      grandFatherNode; RedBlackTreeNode
      uncleNode; while(true){ //开始维护黑平衡 fatherNode=(RedBlackTreeNode
      )checkNode.parent;//更新fatherNode的值 if(fatherNode.value==null){ //checkNode是实际的根结点————表现在fatherNode为空头结点 checkNode.isRed=false;//根结点必须是黑的 return;//插入结束 } //checkNode不是根结点,即它有实体父结点; if(!fatherNode.isRed){ //fatherNode是黑色的,checkNode是红色的,ok,没有破坏性质 return; } //fatherNode是红的,checkNode为红的,并且fatherNode不是根结点 grandFatherNode=(RedBlackTreeNode
      )fatherNode.parent;//得到祖父节点,因为fatherNode为红的,所以grandFatherNode一定为黑的 if(grandFatherNode.isLeftTree(fatherNode)){ //fatherNode为grandFatherNode的左子树 uncleNode=(RedBlackTreeNode
      )grandFatherNode.rightTree;//uncleNode为fatherNode的兄弟结点 if(uncleNode.isRed){ //uncleNode为红色的 fatherNode.isRed=false;//fatherNode改为黑色 uncleNode.isRed=false;//unclueNode改为黑色 grandFatherNode.isRed=true;//祖父节点改为红的 checkNode=grandFatherNode;//调整checkNode,从头再来 continue; } //fatherNode是红的,checkNode为红的,grandFatherNode为黑的,uncleNode为黑的 if(fatherNode.isRightTree(checkNode)){ fatherNode=leftRotate(fatherNode); checkNode=(RedBlackTreeNode
      ) fatherNode.leftTree; } //至此,checkNode是fatherNode的左孩子,checkNode为红的,fatherNode为红色的,uncleNode为黑的; fatherNode.isRed=false; grandFatherNode.isRed=true; rightRotate(grandFatherNode); return; }else{ //处理fatherNode是grandfatherNode右孩子的情况,分析省去,对称处理即可~ uncleNode=(RedBlackTreeNode
      )grandFatherNode.leftTree; if(uncleNode.isRed){ fatherNode.isRed=false; uncleNode.isRed=false; grandFatherNode.isRed=true; checkNode=grandFatherNode; continue; } if(fatherNode.isLeftTree(checkNode)){ fatherNode=rightRotate(fatherNode); checkNode=(RedBlackTreeNode
      ) fatherNode.leftTree; } fatherNode.isRed=false; grandFatherNode.isRed=true; leftRotate(grandFatherNode); return; } } }

      插入算法总结(这里主要总结维护黑平衡的过程~):

      仍然使用代码中的变量:checkNode、fatherNode、grandfatherNode表示相应概念;

      这一部分代码首先处理了checkNode为根结点的情况(将其颜色改为黑色即可返回)和fatherNode为黑色的情况(什么都不影响,直接返回);然后,我们就可以推出fatherNode为红色以及fatherNode的父结点grandfatherNode为黑色的;

      接下来分两种情况:fatherNode是grandfatherNode的左孩子还是右孩子;

      然后分析是左孩子的情况:当我们确认fatherNode和grandfatherNode的关系后,我们就能得到uncleNode,之后按道理又会分两种情况:uncleNode是红色结点或者黑色结点~但是,这里并没有采用“if-else”结构处理,而是采用了“合”的方法:将checkNode的uncleNode转化为同一种颜色(在这里是黑色),然后统一处理;经过统一处理后,checkNode的uncleNode就一定是黑色的啦,接下来又要“分”:checkNode是fatherNode的左孩子还是右孩子。之后便是“合”:将checkNode转化为fatherNode的左孩子~;之后就是“大一统”的处理啦。

      这里写图片描述
      图1
      这里写图片描述
      图2
      分析完while循环里的代码(细节的角度),还要讨论while循环在这里的作用~(整体的角度)

      我们可以发现进入while循环时,checkNode一定指向一个红色结点,由于红色结点不影响黑平衡,但是有可能其父结点也是红色的,所以while循环就是在检查checkNode为红色是否恰当以及不恰当时如何处理(调整一次颜色就可以解决还是需要调整多次)~while循环的前部分代码其实处理了“恰当”的情况;后面的代码则处理了“不恰当”的情况。

      最后来一张插入算法的流程示意图:

      这里写图片描述

    2. 红黑树删除算法

      虽然代码量比较大,但是注释清楚呀~

      public BinaryTreeNode
      remove(T newValue) { RedBlackTreeNode
      removeNode=this;//指向逻辑上被删除的结点 RedBlackTreeNode
      parent; RedBlackTreeNode
      p;//removeNode的子结点 int compareResult; while(removeNode!=null){ compareResult=newValue.compareTo(removeNode.value); if(compareResult<0){ removeNode=(RedBlackTreeNode
      )removeNode.leftTree; }else if(compareResult>0){ removeNode=(RedBlackTreeNode
      )removeNode.rightTree; }else{ break; } }//该部分完成对待删除结点的查找 if(removeNode!=null) { //找到了要删除的结点 if (removeNode.leftTree != null && removeNode.rightTree != null) { removeNode.value = ((RedBlackTreeNode
      ) removeNode.rightTree).pollMin();//删除物理上的中序后继,然后赋值完成从逻辑上的删除 } else { parent = (RedBlackTreeNode
      ) removeNode.parent; if (removeNode.leftTree != null) { p = (RedBlackTreeNode
      ) removeNode.leftTree; } else { p = (RedBlackTreeNode
      ) removeNode.rightTree; }//至此,p指向removeNode的子结点,当然p有可能指向虚拟结点 if (parent.value == null) { //removeNode是根结点,子结点就位 parent.leftTree = p; p.isRed = false;//根结点必须为黑的 //函数返回 return this; } else { //removeNode不是根结点 if (parent.isLeftTree(removeNode)) { parent.setLeftTree(p); } else { parent.setRightTree(p); } }//在这里已经从物理上删除了removeNode if (removeNode.isRed) { return this;//删除结点是红色的,没有对红黑树的性质造成影响 } else if (p.isRed) { //p是红的,并且p是removeNode(黑色)的唯一子结点,所以p改为黑色就好 p.isRed = false; return this; } //至此,removeNode是黑的,p也是黑的 RedBlackTreeNode
      q; while (p.value != null && !p.isRed) { //p是黑的并且p不是根结点 if (p.parent.isLeftTree(p)) { //p是左子结点 q = (RedBlackTreeNode
      ) p.parent.rightTree;//q是p的兄弟结点 if (q.isRed) { //如果q是红色的 q.isRed = false;//q改为黑色的 ((RedBlackTreeNode
      ) p.parent).isRed = true;//p的parent改为红色的 parent = leftRotate(parent);//对parent执行左旋 q = (RedBlackTreeNode
      ) parent.rightTree;//q仍然指向p的兄弟结点 }//至此q为黑色的 if ((!isRed((RedBlackTreeNode
      ) q.leftTree)) && !isRed((RedBlackTreeNode
      ) q.rightTree)) { //q的左右孩子都是黑色的 q.isRed = true;//q改为红色的 p = (RedBlackTreeNode
      ) p.parent;//p指向p的父结点,开始下一次检查 } else { if ((!isRed((RedBlackTreeNode
      ) q.rightTree)) && (isRed((RedBlackTreeNode
      ) q.leftTree))) { //p的右儿子是黑的,左儿子是红的 ((RedBlackTreeNode
      ) q.leftTree).isRed = false;//左儿子改黑 q.isRed = true;//q改红 parent = rightRotate(q);//对q右旋 q = (RedBlackTreeNode
      ) parent.rightTree;//q仍然指向p的兄弟结点 }//至此,p是黑的,q是黑色,q的右儿子是红的,q的左儿子是黑的 q.isRed = ((RedBlackTreeNode
      ) (p.parent)).isRed;//父结点的颜色赋予q ((RedBlackTreeNode
      ) (p.parent)).isRed = false;//父结点改为黑色 ((RedBlackTreeNode
      ) q.rightTree).isRed = false;//q的右孩子改为黑色 leftRotate((RedBlackTreeNode
      ) p.parent);//父结点左旋 //结束循环 break; } } else { //对称处理p是右孩子的情况,注释省略,对称处理,思路一致 q = (RedBlackTreeNode
      ) p.parent.leftTree; if (q.isRed) { q.isRed = false; ((RedBlackTreeNode
      ) p.parent).isRed = true; parent = rightRotate(parent); q = (RedBlackTreeNode
      ) parent.leftTree; } if (isRed((RedBlackTreeNode
      ) q.leftTree) && isRed((RedBlackTreeNode
      ) q.rightTree)) { q.isRed = true; p = (RedBlackTreeNode
      ) p.parent; } else { if ((isRed((RedBlackTreeNode
      ) q.rightTree)) && (!isRed((RedBlackTreeNode
      ) q.leftTree))) { ((RedBlackTreeNode
      ) q.rightTree).isRed = false; q.isRed = true; parent = leftRotate(q); q = (RedBlackTreeNode
      ) parent.leftTree; } q.isRed = ((RedBlackTreeNode
      ) (p.parent)).isRed; ((RedBlackTreeNode
      ) (p.parent)).isRed = false; ((RedBlackTreeNode
      ) q.leftTree).isRed = false; rightRotate((RedBlackTreeNode
      ) p.parent); //结束循环 break; } } }//while循环结束 p.isRed = false; }//删除结点结束 }//判空结束 return this; }

      删除算法总结:

      仍然使用代码中的变量:p、parent、q表示相应概念;

      这里写图片描述

      这里写图片描述
      这里写图片描述
      也就是如下图方框的统一
      这里写图片描述
      接下来将对q的子结点的颜色做讨论
      这里写图片描述
      上图表示再次进入while循环
      这里写图片描述
      上图表示,将情况2转变为情况3
      这里写图片描述
      注意,这里蓝色结点表示颜色未确定~接下来是文字叙述思路~

      这一部分代码首先处理了removeNode有两个子结点的情况,转化为pollMin()函数的调用,其实这里pollMin()是我整套API的一个函数,意味着弹出树中最小的一个结点,返回其值,其实pollMin函数相当于在物理上删除结点,而赋值相当于从逻辑上删除结点。值得一提的是,在二叉平衡树的实现中,维护树平衡性质的代码是单独抽出来的,因为pollMin和pollMax函数中也涉及到删除结点并维护平衡。这里removeNode里调用了pollMin(),只需要pollMin函数里再调用一次remove就好,此时remove函数将不再调用pollMin而进入后面的处理代码,从而实现相关功能;(个人觉得,从理解红黑树删除算法的角度来看,代码实现并没有特别大的问题,但是从实际工程的使用角度来看,这份代码实现不是很好,一是因为公共代码,即维护黑平衡的代码并没有抽离出来,造成了函数相互调用,虽然功能ok,但是逻辑有一点点复杂,不好。二是因为红黑树继承了二叉搜索树,所以在赋值过程中存在大量的强制类型转换,考虑到红黑树结点的特点,重构红黑树代码是必然的,但好在这并不影响我们对算法的理解~)。

      接下来就是removeNode最多只有一个结点,即p的情况。

      如果removeNode是根结点,那么其子结点将代替其成为根结点,改为黑色,结束;如果不是,那么从物理上删除removeNode。如果removeNode是红色的,那么并不影响黑平衡性质;如果removeNode是黑色的,但是p是红色的,那么将p改为黑色即可;但是如果p也是黑的,那么就比较麻烦了,因为从根结点到p的路上,少了一个黑结点呀。之后便是while循环维护黑平衡了。while循环内代码分为两部分,分别处理p为左子结点和右子节点的情况;首先把p的兄弟结点统一为黑色,如果是红色,那么通过调整变为黑色;然后如果q的左右结点都为黑色,那么把q改为红色,p指向其父结点,再次进入while循环;如果不是,说明q有一个红色结点,接下来就是把q的孩子结点情况统一为右孩子为红结点,左孩子为黑结点;接下来就是大一统

      分析完while循环里的代码(细节的角度),还要讨论while循环在这里的作用~(整体的角度)

      我们可以发现进入while循环时,checkNode一定指向一个红色结点,由于红色结点不影响黑平衡,但是有可能其父结点也是红色的,所以while循环就是在检查checkNode为红色是否恰当以及不恰当时如何处理(调整一次颜色就可以解决还是需要调整多次)~while循环的前部分代码其实处理了“恰当”的情况;后面的代码则处理了“不恰当”的情况。

      最后来一张插入算法的流程示意图:

      这里写图片描述

你可能感兴趣的文章
git中文安装教程
查看>>
虚拟机 CentOS7/RedHat7/OracleLinux7 配置静态IP地址 Ping 物理机和互联网
查看>>
弱类型、强类型、动态类型、静态类型语言的区别是什么?
查看>>
Struts2技术内幕图书 转载
查看>>
Java异常分类
查看>>
项目中的jackson与json-lib使用比较
查看>>
Jackson Tree Model Example
查看>>
j2ee-验证码
查看>>
日志框架logj的使用
查看>>
js-高德地图规划路线
查看>>
常用js收集
查看>>
mydata97的日期控件
查看>>
如何防止sql注入
查看>>
maven多工程构建与打包
查看>>
springmvc传值
查看>>
Java 集合学习一 HashSet
查看>>
在Eclipse中查看Android源码
查看>>
Android-Socket登录实例
查看>>
Android使用webservice客户端实例
查看>>
层在页面中的定位
查看>>