java数据结构和算法06(红黑树)
这一篇我们来看看红黑树,首先说一下我啃红黑树的一点想法,刚开始的时候比较蒙,what?这到底是什么鬼啊?还有这种操作?有好久的时间我都缓不过来,直到我玩了两把王者之后回头一看,好像有点儿意思,所以有的时候碰到一个问题困扰了很久可以先让自己的头脑放松一下,哈哈!
不瞎扯咳,开始今天的正题;
前提:看红黑树之前一定要先会搜索二叉树
1.红黑树的概念
红黑树到底是个什么鬼呢?我最开始也在想这个问题,你说前面的搜索二叉树多牛,各种操作效率也不错,用起来很爽啊,为什么突然又冒出来了红黑树啊?
确实,搜索二叉树一般情况下足够了,但是有个很大的缺陷,向搜索二叉树中插入的数据必须是随机性比较强大的;如果你是插入的顺序是按照一定的顺序的,比如10、9、8、7、6、5、4、3、2、1,你把这十个数据插入到搜索二叉树中你就会看到一个比较有趣的现象;玛德,这二叉树居然变成链表了(此时的链表也可以说是不平衡树),这就意味着变成链表之后就丧失了身为搜索二叉树的所有特性,这就很可怕,而且当这种有顺序的数据很多的时候,就特别坑爹,查询的效率贼慢;
所以就出现了红黑树这种数据结构,可以说这是一种特殊的搜索二叉树,是对搜索二叉树进行改进之后的一种很完美的二叉树,这种数据结构最厉害的就是可以自动调整树的结构,就比如上面这种有顺序的数据插入到红黑树之后,红黑树就会自动的啪啪啪给你一顿调节最后还是一棵正常的搜索二叉树,不会变成链表就对了;
那么就有人要问了,要怎么样才能将一个搜索二叉树变成红黑树呢?
答:这很容易回答,字如其名,你把搜索二叉树的每个节点要么涂成红色要么涂成黑色,使得最后这个二叉树中所有节点只有红黑两种颜色,这就是一个红黑树;
这时还有人要问了,是不是可以随意把搜索二叉树中的节点涂成红色或者黑色呢?
答:emmmm.....你觉得有这么容易么?哪有这么随便的!肯定是要符合一些规则你才能涂啊,而且大佬们已经把这些规则总结出来了,我们只需要记好这些笔记就好了!
下面我们就看看红黑树要满足的规则:
(1):每个节点不是红色就是黑色;
(2):根节点总是黑色;
(3):不能有两个连续的红色节点;
(4):从根节点到每一个叶节点或空子节点的黑色节点的数量一定要相同,这个黑色节点的数量叫做黑色高度,所以这个规则换句话来说就是根节点到每一个叶节点或空子节点的黑色高度相等;
这四个规则很重要,任何红黑树都必须同时满足这四个规则,否则就不是红黑树,前三个很容易,话说第四个的空子节点是什么意思呢?字如其名,就是一个空的节点,里面什么都没有,可以当作一个null节点,比如下图所示,这个其实理解就好,不用在意;
第四条规则为了好理解才从根节点开始的,其实从任意一个节点开始也是一样的;可以拆分为两条,某个节点到该节点每一个叶节点的黑色高度要一样,同时还要该节点到该节点的每一个空子节点的黑色高度要一样;
空子节点的定义为:非叶节点可以接子节点的位置;(注意,有的版本没有这个空子节点这个说法,只是说每一个叶节点(nil)都是黑色的。。。。而且这里的叶节点和之前我们理解的叶节点还不一样,看看下图,但本篇我们还是按照空子节点的这个说法,参考《java数据结构和算法第二版》),理解了之后其实是一样的
我们再看看下面这个我截的图,假如不看那两个空子节点,看起来好像是符合红黑树规则的,但是我们还要判断根节点到每个空子节点的黑色高度是不是一样,结果不一样,于是下图其实违背了规则四;
这里继续说一点东西:
新插入的节点必须是红色的,为什么呢?你想啊,你往一个正常的红黑树中插入一个黑色节点,肯定就会百分之百违反第四规则,这就比较坑,每插入一个节点你都要想办法去调整整个树的颜色和结构,这很影响效率;但是假如你插入的节点是红色的,而且这个红色节点还刚好是插入在一个黑色的叶节点那里,诶呀,舒服,什么都不用动;当然还有可能插入到另一个红色节点下面,所以插入红色节点违反规则的概率是百分之五十,用脚趾头都能想到新插入的节点肯定要是红色的啊!
2.红黑树调整的方式
对了,知不知道计算机中红黑树怎么区分红黑色节点啊?我们不可能真的去给计算机中的节点涂颜色吧。。。。其实我们只需要在节点类中添加一个boolean color的属性即可,color为true表示黑色,false为红色;
我们在插入红色的节点的时候有两种可能:(1)刚好把这个红色节点插入到一个黑色节点下面,这个时候直接添加就好;(2)比较不幸,插入到一个红色节点下面,这个时候就违反规则3,连续两个节点为红色,这个时候我们要对红黑树的颜色和进行一定调整;
我们对不符合规则的红黑树进行调整操作主要是分为两个步骤:改变颜色和旋转
改变颜色就不多说了,看名字就知道,我们重点就看看旋转到底是什么鬼?通过改变一些节点的颜色使得满足红黑树规则;
通常情况下旋转分为左旋(逆时针)和右旋(顺时针),我们就简单看看右旋吧,左旋差不多;注意下图这里的右旋可不是绕着节点a旋转啊,a叫做顶端,这里相当于是把这两个节点之间的路径进行了一个旋转;(这里很像绕着a节点旋转,很抱歉不是绕着a旋转,下面我们看看红黑树中的右旋就看的很明显了。。。)
在红黑树中的右旋,在下图中80是“顶端节点”,经过右旋之后顶端节点变为右子节点,而原来的左子节点50变为顶端节点,这样调整了之后70这个节点就没地方放了,于是我们可以顺着右旋之后的50节点的右子节点找到可以存放的位置,也就是80的左子节点,我们可以把70这个节点的移动看作横向移动;
从右往左看就是左旋,这里就不详说了。。。,
对了,顺便提一下,假如这里的70节点有子节点,那么子节点也会跟着一起移动的;我们把70这个节点叫做80这个顶端节点的内侧子孙节点,把30叫做顶端节点80的外侧子孙节点,这个还是很好理解的,30这个节点在树的靠外面,70这个节点始终都是在中里面。。。。
网上找到两张动态的图可以看看右旋和左旋,可以好好理解这旋转,旋转真的很重要!!!
3.添加红黑树节点
下面我们通过慢慢的添加一个一个节点,看看红黑树当遇到问题的时候是怎么调整的;
(1)
(2)
(3)右图这种情况下根节点左边两层,右边一层,稍微有点不平衡,但是没有违反红黑树规则,于是我们不必在意什么;但是假如一条路径比另外一条路径多两层或者两层以上,这个肯定是会违反红黑树规则的,为什么呢?我也不是怎么清楚可能是经过大佬们无数次试验得出来的结论吧!
(4)
此时我们碰到这种情况,第一感觉是改变10和25的颜色,下图所示,看起来貌似是符合红黑树结构的;但是我们要记住当一条路径多于另外一条路径两层及以上的时候肯定会违反红黑树规则,我们再仔细看看这个图就会发现违反了第四规则中:根节点到空子节点的黑色长度要一样
所以我们可以知道只是单纯的改变颜色肯定是不能满足红黑树规则的,我们还要再进行旋转,我们以25为顶端节点进行右旋,变成了下图,满足条件,ok!
(5).对上面(4)中进行的完善
当我们以为(4)这就完美解决的时候,很抱歉还有另外一种情况,当我们新添加的红色节点在10的右子节点上,下图所示:
这种情况就比较坑爹,肯定不能像(4)中那样右旋,比如我就不信这个邪,我就要右旋,于是结果如下图一所示,我就默默地信了这个邪!
那么我就要换一个方法了,我就想啊,如果我们能把这个图变成(4)中的那样的结构那不就可以直接用(4)中的解决方法了吗?基于这个想法,我们可以先试着以10为顶点节点,和15节点一起进行左旋,如图二所示,然后我们就发现世界原来一切如此美好,后面的就跟(4)中一样了,这里就不多说了;
但是在这里要注意颜色的变换和上面那个有一点不同,(4)中是改变父节点和爷爷节点的颜色,而图二是经过旋转之后也是改变父节点和爷爷节点的颜色,就是相当于旋转之前的当前节点和爷爷节点
现在我们把上面调整方式整理一下(想必大家应该知道爷爷节点的意思吧。。。通常都叫做祖父节点,我就喜欢叫爷爷节点,哈哈):
第一种:假如我们添加的红色节点是添加在黑色节点下,完美;
第二种:假如我们添加的红色节点不小心添加到红色节点下,这里要分为两种情况:
假如是左节点(也可以叫做爷爷节点的外侧子孙),那么就改变父节点和爷爷节点的颜色,并且以爷爷节点为顶端节点进行右旋,就ok了;
假如是右节点(也可以叫做爷爷节点的内侧子孙) ,那么就改变当前节点和爷爷节点的颜色,然后要以父节点为顶端节点进行左旋,再绕爷爷节点右旋;
小知识:怎么快速的判断一个节点是不是它爷爷节点的外侧子孙还是内侧子孙呢?你要看当前节点和父节点是不是在同侧,同侧的就是外侧节点,不同侧就是内侧节点;举个例子,假如当前节点的父节点是左节点,当前节点也是属于左节点,都是左边,那当前节点就是其爷爷节点的外侧节点,如果当前节点是右节点,那就是内侧子孙。。。
(6).对上面(5)中进一步的完善
╮( ̄▽ ̄")╭,是不是觉得各种补充的内容啊,哈哈哈,正常!这是最后一个补充了。。。
说出来你们可能不信,上面的(4)和(5)其实都是针对在节点插入之后导致树不平衡而做出的调整,但是会有点小问题,就比如在(4)中,假如50这个节点不是根节点而是一个普通的红色节点,那么在我们首先进行颜色变换的时候就会出现问题,例如下图,那么我们后面的所谓右旋也就没啥用了,所以我们要解决一下这种隐患,最好是在插入数据之前首先对红黑树中的这种有隐患的节点首先进行颜色调整或者旋转;
那么肯定有人要问了,卧槽!这该怎么做啊?我不会呀,怎么办?
答:不会才正常啊,才能显示那些大佬很牛啊!我们只需要在插入节点之前对树的一些有隐患的结构进行调整即可(颜色调整和旋转),调整这个有隐患的结构是为了让我们后续的插入节点更加方便;
6.1.颜色调整:红黑树中我们要插入节点,其实是和搜索二叉树一样,从根节点开始一个一个的比较节点数值大小,小就继续和左子节点比较.....最终肯定可以找到确定的位置,在这个找的过程中,假如一个节点为黑色,它的两个子节点都为红色,这就是一种有隐患的结构,我们需要将父节点和两个子节点颜色都改变一下,下图所示:
6.2.旋转:在6.1中虽然对这样的结构进行了颜色的改变,但是有个小缺陷,假如10节点的父节点是红色的呢?那么我们这样改变颜色也是不符合红黑树规则三(不能有连续的两个红节点)的,于是我们还要进行旋转操作,而旋转操作的的话,无非还是上面说的那两种,外侧子孙和内侧子孙;
注意:这里的内侧子孙和外侧子孙,不是指新插入的节点,而是两个连续红色节点中的子节点。。。。也就是下面第二个图中的节点10就是爷爷节点50的外侧子孙
这样说起来比较抽象,我们来实际看看两个例子就ok了;
外侧子孙:假如在向下查找插入点的途中找到了如下结构:
对红黑树的调整就结束了,有没有发现经过这种调整之后使得后续插入红色节点就容易了很多,而且纵观整棵红黑树,红色节点在慢慢向上运动,直到根节点也被调整成红色,最终我们只需要把根节点变成黑色就好! 插入节点2就不用多说了吧!
内侧子孙:这个我是在不想说了,偷个懒,嘿嘿嘿!其实和前面一样的,就是先改变两个连续红节点的子节点和爷爷节点的颜色,然后绕父节点左旋,最后绕爷爷节点右旋,换汤不换药;
4.总结重点
我们把这篇的重点提出来,其实就是分为插入前和插入后两步:
(1).插入前我们必须调整一下有隐患的结构,具体操作:当一个黑色节点有两个红色节点的时候,我们就改变这三个节点的颜色,红变黑,黑变红;但是由于这个黑色节点的父节点可能是黑色,也可能是红色
当黑色节点的父节点是黑色的时候,那么这个改变颜色不会造成任何影响
当黑色节点的父节点是红色的时候,改变颜色之后就会违反红黑树规则三,有连续的两个红色节点,我们就需要进行旋转,对于旋转,我们有两种情况
第一种,假如两个连续的红色节点的子节点是外侧子孙,那么就先改变父节点和爷爷节点的颜色,然后以这个外侧子孙的爷爷节点进行右旋
第二种,假如两个连续的红色节点的子节点是内侧子孙,那么就先改变内侧子孙和爷爷节点的颜色,然后先绕内侧子孙的父节点进行左旋,最后绕爷爷节点右旋;
(2)插入节点之后,假如插入的是黑色节点下面,那没有什么改变;假如是插入在红色节点之下,那么就会违反红黑树规则三,两个连续的红色节点,此时就会有两种调整方式:
第一种,假如这个新插入的节点是外侧子孙,那么改变父节点和爷爷节点的颜色,然后绕着爷爷节点进行右旋
第二种,假如这个新插入的节点是内侧子孙,那么改变当前插入节点和爷爷节点的颜色,再绕着父节点左旋,再绕着爷爷节点右旋
5.代码
看看前面的逻辑贼多,所以代码的话最好心里准备,下面我们就用java代码来看一下红黑树添加节点的过程;
为什么暂时不说删除红黑树节点呢?因为删除节点有点儿复杂,后面有时间再说吧!而且删除的分为真正的删除和伪删除,真正的删除就是慢慢研究每一个删除的步骤每一步代码,从树中删除节点;而伪删除其实就是在节点类中加一个boolean变量,标识该节点是否为已删除节点,伪删除其实避免了删除红黑树的全部复杂的逻辑,很容易,但是缺陷也很大,因为删除的节点还保存在树中。。。
emmm....本来想自己实现一下的,然而看到一些大佬的博客实现代码,瞬间感觉自己的代码很丑陋,就借用一下大佬的代码;
节点类
public class rbtree<t extends comparable<t>> { private rbtnode<t> mroot; // 根结点 private static final boolean red = false; private static final boolean black = true; public class rbtnode<t extends comparable<t>> { boolean color; // 颜色 t key; // 关键字(键值) rbtnode<t> left; // 左孩子 rbtnode<t> right; // 右孩子 rbtnode<t> parent; // 父结点 public rbtnode(t key, boolean color, rbtnode<t> parent, rbtnode<t> left, rbtnode<t> right) { this.key = key; this.color = color; this.parent = parent; this.left = left; this.right = right; } } ... }
右旋
/* * 对红黑树的节点(y)进行右旋转 * * 右旋示意图(对节点y进行左旋): * py py * / / * y x * / \ --(右旋)-. / \ # * x ry lx y * / \ / \ # * lx rx rx ry * */ private void rightrotate(rbtnode<t> y) { // 设置x是当前节点的左孩子。 rbtnode<t> x = y.left; // 将 “x的右孩子” 设为 “y的左孩子”; // 如果"x的右孩子"不为空的话,将 “y” 设为 “x的右孩子的父亲” y.left = x.right; if (x.right != null) x.right.parent = y; // 将 “y的父亲” 设为 “x的父亲” x.parent = y.parent; if (y.parent == null) { this.mroot = x; // 如果 “y的父亲” 是空节点,则将x设为根节点 } else { if (y == y.parent.right) y.parent.right = x; // 如果 y是它父节点的右孩子,则将x设为“y的父节点的右孩子” else y.parent.left = x; // (y是它父节点的左孩子) 将x设为“x的父节点的左孩子” } // 将 “y” 设为 “x的右孩子” x.right = y; // 将 “y的父节点” 设为 “x” y.parent = x; }
左旋
/* * 对红黑树的节点(x)进行左旋转 * * 左旋示意图(对节点x进行左旋): * px px * / / * x y * / \ --(左旋)-. / \ # * lx y x ry * / \ / \ * ly ry lx ly * * */ private void leftrotate(rbtnode<t> x) { // 设置x的右孩子为y rbtnode<t> y = x.right; // 将 “y的左孩子” 设为 “x的右孩子”; // 如果y的左孩子非空,将 “x” 设为 “y的左孩子的父亲” x.right = y.left; if (y.left != null) y.left.parent = x; // 将 “x的父亲” 设为 “y的父亲” y.parent = x.parent; if (x.parent == null) { this.mroot = y; // 如果 “x的父亲” 是空节点,则将y设为根节点 } else { if (x.parent.left == x) x.parent.left = y; // 如果 x是它父节点的左孩子,则将y设为“x的父节点的左孩子” else x.parent.right = y; // 如果 x是它父节点的左孩子,则将y设为“x的父节点的左孩子” } // 将 “x” 设为 “y的左孩子” y.left = x; // 将 “x的父节点” 设为 “y” x.parent = y; }
插入节点
/* * 将结点插入到红黑树中 * * 参数说明: * node 插入的结点 // 对应《算法导论》中的node */ private void insert(rbtnode<t> node) { int cmp; rbtnode<t> y = null; rbtnode<t> x = this.mroot; // 1. 将红黑树当作一颗二叉查找树,将节点添加到二叉查找树中。 while (x != null) { y = x; cmp = node.key.compareto(x.key); if (cmp < 0) x = x.left; else x = x.right; } node.parent = y; if (y!=null) { cmp = node.key.compareto(y.key); if (cmp < 0) y.left = node; else y.right = node; } else { this.mroot = node; } // 2. 设置节点的颜色为红色 node.color = red; // 3. 将它重新修正为一颗二叉查找树 insertfixup(node); } /* * 新建结点(key),并将其插入到红黑树中 * * 参数说明: * key 插入结点的键值 */ public void insert(t key) { rbtnode<t> node=new rbtnode<t>(key,black,null,null,null); // 如果新建结点失败,则返回。 if (node != null) insert(node); } /* * 红黑树插入修正函数 * * 在向红黑树中插入节点之后(失去平衡),再调用该函数; * 目的是将它重新塑造成一颗红黑树。 * * 参数说明: * node 插入的结点 // 对应《算法导论》中的z */ private void insertfixup(rbtnode<t> node) { rbtnode<t> parent, gparent; // 若“父节点存在,并且父节点的颜色是红色” while (((parent = parentof(node))!=null) && isred(parent)) { gparent = parentof(parent); //若“父节点”是“祖父节点的左孩子” if (parent == gparent.left) { // case 1条件:叔叔节点是红色 rbtnode<t> uncle = gparent.right; if ((uncle!=null) && isred(uncle)) { setblack(uncle); setblack(parent); setred(gparent); node = gparent; continue; } // case 2条件:叔叔是黑色,且当前节点是右孩子 if (parent.right == node) { rbtnode<t> tmp; leftrotate(parent); tmp = parent; parent = node; node = tmp; } // case 3条件:叔叔是黑色,且当前节点是左孩子。 setblack(parent); setred(gparent); rightrotate(gparent); } else { //若“z的父节点”是“z的祖父节点的右孩子” // case 1条件:叔叔节点是红色 rbtnode<t> uncle = gparent.left; if ((uncle!=null) && isred(uncle)) { setblack(uncle); setblack(parent); setred(gparent); node = gparent; continue; } // case 2条件:叔叔是黑色,且当前节点是左孩子 if (parent.left == node) { rbtnode<t> tmp; rightrotate(parent); tmp = parent; parent = node; node = tmp; } // case 3条件:叔叔是黑色,且当前节点是右孩子。 setblack(parent); setred(gparent); leftrotate(gparent); } } // 将根节点设为黑色 setblack(this.mroot); }
参考大佬博客:https://www.cnblogs.com/skywang12345/p/3624343.html