基于Java实现红黑树的基本操作
Posted 姓码名农小小
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于Java实现红黑树的基本操作相关的知识,希望对你有一定的参考价值。
首先,在阅读文章之前,我希望读者对二叉树有一定的了解,因为红黑树的本质就是一颗二叉树。所以本篇博客中不在将二叉树的增删查的基本操作了,需要了解的同学可以到我之前写的一篇关于二叉树基本操作的博客:https://www.cnblogs.com/rainple/p/9970760.html;
有随机数节点组成的二叉树的平均高度为logn,所以正常情况下二叉树查找的时间复杂度为O(logn)。但是,根据二叉树的特性,在最坏的情况下,比如存储的是一个有序的数据的话,那么所以的数据都会形成一条链,此时二叉树的深度为n,时间复杂度为O(n)。红黑树就是为了解决这个问题的,它能够保证在任何情况下树的深度都保持在logn左右,红黑树通过一下约束来完成这个特性:
1、每个节点不是红色就是黑色。
2、根节点为黑色。
3、每个叶子节点都是黑色的。
4、每个红色节点的子节点都是黑色。
5、任意节点,到其任意叶节点的所有路径都包含相同的黑色节点。
结构如下图:
红黑树的基本操作包括删除和添加。在删除或者添加一个节点的时候就有可能打破原有的红黑树维持的平衡,那么就需要通过着色和旋转的方式来使红黑树重新达到平衡。着色是非常简单的,直接将节点的颜色改变就可以了,多以要理解红黑树,就必须需要懂得如何进行旋转,旋转又分为左旋和右转,两个操作相反的,所以理解了一个旋转的操作就很容易理解另一个旋转了。
左旋:
如图所示,红色节点为旋转支点,支点往左子树移动即为左旋。左旋之后我们可以看到原支点的位置被原支点的右子节点代替,新支点的左子节点变为了原来为父节点的原支点,新支点的左子节点变为原支点的右子节点,因此左旋操作总共右3个节点,以为旋转前的结构举例,分别为红色节点(原支点),黄色节点(新支点)和L节点。Java代码实现如下:
/** * 左旋 * @param e 支点 */ private void leftRotate(Entry<K,V> e){ //支点的右子节点 Entry<K,V> right = e.right; //支点右子节点的左子节点 Entry<K,V> rightOfLeft = right.left; //新旧支点的替换 right.parent = e.parent; if (e.parent == null){ root = right; }else { if (e == e.parent.left) e.parent.left = right; else e.parent.right = right; } //将原支点变为新支点的左节点 right.left = e; e.parent = right; //将新支点的左节点变为就支点的右节点 e.right = rightOfLeft; if (rightOfLeft != null) rightOfLeft.parent = e; }
因为在红黑树中每个节点都有一个指针指向自己的父节点,父节点也有指针指向子节点,因为在改动一个节点的时候都需要分别改动当前节点和父节点的指向,结合左旋的示意图,用Java代码实现起来就不会很困难了。
右旋
右旋操作和左旋相反的,两者互反。依然是红色作为旋转支点,右旋后黄色节点代替了红色节点原来的位置,黄色节点的右节点旋转后变为红色节点的左节点。Java 代码实现如下:
/** * 右旋 * @param e 旋转支点 */ private void rightRotate(Entry<K,V> e){ //原支点的左节点 Entry<K,V> left = e.left; //原支点的左节点的右节点 Entry<K,V> leftOfRight = left.right; //新旧支点的替换 left.parent = e.parent; if (e.parent == null){//支点的父节点为根节点的情况 root = left; }else {//非跟节点 if (e == e.parent.left) e.parent.left = left; else e.parent.right = left; } //将原支点变为新支点的右节点 left.right = e; e.parent = left; //将新支点未旋转前的右节点变为转换后的原支点的左节点 e.left = leftOfRight; if (leftOfRight != null) leftOfRight.parent = e; }
添加节点
首先,在进入主题之前我们再来回顾一下红黑树的5个特点:
1、每个节点不是红色就是黑色。
2、根节点为黑色。
3、每个叶子节点都是黑色的。
4、每个红色节点的子节点都是黑色。
5、任意节点,到其任意叶节点的所有路径都包含相同的黑色节点。
红黑树插入节点与二叉树是一致的,所以每次添加节点肯定是添加到叶子节点上,具体步骤如下:
第一步:将新节点插入到红黑树中。
第二步:将新节点设置为红色。这里为什么需要设置成红色呢?主要是为了满足特性5,这样在插入节点后就少解决了一个冲突,也就少一点麻烦。插入完成后,我们来看一下还有那些特性是有可能发生冲突的,特性1每个节点不是红色就是黑色的,这明显没有冲突,特性2根节点为黑色,当插入节点为根节点的时候就会有冲突了,这种就很简单了,直接将根节点着色为黑色即可。特性3每个叶子节点都是黑色,这个明显没有冲突。特性4每个红色节点的子节点都是黑色的,这个特性就有可能会冲突,因为在插入新节点的时候我们无法确定新节点的父节点的颜色是黑色的还是红色,如果新节点的父节为黑色,那么就不会有冲突,否则就会违背了特性4。特性5任意节点,到其任意子节点的所有路径都包含相同的黑色节点,因为我们插入的新节点被着色为红色,所以并不会影响到每个路径的黑色节点的数量,因此也不会有冲突。综上所诉,那么在插入新节点的时候,只有特性4有可能发生冲突。
第三步:平衡红黑树,使之成为新的红黑树。
根据第二部得到的结论,我们可以知道只有情况是需要解决冲突的,那就是新节点的父节点为红色的时候违背了特性4。接下来我们将要讨论这个问题,因为在新插入一个节点之前是一颗已经平衡了的红黑树,因此根据特新4,新节点的祖父节点必定为黑色。根据这种情况,我们又可以分为以下四种情况:
情况1:新节点为左节点,叔叔节点为红色;
情况2:新节点为左节点,叔叔节点为黑色;
情况3:新节点为右节点,叔叔节点为红色;
情况4:新节点为右节点,叔叔节点为黑色;
情况1和情况3的情况是一样的,所以我们可以将这两种情况看作是一种情况,这个情况我们稍后再讨论,然后看一下情况2和情况4,通过左旋就可以转换成情况2。
综上所述,我们可以归结为3中情况:
情况1:叔叔节点是红色节点;
情况2:叔叔节点是黑色节点,新节点为右节点;
情况3:叔叔节点是黑色节点,新节点为左节点;
上面我也有提到,当插入新节点时肯定是属于第一种情况的,然后2、3由1转换而来,在此之前我希望你之前已经了解过递归的原理和思想,把局部看作整体的思想,因为这将有助于下面讨论的理解。下面我们将要继续分析这三种情况,情况1这种情况处理起来比比较简单,只需要将祖父节点变为红色节点,父节点和叔叔节点变为黑色即可,这仅仅只是当整个红黑树只有这几个节点的时候是可以了,但事实并非如此,这仅仅只是达到了局部平衡。
上图,我们看到已经达到了局部的平衡,但是,我们还会有其他的情况,那就是祖父节点有可能也会有父节点。那么又会有两种情况,1是祖父节点的父节点可能是黑色的,2是可能是红色的,如果黑色那么整个红黑树就达到平衡了。不知道大家根觉到了没有,这两种情况是不是跟新插入一个节点的情况是一致的,是不是又回到了插入新节点的问题了?于是我将局部收到影响的部分画出来,如图:
图a就是将情况1从新着色后的部分受影响的节点,当然只是其中的一种情况,此时我们将已经平衡的部分去掉就变成的图b的情况,这种情况是不是很熟悉呢?我们的祖父节点当成新节点,是不是相当于上面讨论的情况1呢?不过与上面讨论的情况不同的是,这里3中可能情况都可能出现,因为叔叔节点有可能为红色或黑色。所以这时候才有可能出现真正的三种情况:
情况1:叔叔节点是红色节点;
情况2:叔叔节点是黑色节点,新节点为右节点;
情况3:叔叔节点是黑色节点,新节点为左节点;
如果为情况1的话,我们一层一层的往上平衡就可以了,当祖父节点为根节点的时候,我们直接将根节点着色为黑色即可,因为祖父节点的两个子节点都是黑色的,所以变为黑色后仍然是平衡的。接下来我们来讨论下情况2和3。
很明显的,这两种情况的右节点多出了一个黑色节点,这种情况是在情况1向上着色的时候造成的,即祖父节点由黑色节点变为了红色节点。情况2以父节点为支点左旋,然后将父节点和新节点互换可以得到情况3:
情况3进行的操作是,首先将父节点着色为黑色,祖父节点着色为红色,然后以祖父为支点进行右旋
情况3旋转结束后整棵红黑也已经重新恢复平衡了。单从部分其实并看不出已经平衡了,我们可以将三个情况连起来就可以看到了,如下图:
上图中都是以n节点为参考点的,其余无关的节点就不标出来了。n节点即为插入节点,但是除了第一次操作n节点为真正的新节点,此后的操作所指的n节点只是有助于我们的理解把他当成新节点。当然,这只是其中的一种情况,其他其他的情况可以通过不断向上旋转或着色,最终也会达到这种情况或者顶部是p节点为根节点的时候,第二种情况直接将根节点着色为黑色即可。
总结:
回顾一下红黑树的5个特性:
1、节点不是红色就是黑色。
2、根节点为黑色。
3、叶子节点为黑色。
4、每个红色节点其子节点必须是黑色节点。
5、任意节点到到其任意的子节点的所有路径的黑色节点的数量相等。
在插入新节点的时候很显然,不会违背1和3,如果插入的是根节点直接将根节点着色为黑色即可,这种情况可以忽略不计,所以插入节点时可能会违背了4和5,又因为插入的是红色节点因此5也不会违背,最后在插入新节点的时候我们只需要关注特性4就可以了。当父节点为红色的时候跟4有冲突,所以我们接下来讨论的就是这种情况。我们知道,在插入新节点之前整颗红黑树是平衡的,因此可以得出一个结论就是祖父节点肯定肯定是黑色的。我们现在只关注相关的节点即可,目前,我们知道了祖父的节点为黑色,父节点为红色,但是叔叔节点的颜色不知道,新节点的位置也不能确定,所以有2x2中情况,当叔叔节点为红色的时候,两种情况的处理方式是一致的,所以最后我们可以总结为3中情况:
1、叔叔节点为红色
2、新节点为右节点,叔叔节点为黑色
3、新节点为左节点,叔叔节点为黑色
类型 | 描述 | 步骤 | 示意图 |
情况1 | 叔叔节点为红色 |
1、父节点设为黑色 2、叔叔节点设为黑色 3、祖父节点设为红色 4、把祖父节点设置为新节点(当前节点) |
|
情况2 | 新节点为右节点,叔叔节点为黑色 |
1、以父节点为支点左旋 2、父节点和新节点互换位置 3、把父节点设为当前节点 |
|
情况3 | 新节点为左节点,叔叔节点为黑色 |
1、父节点设为黑色 2、祖父节点设为红色 3、以祖父节点为支点右旋 |
|
整合 |
树a就是表中的情况1,通过着色后直接转换成了情况3,情况3进行着色旋转后达到了平衡,当树b中的叔叔节点为红色的时候与树a一致,循环调用树a的处理方式,直至达到树b的情况或者树a中的祖父节点到达了根节点,这时候将祖父节点设为黑色即可。 这种情况就是由情况1转情况2再转情况3,由情况3重新着色旋转后达到平衡。 需要注意的是:不是每次插入节点都会出现3中情况,有可能只出现了2和3,或者只出现了3一种情况。 |
上面是讨论左子树的问题,因为红黑色具有堆成性,因此在处理右子树的时候与处理左子树相反即可。Java代码示例如下:
/** * 插入新节点后平衡红黑树 * @param e 新节点 */ private void fixAfterInsertion(Entry<K, V> e) { //将新插入节点设置为红色 setRed(e); Entry<K,V> p,g,u;//父节点和祖父节点和叔叔节点 Entry<K,V> current = e;//新节点 /** * 这里通过循环不断向上平衡 */ while ((p = parentOf(current)) != null && isRed(p)){ g = parentOf(p);//祖父节点 if (p == g.left){ u = g.right; //情况1:叔叔节点为红色 if (u != null && isRed(u)){ setBlack(p);//父节点设为黑色 setBlack(u);//叔叔节点设为黑色 setRed(g);//祖父节点设为红色 current = g;//把祖父节点设为当前节点 //继续向上平衡 continue; } //情况2:当前节点为右节点,叔叔节点为黑色 if (current == p.right){ leftRotate(p);//父节点为支点左旋 Entry<K,V> tmp = p; p = current;//父节点和当前节点互换 current = tmp;//父节点设为当前节点 } //情况3:当前节点为左节点,叔叔节点为黑色 setBlack(p);//父节点设为黑色 setRed(g);//祖父节点设为红色 rightRotate(g);//祖父节点为支点右旋 }else {//相反的操作 u = g.left; if (u != null && isRed(u)){ setBlack(p); setBlack(u); setRed(g); current = g; continue; } if (current == p.left){ rightRotate(p); Entry<K,V> tmp = p; p = current; current = tmp; } setBlack(p); setRed(g); leftRotate(g); } } //最后将根节点设置为红色 setBlack(root); }
删除节点
在二叉树分析一文中已经说过,删除一个节点的时候有3中情况:
1、删除节点没有子节点
2、删除节点只有一个子节点
3、删除节点有两个子节点
首先,我们逐个来分析每种情况删除节点后对整颗红黑树的平衡性的影响。在删除节点时候红黑树的特性1,2,3肯定不会违背,所以只需要考虑特性4,5即可。
对于情况1,肯定不会违背特性4,如果删除节点为红色,那么对整颗红黑树的平衡性都不会影响,如果是黑色则违背了特性5,我们先将这种情况记录下来,稍后再进一步讨论。
对于情况2,有可能删除的是左子树或右子树,暂且不讨论。如果删除的节点为红色,不影响平衡性,如果删除的是黑色,那么肯定会和特性5有冲突,当删除节点的父节点为红色,子节点为红色是也和特性4有冲突。
对于情况3,其实最后删除的是它的替代节点,根据替代节点的特点,最终其实是回到了1这种情况或者情况2。
总结上面的3种情况可得到一个结论,只有删除节点为黑色时才会破坏红黑树原来的平衡,因在删除节点之前红黑树是出于平衡状态的,删除之后很明显的其兄弟节点分支必然比删除节点的分支多了一个黑色的节点,因此我们只需要改变兄弟节点的颜色即可,我们只讨论左节点,右节点对称。
一、删除节点的兄弟节点是红色
将兄弟节点设为黑色,父节点设为红色,以父节点为支点左旋转,然后将父节点的右节点放到兄弟节点上:
二、兄弟节点是黑色的,兄弟的两个子节点也都是黑色的
兄弟节点设为红色,把父节点设置为新的删除节点:
三、兄弟节点是黑色的,且兄弟节点的左子节点是红色,右子节点是黑色
将兄弟节点的左子节点设为黑色,兄弟节点设为红色,以兄弟节点为支点右旋,把父节点的右节点设置为兄弟节点
四、兄弟节点是黑色的,且兄弟节点的右子节点是红色,左子节点任意颜色
把兄弟节点的设为父节点的颜色,父节点设为黑色,父节点的右节点设为黑色,父节点为支点左旋
删除的Java代码示例:
public V remove(Object key){ if (key == null) return null; Entry<K,V> delEntry; delEntry = getEntry(key); if (delEntry == null) return null; size--; Entry<K,V> p = delEntry.parent; if (delEntry.right == null && delEntry.left == null){ if (p == null){ root = null; }else { if (p.left == delEntry){ p.left = null; }else { p.right = null; } } }else if (delEntry.right == null){//只有左节点 Entry<K,V> lc = delEntry.left; if (p == null) { lc.parent = null; root = lc; } else { if (delEntry == p.left){ p.left = lc; }else { p.right = lc; } lc.parent = p; } }else if (delEntry.left == null){//只有右节点 Entry<K,V> rc = delEntry.right; if (p == null) { rc.parent = null; root = rc; }else { if (delEntry == p.left) p.left = rc; else p.right = rc; rc.parent = p; } }else {//有两个节点,找到后继节点,将值赋给删除节点,然后将后继节点删除掉即可 Entry<K,V> successor = successor(delEntry);//获取到后继节点 boolean color = successor.color; V old = delEntry.value; delEntry.value = successor.value; delEntry.key = successor.key; if (delEntry.right == successor){//后继节点为右子节点, if (successor.right != null) {//右子节点有右子节点 delEntry.right = successor.right; successor.right.parent = delEntry; }else {//右子节点没有子节点 delEntry.right = null; } }else { successor.parent.left = null; } if (color == BLACK) //fixUpAfterRemove(child,parent); return old; } V old = delEntry.value; if (delEntry.color == BLACK)//删除为黑色时,需要重新平衡树 if (delEntry.right != null)//删除节点的子节点只有右节点 fixUpAfterRemove(delEntry.right,delEntry.parent); else if (delEntry.left != null)//删除节点只有左节点 fixUpAfterRemove(delEntry.left,delEntry.parent); else fixUpAfterRemove(null,delEntry.parent); delEntry.parent = null; delEntry.left = null; delEntry.right = null; return old; } private Entry<K, V> getEntry(Object key) { if (key == null) return null; Entry<K, V> delEntry = null; Entry<K, V> current = root; int ret; if (comparator == null){ Comparable<K> k = (Comparable<K>) key; while (current != null){ ret = k.compareTo(current.key); if (ret < 0) current = current.left; else if (ret > 0) current = current.right; else{ delEntry = current; break; } } }else { for (;current != null;){ ret = comparator.compare(current.key, (K) key); if (ret < 0) current = current.left; else if (ret > 0) current = current.right; else{ delEntry = current; break; } } } return delEntry; } //node表示待修正的节点,即后继节点的子节点(因为后继节点被挪到删除节点的位置去了) private void fixUpAfterRemove(Entry<K, V> node,Entry<K,V> parent) { Entry<K,V> other; while((node == null || isBlack(node)) && (node != root)) { if(parent.left == node) { //node是左子节点,下面else与这里的刚好相反 other = parent.right; //node的兄弟节点 if(isRed(other)) { //case1: node的兄弟节点other是红色的 setBlack(other); setRed(parent); leftRotate(parent); other = parent.right; } //case2: node的兄弟节点other是黑色的,且other的两个子节点也都是黑色的 if((other.left == null || isBlack(other.left)) && (other.right == null || isBlack(other.right))) { setRed(other); node = parent; parent = parentOf(node); } else { //case3: node的兄弟节点other是黑色的,且other的左子节点是红色,右子节点是黑色 if(other.right == null || isBlack(other.right)) { setBlack(other.left); setRed(other); rightRotate(other); other = parent.right; } //case4: node的兄弟节点other是黑色的,且other的右子节点是红色,左子节点任意颜色 setColor(other, colorOf(parent)); setBlack(parent); setBlack(other.right); leftRotate(parent); node = this.root; break; } } else { //与上面的对称 other = parent.left; if (isRed(other)) { // Case 1: node的兄弟other是红色的 setBlack(other); setRed(parent); rightRotate(parent); other = parent.left; } if ((other.left==null || isBlack(other.left)) && (other.right==null || isBlack(other.right))) { // Case 2: node的兄弟other是黑色,且other的俩个子节点都是黑色的 setRed(other); node = parent; parent = parentOf(node); } else { if (other.left==null || isBlack(other.left)) { // Case 3: node的兄弟other是黑色的,并且other的左子节点是红色,右子节点为黑色。 setBlack(other.right); setRed(other); leftRotate(other); other = parent.left; } // Case 4: node的兄弟other是黑色的;并且other的左子节点是红色的,右子节点任意颜色 setColor(other, colorOf(parent)); setBlack(parent); setBlack(other.left); rightRotate(parent); node = this.root; break; } } } if (node!=null) setBlack(node); } private Entry<K, V> successor(Entry<K, V> delEntry) { Entry<K,V> r = delEntry.right;//assert r != null; while (r.left != null){ r = r.left; } return r; }
完整的代码示例:
public class MyTreeMap<K,V> { private static final boolean BLACK = true; private static final boolean RED = false; private Entry<K,V> root; private int size = 0; private final Comparator<K> comparator; MyTreeMap(){ comparator =null; } public MyTreeMap(Comparator comparator){ this.comparator = comparator; } public V put(K key,V value){ if (root == null){ root = new Entry<>(key,value,null); size++; return null; }else { int ret = 0; Entry<K,V> p = null; Entry<K,V> current = root; if (comparator == null){ if (key == null) throw new NullPointerException("key = null"); Comparable<K> k = (Comparable<K>) key; while (current != null){ p =current; ret = k.compareTo(current.key); if (ret < 0) current = current.left; else if(ret > 0) current = current.right; else { current.value = value; return current.value; } } }else { do { p = current; ret = comparator.compare(key,current.key); if (ret < 0) current = current.left; else if (ret > 0) current = current.right; else { current.value = value; return value; } }while (current != null); } Entry<K,V> e = new Entry<>(key,value,p); if (ret < 0) p.left = e; else p.right = e; size++; fixAfterInsertion(e); return e.value; } } /** * 插入新节点后平衡红黑树 * @param e 新节点 */ private void fixAfterInsertion(Entry<K, V> e) { //将新插入节点设置为红色 setRed(e); Entry<K,V> p,g,u;//父节点和祖父节点和叔叔节点 Entry<K,V> current = e;//新节点 /** * 这里通过循环不断向上平衡 */ while ((p = parentOf(current)) != null && isRed(p)){ g = parentOf(p);//祖父节点 if (p == g.left){ u = g.right; //情况1:叔叔节点为红色 if (u != null && isRed(u)){ setBlack(p);//父节点设为黑色 setBlack(u);//叔叔节点设为黑色 setRed(g);//祖父节点设为红色 current = g;//把祖父节点设为当前节点 //继续向上平衡 continue; } //情况2:当前节点为右节点,叔叔节点为黑色 if (current == p.right){ leftRotate(p);//父节点为支点左旋 Entry<K,V> tmp = p; p = current;//父节点和当前节点互换 current = tmp;//父节点设为当前节点 } //情况3:当前节点为左节点,叔叔节点为黑色 setBlack(p);//父节点设为黑色 setRed(g);//祖父节点设为红色 rightRotate(g);//祖父节点为支点右旋 }else {//相反的操作 u = g.left; if (u != null && isRed(u)){ setBlack(p); setBlack(u); setRed(g); current = g; continue; } if (current == p.left){ rightRotate(p); Entry<K,V> tmp = p; p = current; current = tmp; } setBlack(p); setRed(g); leftRotate(g); } } //最后将根节点设置为红色 setBlack(root); } public boolean containsKey(Object key){ return getEntry(key) != null; } public Set<Entry<K,V>> entrySet(){ Set<Entry<K,V>> list = new HashSet<>(size + 4); entries(root,list); return list; } private void entries(Entry<K,V> e,Set<Entry<K,V>> list){ if (e != null){ entries(e.left,list); list.add(e); entries(e.right,list); } } public boolean containsValue(V v){ return values().contains(v); }以上是关于基于Java实现红黑树的基本操作的主要内容,如果未能解决你的问题,请参考以下文章