红黑树(RBTree)是一种相比平衡二叉树(AVL)平衡要求较低的的一种二叉搜索树,所谓平衡要求较低的意思是相比AVL树的每个节点的左右子树的高度差不能超过2,红黑树使用红黑两种颜色来标记二叉搜索树中的节点,并对这种着色进行限制,使得在插入删除操作后对不符合的情况必须进行调整来保持这样一种限制,从而实现自我平衡。我们先来看一下红黑树的定义,也就是着色限制
* 每个节点必须着色成红色或者黑色
* 根节点是黑色的
* 空节点是黑色的
* 父子节点之间不能出现两个连续的红色节点
* 每个节点到其后代叶子节点的所有路径上均包含相同数目的黑色节点(一个节点出发的所有路径的黑高相同)
通过这样的限制,红黑树可以实现没有一条路径会比最短路径长出两倍,相比AVL树来说平衡要求较低,这种低要求换来的是更高效的调整操作,红黑树可能仅需要进行局部的调整,而AVL树必须要回溯到根节点进行信息更新,这是红黑树的优势,并且红黑树的统计效率比AVL树好,有大量的工程项目已经在实际的使用这种数据结构存储数据,例如Java 8中的HashMap在处理冲突的时候就采用了红黑树代替了原来的链表以加快查询速度。
节点的定义
除了常规的左右子节点,因为我们在插入删除的时候经常需要进行查找当前节点的父节点,所以我们需要一个父节点信息,然后还有一个颜色信息,我们只需要一个Byte就可以指示当前节点是红色还是黑色,红色我们用0来表示,黑色用1来表示,其节点信息如下
public class RBNode<T> {
T element;
RBNode<T> left;
RBNode<T> right;
RBNode<T> parent;
byte color;//0为红色,1为黑色
public RBNode(T x) {
this.element=x;
}
}
另外我们还在RBTree构造方法里初始化了一个全局的nullNode,颜色为黑色,目的是简化繁杂的判空操作,它可以像正常节点一样,只是它的作用仅限于一下两点:
* root的父节点指向nullNode
* 树中节点原来指向空指针的地方用nullNode代替
nullNode自身的left,right,parent可以指向任何地方,可以在有需要的时候进行分配,也可以弃之不用,关于nullNode使用到的地方我会在最后进行总结。
基本的旋转操作
旋转操作是恢复红黑树性质的一条基本操作,它包括将一个当前节点的右孩子提到当前节点旋转到当前节点的位置,也就是左旋,对称的是将将当前节点的左孩子旋转到当前节点的位置,也就是右旋,我们用前一种情况作为例子进行分析,我们把它命名为rotateWithRightChild,整个转换过称如图所示,其中转换前图的红色标记为断开的链的序列,转换后图的红色标记为对应断开的链接序列重新修复的链接。
这里采用的断链的序列根据关系对来确定的,比如说p和x1之间的断开修复关系为1,2;p和x的关系修复为3,4两条,x和g的关系修复是5,6两条,在没有图示的情况下这样是比较直观想象的,如果是画出的图例,我们也可以根据每个节点进行逐一修复,比如p的parent和child修复,x的parent,child修复,g的child修复,x1的parent修复,这样的方式进行也是可以的。
另外我们还需要考虑nullNode的情况,那么哪里会出现nullNode呢?p和x不可能,这是由rotateWithRightChild的适用环境所决定的,而x1和g可能为nullNode, 如果x1为nullNode,旋转后x1的parent指向了p,这并不会造成不良的后果,所以可以不予考虑,而如果g是nullNode,则说明p是root,旋转结束后就需要将x置为新根,这是一个必选的操作,不然root还是p。
基于以上考虑此我们有如下代码
/**
* 旋转父节点p的右儿子到p的位置
* @param p
* @return
*/
public void rotateWithRightChild(RBNode<T> p){
RBNode<T> x=p.right;
RBNode<T> g=p.parent;
//break1: p的右换指,同时改父指针
//tip1: x.left可能为nullNode,因为在处理的过程中对nullNode的parent
//可以是任意值,所以这里不做处理
p.right=x.left;
x.left.parent=p;
//break2: x的左换指,同时改父指针
x.left=p;
p.parent=x;
//break3: g的孩子更换为x
//tip2: g为nullNode,说明p为root,旋转需要换新根
if(g==nullNode) {
root=x;//旋转后换新根
root.parent=nullNode;
}
else if(g.left==p) {
g.left=x;
x.parent=g;
}else {
g.right=x;
x.parent=g;
}
}
对称的你可以写出rotateWithLeftChild,思路是一样的。
红黑树的插入
插入和删除是一个树的基本操作,对于有着色现在的红黑树来说,插入和删除都有可能引起红黑树着色限制的破坏,如何在插入和删除之后进行调整以维持红黑树的性质是红黑树实现的关键也是难点,这一节我们来探讨红黑树的插入。
我们插入的新节点总是红色的,原因是如果插入的节点是黑色的,那么必定会导致一条路径上的黑高增加一,每次插入必须进行调整,而如果插入节点是红色,而其父节点是黑色的话就可以不用调整。所以,我们选择插入红色的节点,在调整之前的插入逻辑和二叉查找树的插入是一样的,唯一不同的是需要在插入后如果插入节点的父节点是红色的话,会出现红红冲突,违反第四条限制规则,需要进行调整。下面是插入逻辑
/**
* 输入一个节点值为x到红黑树根root