红黑树算法原理
前言
最近断断续续花了一个礼拜的时间去看红黑树算法,关于此算法还是比较难,因为涉及到诸多场景要考虑,同时接下来我们要讲解的HashMap、TreeMap等原理都涉及到红黑树算法,所以我们不得不了解其原理,关于一些基础知识这里不再讲解,本文参考博文:《https://www.cnblogs.com/aspirant/p/9084199.html》,参考链接太多文字描述,看过很多关于红黑树的文章,有些越讲越懵逼,有些讲的挺好关键是不说人话(这里不是骂人哈,指的是文章讲解的还是有点抽象),在这里希望通过我个人的理解既让阅读本文的您能够充分理解其原理也能完全快速记住各种场景。
红黑树原理
红黑树是一种自平衡二进制搜索树(BST),红黑树与AVL树相比,AVL树更加平衡,但是它们可能会在插入和删除过程中引起更多旋转。因此,如果我们的应用程序涉及许多频繁的插入和删除操作,则应首选红黑树。但是,如果插入和删除操作的频率较低,而搜索操作的频率较高,则AVL树应优先于红黑树。我们需牢记红黑树的每个节点所遵循的以下规则
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点是黑色。 [注意:这里叶子节点,是指为空的叶子节点,在算法原理中空用Nil表示,但是在面向对象语言中空都用NULL表示]
(4)如果一个节点是红色的,则它的子节点必须是黑色的(注意:这里指的是不能有两个连续的红色节点)。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
了解如上规则后,接下来将进入红黑树的插入和删除操作,插入操作还好,最复杂的在于删除操作,莫慌,我们一步步来,无论是插入还是删除操作都可能会引起树的再次不平衡即会打破以上红黑树的规则,在进行插入或删除操作时,为使得树再次平衡我们使用【变色】和【旋转】方法来解决。假如Z为插入节点,在这里我们做如下命名约定:父亲节点、祖父节点、叔叔节点。好了,接下来我们首先来看插入操作。
红黑树插入
一说到插入我们立马就有了疑惑,根据红黑树规则一来看,每个节点非红即黑,那么我们插入的节点到底是红色还是黑色呢?如果为黑色将很大可能性会破坏规则五,此时我们为使树再次平衡将花费很大功夫,但是如果为红色,也很有可能性破坏以上规则二和四,但是比插入节点为黑色更加易于修复。所以这就是为什么插入节点为红色的原因。所以第一步,我们执行标准的BST插入且节点颜色为红色,插入操作分为以下四种场景。
(1)Z是根节点
(2)Z的父亲为红色节点、叔叔为红色节点
(3)Z的父亲为红色节点、叔叔为黑色节点(直线)
(4)Z的父亲为红色节点、叔叔为黑色节点(三角形)
Z是根节点
当Z是根节点时,因为默认插入节点为红色,但根据红黑树规则二根节点为黑色,所以进行变色,直接将红色变为黑色,如下:
Z的父亲为红色节点、叔叔为红色节点
不区分Z是在其父亲节点左侧或者右侧,也不区分Z的父亲节点是在Z的祖父节点左侧或者右侧都进行如下相同处理操作。
(1) 将“父亲节点”设为黑色。
(2) 将“叔叔节点”设为黑色。
(3) 将“祖父节点”设为“红色”。
(4) 将“祖父节点”设为“当前节点”(红色节点);即,之后继续对“当前节点”进行操作。
或者
Z的父亲为红色节点、叔叔为黑色节点(直线)
根据如上大前提,有的童鞋可能分为Z在其父亲节点左侧和右侧两种情况,这里我采用的是Z、Z的父亲节点、Z的祖父节点在同一条直线上时的两种对称情况,同理如下讲解三角形时也是一样,将Z、Z的父亲节点、Z的祖父节点构成三角形时的两种对称情况,这样在脑海中思考并画一笔是不是会更好理解一点呢。由于对称分为两种情况:
(1)当Z的父亲节点在Z的祖父节点左侧时:【1】将“父亲节点”设置为黑色 【2】将“祖父节点”设置为红色 【3】以“父亲节点”右旋
(2)当Z的父亲节点在Z的祖父节点右侧时:【1】将“父亲节点”设置为黑色 【2】将“祖父节点”设置为红色 【3】以“祖父节点”左旋
或者
Z的父亲为红色节点、叔叔为黑色节点(三角形)
(1)当Z的父亲节点在Z的祖父节点左侧时:【1】将“父亲节点”左旋 【2】将“父亲节点”设置为当前节点(即如下A节点)【3】演变为如上直线第1种情况,继续操作
(2)当Z的父亲节点在Z的祖父节点右侧时:【1】将“父亲节点”右旋 【2】将“父亲节点”设置为当前节点(即如下A节点)【3】演变为如上直线第2种情况,继续操作
或者
数据结构定义
首先我们需要定义节点元素,每一个节点有左孩子、右孩子、父亲节点、节点颜色和存储的元素,所以我们对节点进行如下定义:
class RedBlackNode<T extends Comparable<T>> { //黑色节点 public static final int BLACK = 0; //红色节点 public static final int RED = 1; //元素 public T key; //父节点 RedBlackNode<T> parent; //左孩子 RedBlackNode<T> left; //右孩子 RedBlackNode<T> right; //节点颜色 public int color; RedBlackNode(){ color = BLACK; parent = null; left = null; right = null; } RedBlackNode(T key){ this(); this.key = key; } }
接下来是定义红黑树,关于左旋和右旋方法就不给出了,纸上画两笔就能搞定的事情,我们简单进行如下定义
public class RedBlackTree<T extends Comparable<T>> { private RedBlackNode<T> root = null; private void rotateLeft(RedBlackNode<T> x) { } private void rotateRight(RedBlackNode<T> x) { } }
插入伪代码
当进行插入操作时,我们需要明确插入节点的具体位置,也就是说我们需要查找插入节点的父亲节点、左孩子和右孩子且默认插入节点为红色,最后通过变色或旋转来进行修复,如下:
private void insert(RedBlackNode<T> z) { RedBlackNode<T> y = null; RedBlackNode<T> x = root; //若根节点不为空,则循环查找插入节点的父节点 while (!isNull(x)) { y = x; // 如果元素值小于当前元素值则从左孩子继续查找 if (z.key.compareTo(x.key) < 0) { x = x.left; } // 如果元素值小于当前元素值则从右孩子继续查找 else { x = x.right; } } // 以y作为z的父亲节点 z.parent = y; // 若父亲节点为空,说明插入节点为根节点 if (isNull(y)) root = z; else if (z.key.compareTo(y.key) < 0) y.left = z; else y.right = z; z.left = null; z.right = null; z.color = RedBlackNode.RED; insertFixup(z); }
接下来则是实现上述插入修复方法,上述我们分析插入操作几种的情况的前提是插入节点的父亲节点为红色,所以这里我们通过循环插入节点的父亲节点若为红色来进行修复,同时呢,无论是插入还是删除都是有其对称情况,也就是说我们可将插入和删除的节点分为是在其父亲节点的左侧还是右侧两种大的情况,毫无疑问这两种操作将必定对称,最浅显易懂的插入修复方法如下(已加上注释,可再次借助于上述分析来看)
private void insertFixup(RedBlackNode<T> z) { RedBlackNode<T> y = null; while (z.parent.color == RedBlackNode.RED) { //如果Z的父亲节点在Z祖父节点左侧 if (z.parent == z.parent.parent.left) { //定义Z的父亲兄弟节点 y = z.parent.parent.right; //如果y是红色 if (y.color == RedBlackNode.RED) { //z的父亲变为黑色 z.parent.color = RedBlackNode.BLACK; //y变为黑色 y.color = RedBlackNode.BLACK; //z的祖父变为红色 z.parent.parent.color = RedBlackNode.RED; //将z的祖父作为z z = z.parent.parent; } // 如果y是黑色且z是右孩子 else if (z == z.parent.right) { // 将z的父亲作为z z = z.parent; //以z的父亲节点进行左旋 rotateLeft(z); } // 否则如果y黑色且z是左孩子 else { //z的父亲变为黑色 z.parent.color = RedBlackNode.BLACK; //z的祖父变为红色 z.parent.parent.color = RedBlackNode.RED; //以z的祖父右旋 rotateRight(z.parent.parent); } } // 如果Z的父亲节点在Z祖父节点右侧 else { // 定义Z的父亲兄弟节点 y = z.parent.parent.left; // 如果y是红色 if (y.color == RedBlackNode.RED) { //z的父亲变为黑色 z.parent.color = RedBlackNode.BLACK; //y变为黑色 y.color = RedBlackNode.BLACK; //z的祖父变为红色 z.parent.parent.color = RedBlackNode.RED; //以z的父亲节点进行左旋 z = z.parent.parent; } // 如果y是黑色且z是左孩子 else if (z == z.parent.left) { // 将z的父亲作为z z = z.parent; //以z的父亲节点进行右旋 rotateRight(z); } //否则如果y黑色且z是右孩子 else { //z的父亲变为黑色 z.parent.color = RedBlackNode.BLACK; //z的祖父变为红色 z.parent.parent.color = RedBlackNode.RED; //以z的祖父左旋 rotateLeft(z.parent.parent); } } } // 操作完毕后,根节点重新变为黑色 root.color = RedBlackNode.BLACK; }
红黑树删除
在上述插入操作中,我们主要是检查叔叔的颜色从而考虑不同的情况,也就是说插入后违反的主要是两个连续的红色。在删除操作中,我们检查同级的颜色也就是说检查兄弟节点的颜色从而考虑不同的情况,删除主要违反的属性是子树中黑色高度的更改,因为删除黑色节点可能会导致根到叶路径的黑色高度降低,换言之就是破坏了红黑树规则五,那么我们到底应该如何删除呢?执行标准的BST删除,当我们在BST中执行标准删除操作时,我们最终总是删除一个叶子节点或只有一个孩子的节点(对于内部节点,我们复制后继节点,然后递归调用删除后继节点,后继节点始终是叶节点或一个有一个孩子的节点),因此,我们只需要处理一个节点为叶子或有一个孩子的情况,因为删除是一个相当复杂的过程,为了理解删除,我们引入双重黑色的概念,当删除黑色节点并用黑色子节点替换时,该子节点被标记为double black,此时黑色的高度将不变,所以对于删除我们主要的任务就是将双黑色转换为单黑色即可。好像听起来感觉还是一脸懵逼,莫慌,接下来我依然将用详细的图解给大家讲解到底双黑是怎样的一个神奇存在。删除操作总的来说分为以下三种情况:
(1) 被删除节点没有儿子,即为叶节点。(直接删除)
(2) 被删除节点只有一个儿子。(直接删除该节点,并用该节点的唯一子节点顶替它的位置)
(3) 被删除节点有两个儿子。
以上第一和第二种情况就不用我多讲,对于第三种情况就涉及到上述我们引入的双黑的概念,参考链接中这样描述:比如删除节点v(黑色),则将后继节点u占据v节点,由于删除节点u为黑色,所以导致经过此节点的黑色节点数目减少了一个,为了解决这个问题,我们将占据的v节点额外引入一个黑色节点,虽然这样解决了红黑树规则五的问题,但是我们知道红黑树规则一为每个节点非红即黑,所以破坏了规则一,然后我们通过变色或旋转解决。我们将占据u节点上额外引入一个黑色节点,所以出现双黑,是不是有点疑惑,这说的到底是什么意思呢,我们看看如下图来理解将一目了然,那么在红黑树中如何将如下出现的双黑变为单黑的呢?请往下看。
在当前节点Z为双黑且不是根节点时
【1】左左情况(A节点是其父节点的左节点,C是A的左节点)
(1)将Z兄弟节点即A节点的左孩子变为黑色(2)以Z的父亲节点即B节点进行右旋(注:我们将看到右旋时D节点将搭接到B节点上,此时将Z节点上的双黑给出一个黑色节点来让D进行搭接,最终双黑演变成单黑)
【2】 右右情况(A节点是其父节点的右节点,C是A的右节点)
(1)将Z兄弟节点即A节点的右孩子变为黑色(2)以Z的父亲节点即B节点进行左旋(注:我们将看到左旋时D节点将搭接到B节点上,此时将Z节点上的双黑给出一个黑色节点来让D进行搭接,最终双黑演变成单黑)
【3】左右情况(A节点是其父节点的左节点,C是A的右节点)
(1)将Z兄弟节点即A节点变为红色(2)将Z兄弟节点即A节点的右孩子变为黑色(3)以Z的兄弟节点A进行左旋(4)演变成如上左左情况,继续操作
【4】右左情况(A节点是其父节点的右节点,C是A的左节点)
(1)将Z兄弟节点即A节点变为红色(2)将Z兄弟节点即A节点的左孩子变为黑色(3)以Z的兄弟节点A进行右旋(4)演变成如上右右情况,继续操作
在当前节点Z兄弟节点为黑色节点且孩子为黑色节点时
【1】父节点为红色情况(变色)
(1)将Z节点的父亲节点即B节点变为红色(2)将Z节点的兄弟节点即A节点变为红色(3)只需变色:红色+双黑色=单个黑色
【2】父节点为黑色情况(父节点双黑,继续递归)
(1)将Z节点的兄弟节点即A节点变为红色(2)将Z的父亲节点即B节点赋给Z节点,继续进行递归操作
在当前节点Z兄弟节点为红色节点时
【1】Z节点的兄弟节点即A节点在Z节点的父亲节点左边情况
(1)将Z节点的兄弟节点即A节点变为黑色(2)将Z的父亲节点即B节点变为黑色(3)以Z节点的父亲节点即B节点进行右旋(4)演变成上述父亲节点为红色情况,继续操作
【2】Z节点的兄弟节点即A节点在Z节点的父亲节点右边情况
(1)将Z节点的兄弟节点即A节点变为黑色(2)将Z的父亲节点即B节点变为黑色(3)以Z节点的父亲节点即B节点进行左旋(4)演变成上述父亲节点为红色情况,继续操作
删除伪代码
对于删除操作,首先我们需要查找到需要删除的节点 ,如我们所分析的那样,若删除节点孩子只有其一直接删除即可,若存在两个孩子,除了找到后继执行标准的删除操作外,还需进行删除修复操作,如下:
public void remove(RedBlackNode<T> v) { RedBlackNode<T> z = search(v.key); RedBlackNode<T> x = null; RedBlackNode<T> y = null; //如果z的孩子之一为null,则必须删除z if (isNull(z.left) || isNull(z.right)) y = z; //否则我们需要删除z的后继 else y = findSuccessor(z); // 令x为y的左或右的孩子(y只能有一个子代) if (!isNull(y.left)) x = y.left; else x = y.right; // 设置y的父亲是x的父亲 x.parent = y.parent; // 如果y的父亲节点是null,说明x就是根节点 if (isNull(y.parent)) root = x; //如果y是左孩子,设置x是y的左兄弟 else if (!isNull(y.parent.left) && y.parent.left == y) y.parent.left = x; //如果y是右孩子,设置x是y的右兄弟 else if (!isNull(y.parent.right) && y.parent.right == y) y.parent.right = x;
// 如果y是黑色,则违反红黑树规则需修复 if (y.color == RedBlackNode.BLACK) removeFixup(x); }
private void removeFixup(RedBlackNode<T> x) { RedBlackNode<T> w; // 当删除节点不是根节点且为黑色时 while (x != root && x.color == RedBlackNode.BLACK) { //如果x在其父亲节点左侧 if (x == x.parent.left) { //定义x的兄弟节点 w = x.parent.right; //w是红色时 if (w.color == RedBlackNode.RED) { //w变为黑色 w.color = RedBlackNode.BLACK; //x的父亲变为红色 x.parent.color = RedBlackNode.RED; //以x的父亲左旋 rotateLeft(x.parent);
w = x.parent.right; } //w两个孩子都是黑色时 if (w.left.color == RedBlackNode.BLACK && w.right.color == RedBlackNode.BLACK) { //w变为黑色 w.color = RedBlackNode.RED; //x的父亲作为x x = x.parent; } else { // w的右孩子为黑色时 if (w.right.color == RedBlackNode.BLACK) { //w的左孩子变为黑色 w.left.color = RedBlackNode.BLACK; //w变为红色 w.color = RedBlackNode.RED; //以w右旋 rotateRight(w); //重新将x的父亲右侧孩子赋给w w = x.parent.right; } // w是黑色,右黑子为红色时 //w变为x父亲的颜色 w.color = x.parent.color; //x的父亲变为黑色 x.parent.color = RedBlackNode.BLACK; //w的右孩子变为黑色 w.right.color = RedBlackNode.BLACK; //以x的父亲左旋 rotateLeft(x.parent);
x = root; } } //如果x在其父亲节点右侧 else { //定义x的兄弟节点 w = x.parent.left; //w是红色时 if (w.color == RedBlackNode.RED) { //w变为黑色 w.color = RedBlackNode.BLACK; //x的父亲变为红色 x.parent.color = RedBlackNode.RED; //以x的父亲右旋 rotateRight(x.parent); //重新将x的父亲左侧孩子赋给w w = x.parent.left; } //w两个孩子都是黑色时 if (w.right.color == RedBlackNode.BLACK && w.left.color == RedBlackNode.BLACK) { //w变为黑色 w.color = RedBlackNode.RED; //x的父亲作为x x = x.parent; } else { // w的右孩子为黑色时 if (w.left.color == RedBlackNode.BLACK) { //w的左孩子变为黑色 w.right.color = RedBlackNode.BLACK; //w变为红色 w.color = RedBlackNode.RED; //以w左旋 rotateLeft(w); w = x.parent.left; } // w是黑色,左黑子为红色时 //w变为x父亲的颜色 w.color = x.parent.color; //x的父亲变为黑色 x.parent.color = RedBlackNode.BLACK; //w的左孩子变为黑色 w.left.color = RedBlackNode.BLACK; //以x的父亲右旋 rotateRight(x.parent); x = root; } } } // 操作完毕后,x节点重新变为黑色 x.color = RedBlackNode.BLACK; }
总结
本节我们详细分析了红黑树原理,同时给出了大部分伪代码,为了让看文章的童鞋能立马看的懂,并未做进一步的优化。纸上得来终觉浅,得知此事要躬行,看网上其他人的分析和自己再次进行分析效果可想而知,这篇文章断断续续搞了个把月才出来,在这里我只是通过图解的方式去理解,看到这篇文章的童鞋再结合网上红黑树大量文字的描述估计也能够理解的七七八八了。有了本节的理解,接下来我们再去分析其他底层实现,必将轻而易举,文中若有叙述不当或错误之处,可在评论中提出。感谢您的阅读,我们下节再会。