最近断断续续花了一个礼拜的时间去看红黑树算法,关于此算法仍是比较难,由于涉及到诸多场景要考虑,同时接下来咱们要讲解的HashMap、TreeMap等原理都涉及到红黑树算法,因此咱们不得不了解其原理,关于一些基础知识这里再也不讲解,本文参考博文:《http://www.javashuo.com/article/p-yfyjkmtq-gv.html》,参考连接太多文字描述,看过不少关于红黑树的文章,有些越讲越懵逼,有些讲的挺好关键是不说人话(这里不是骂人哈,指的是文章讲解的仍是有点抽象),在这里但愿经过我我的的理解既让阅读本文的您可以充分理解其原理也能彻底快速记住各类场景。html
红黑树是一种自平衡二进制搜索树(BST),红黑树与AVL树相比,AVL树更加平衡,可是它们可能会在插入和删除过程当中引发更多旋转。所以,若是咱们的应用程序涉及许多频繁的插入和删除操做,则应首选红黑树。可是,若是插入和删除操做的频率较低,而搜索操做的频率较高,则AVL树应优先于红黑树。咱们需牢记红黑树的每一个节点所遵循的如下规则算法
(1)每一个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每一个叶子节点是黑色。 [注意:这里叶子节点,是指为空的叶子节点,在算法原理中空用Nil表示,可是在面向对象语言中空都用NULL表示]
(4)若是一个节点是红色的,则它的子节点必须是黑色的(注意:这里指的是不能有两个连续的红色节点)。
(5)从一个节点到该节点的子孙节点的全部路径上包含相同数目的黑节点。数据结构
了解如上规则后,接下来将进入红黑树的插入和删除操做,插入操做还好,最复杂的在于删除操做,莫慌,咱们一步步来,不管是插入仍是删除操做均可能会引发树的再次不平衡即会打破以上红黑树的规则,在进行插入或删除操做时,为使得树再次平衡咱们使用【变色】和【旋转】方法来解决。假如Z为插入节点,在这里咱们作以下命名约定:父亲节点、祖父节点、叔叔节点。好了,接下来咱们首先来看插入操做。优化
一说到插入咱们立马就有了疑惑,根据红黑树规则一来看,每一个节点非红即黑,那么咱们插入的节点究竟是红色仍是黑色呢?若是为黑色将很大可能性会破坏规则五,此时咱们为使树再次平衡将花费很大功夫,可是若是为红色,也颇有可能性破坏以上规则二和四,可是比插入节点为黑色更加易于修复。因此这就是为何插入节点为红色的缘由。因此第一步,咱们执行标准的BST插入且节点颜色为红色,插入操做分为如下四种场景。this
(1)Z是根节点spa
(2)Z的父亲为红色节点、叔叔为红色节点code
(3)Z的父亲为红色节点、叔叔为黑色节点(直线)htm
(4)Z的父亲为红色节点、叔叔为黑色节点(三角形)对象
当Z是根节点时,由于默认插入节点为红色,但根据红黑树规则二根节点为黑色,因此进行变色,直接将红色变为黑色,以下:blog
不区分Z是在其父亲节点左侧或者右侧,也不区分Z的父亲节点是在Z的祖父节点左侧或者右侧都进行以下相同处理操做。
(1) 将“父亲节点”设为黑色。
(2) 将“叔叔节点”设为黑色。
(3) 将“祖父节点”设为“红色”。
(4) 将“祖父节点”设为“当前节点”(红色节点);即,以后继续对“当前节点”进行操做。
或者
根据如上大前提,有的童鞋可能分为Z在其父亲节点左侧和右侧两种状况,这里我采用的是Z、Z的父亲节点、Z的祖父节点在同一条直线上时的两种对称状况,同理以下讲解三角形时也是同样,将Z、Z的父亲节点、Z的祖父节点构成三角形时的两种对称状况,这样在脑海中思考并画一笔是否是会更好理解一点呢。因为对称分为两种状况:
(1)当Z的父亲节点在Z的祖父节点左侧时:【1】将“父亲节点”设置为黑色 【2】将“祖父节点”设置为红色 【3】以“父亲节点”右旋
(2)当Z的父亲节点在Z的祖父节点右侧时:【1】将“父亲节点”设置为黑色 【2】将“祖父节点”设置为红色 【3】以“祖父节点”左旋
或者
(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节点上额外引入一个黑色节点,因此出现双黑,是否是有点疑惑,这说的究竟是什么意思呢,咱们看看以下图来理解将一目了然,那么在红黑树中如何将以下出现的双黑变为单黑的呢?请往下看。
【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)演变成如上右右状况,继续操做
【1】父节点为红色状况(变色)
(1)将Z节点的父亲节点即B节点变为红色(2)将Z节点的兄弟节点即A节点变为红色(3)只需变色:红色+双黑色=单个黑色
【2】父节点为黑色状况(父节点双黑,继续递归)
(1)将Z节点的兄弟节点即A节点变为红色(2)将Z的父亲节点即B节点赋给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; }
本节咱们详细分析了红黑树原理,同时给出了大部分伪代码,为了让看文章的童鞋能立马看的懂,并未作进一步的优化。纸上得来终觉浅,得知此事要躬行,看网上其余人的分析和本身再次进行分析效果可想而知,这篇文章断断续续搞了个把月才出来,在这里我只是经过图解的方式去理解,看到这篇文章的童鞋再结合网上红黑树大量文字的描述估计也可以理解的七七八八了。有了本节的理解,接下来咱们再去分析其余底层实现,必将垂手可得,文中如有叙述不当或错误之处,可在评论中提出。感谢您的阅读,咱们下节再会。