红黑树是一种结点带有颜色属性的二叉查找树,但它除了满足二叉查找树的特点外,还有以下要求:
下图就是一个典型的红黑树:
由随机数量节点组成的二叉树的平均高度为logn,所以正常情况下二叉树查找的时间复杂度为O(logn)。但是,根据二叉树的特性,在最坏的情况下,比如存储的是一个有序的数据的话,那么所以的数据都会形成一条链,此时二叉树的深度为n,时间复杂度为O(n)。红黑树就是为了解决这个问题的,它能够保证在任何情况下树的深度都保持在logn左右。
代码如下:
public class RBTNode> {
boolean color; // 颜色,false为红色,true为黑色;默认为false
T key; // 键值
RBTNode left; // 左子结点
RBTNode right; // 右子结点
RBTNode parent; // 父结点
public RBTNode(T key, boolean color, RBTNode parent, RBTNode left, RBTNode right) {
this.key = key;
this.color = color;
this.parent = parent;
this.left = left;
this.right = right;
}
public T getKey() {
return key;
}
public String toString() {
return key + "(" + (this.color ? "Black" : "Red") + ")";
}
}
树的基本操作包括查找,删除和添加。在删除或者添加一个节点的时候就有可能打破原有的红黑树维持的平衡,那么就需要通过变色和旋转的方式来使红黑树重新达到平衡。
变色是非常简单的,原节点是红色,就置为黑色,若原节点是黑色,就置为红色。要理解红黑树,就必须需要懂得如何进行旋转,旋转又分为左旋和右转,两个操作相反的,所以理解了一个旋转的操作就很容易理解另一个旋转了。
如图所示,红色节点为旋转支点,支点往左子树移动即为左旋。左旋之后我们可以看到原支点的位置被原支点的右子节点代替,新支点的左子节点变为了原来为父节点的原支点,新支点的左子节点变为原支点的右子节点,因此左旋操作总共右3个节点,以为旋转前的结构举例,分别为红色节点(原支点),黄色节点(新支点)和L节点。
Java代码实现如下:
/*
* 对红黑树的节点(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.root = 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;
}
右旋操作和左旋相反的,两者互反。依然是红色作为旋转支点,右旋后黄色节点代替了红色节点原来的位置,黄色节点的右节点旋转后变为红色节点的左节点。
Java 代码实现如下:
/*
* 对红黑树的节点(y)进行右旋转
*
* py py
* / /
* y x
* / \ ----> / \
* x ry lx y
* / \ / \
* lx rx rx ry
*
*/
private void rightRotate(RBTNode y) {
// 设置x是当前节点的左孩子。
RBTNode 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.root = 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;
}
新创建的节点默认为红色(假如为黑色,不论新添加的节点在哪个节点下,则必然会导致某一条路径上的黑色节点数比别的路径多出一个来,即违背了约束5,导致红黑树的调整(空树情况除外);而若是为红色,则插入在黑节点下时,不会导致红黑树的调整)。
将新节点插入到红黑树中,红黑树插入节点与二叉树是一致的,所以每次添加节点肯定是添加到叶子节点上。我们可以知道插入节点是会导致红黑树的约束被破坏的。比如新节点的父节点为红色的时候违背了特性4(若是其父节点为黑色,则意味这该树不需要调整了)。
下面我们主要分析一下红黑树的插入的几种场景:
插入一颗空树:
直接把当前节点颜色变黑,并置为根节点即可;
插入节点的父节点为黑色:
直接插入即可,无需处理
插入节点的父节点为红色:
导致红黑树的约束被破坏,需要对该树重新平衡。
在插入一个节点之前该树肯定是一颗已经平衡了的红黑树,因此根据特新4,新节点的祖父节点必定为黑色。根据这种情况,我们又可以分为以下四种情况:
情况1:新节点为左节点,叔叔节点为红色;
情况2:新节点为左节点,叔叔节点为黑色;
情况3:新节点为右节点,叔叔节点为红色;
情况4:新节点为右节点,叔叔节点为黑色;
实际处理中情况1和情况3的过程其实是一样的,所以我们可以将这两种情况看作是一种情况
综上所述,我们可以归结为3中情况:
情况1:叔叔节点是红色节点;
情况2:叔叔节点是黑色节点,新节点为右节点;
情况3:叔叔节点是黑色节点,新节点为左节点;
情况1这种情况处理起来比较简单,只需要将祖父节点变为红色节点,父节点和叔叔节点变为黑色即可:
上图,我们看到该红黑树已经平衡,但是,我们还会有其他的情况,那就是祖父节点有可能也会有父节点。那么又会有两种情况,1是祖父节点的父节点可能是黑色的,2是可能是红色的,如果黑色那么整个红黑树也是平衡的了,如果是红色,则以其祖父节点作为当前节点,重复上述判断并进行平衡,直到当前节点的父节点为空:
情况2新节点为左节点,叔叔节点为黑色,操作如下:
情况3新节点为右节点,叔叔节点为黑色,操作如下:
即使最后其祖父节点还存在父节点也无妨,因为不论其上面的节点是红是黑都不会破坏红黑树的约束了,所以不用考虑。
上面是讨论左子树的问题,因为红黑色具有堆成性,因此在处理右子树的时候与处理左子树相反即可。
Java代码示例如下:
/*
* 新建结点(key),并将其插入到红黑树中
*/
public void insert(T key) {
RBTNode node = new RBTNode(key, false, null, null, null);
insert(node);
}
/*
* 将结点插入到红黑树中
*/
private void insert(RBTNode node) {
int cmp;
RBTNode y = null;
RBTNode x = this.root;
// 将节点添加到叶子节点上。
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.root = node;
}
// 检查是否破坏红黑树的五个特性,并进行平衡操作
insertRebalance(node);
}
/*
* 红黑树插入重新平衡
* 若向红黑树中插入节点之后破坏了红黑树的约束,则通过对几种特定情况的判断进行再平衡操作
*/
private void insertRebalance(RBTNode node) {
RBTNode parent, gParent;
// 若“父节点存在,并且父节点的颜色是红色”
while (((parent = parentOf(node)) != null) && isRed(parent)) {
gParent = parentOf(parent);
//若“父节点”是“祖父节点的左孩子”
if (parent == gParent.left) {
// 情况1:叔叔节点是红色
RBTNode uncle = gParent.right;
if ((uncle != null) && isRed(uncle)) {
reverseColor(uncle);
reverseColor(parent);
reverseColor(gParent);
node = gParent;
continue;
}
// 情况2:叔叔是黑色,且当前节点是右孩子,则把它左旋转化为情况3的场景
if (parent.right == node) {
RBTNode tmp;
leftRotate(parent);
tmp = parent;
parent = node;
node = tmp;
}
// 情况3:叔叔是黑色,且当前节点是左孩子。
reverseColor(parent);
reverseColor(gParent);
rightRotate(gParent);
} else { //若“父节点”是“祖父节点的右孩子”
// 情况1:叔叔节点是红色
RBTNode uncle = gParent.left;
if ((uncle != null) && isRed(uncle)) {
reverseColor(uncle);
reverseColor(parent);
reverseColor(gParent);
node = gParent;
continue;
}
// 情况2:叔叔是黑色,且当前节点是左孩子,则把它左旋转化为情况3的场景
if (parent.left == node) {
RBTNode tmp;
rightRotate(parent);
tmp = parent;
parent = node;
node = tmp;
}
// 情况3:叔叔是黑色,且当前节点是右孩子。
reverseColor(parent);
reverseColor(gParent);
leftRotate(gParent);
}
}
// 若当前节点父节点不存在则此节点必为根节点,且为红色,则置为黑
if(parent == null && !node.color)
reverseColor(node);
}
Java代码实现如下:
public RBTNode search(T value) {
return search(root, value);
}
/*
* (递归实现)查找"红黑树x"中键值为value的节点
*/
private RBTNode search(RBTNode x, T value) {
if (x == null)
return null;
int cmp = value.compareTo(x.value);
if (cmp < 0)
return search(x.left, value);
else if (cmp > 0)
return search(x.right, value);
else
return x;
}
public RBTNode iterativeSearch(T value) {
return iterativeSearch(root, value);
}
/*
* (非递归实现)查找"红黑树x"中键值为value的节点
*/
private RBTNode iterativeSearch(RBTNode x, T value) {
while (x != null) {
int cmp = value.compareTo(x.value);
if (cmp < 0)
x = x.left;
else if (cmp > 0)
x = x.right;
else
return x;
}
return x;
}
删除一个节点的时候也分为3种情况:
删除节点有两个子节点
找到比它大的节点中最小的那个(也可以找比它小的节点中最大的那个),用找到的节点的值取代它的值即可,如果找到的节点是红色,则不用调整,如果为黑色(由红黑树特性可推断其兄弟节点必为黑色),则删除此节点,并将其兄弟节点变为红色,这就转化成了一个插入红色节点的问题,对其兄弟节点进行重新平衡的操作。
删除节点只有左子节点
由红黑树特性可知,此节点必为黑色,直接用左子节点的值取代它的值后,删除其左子节点即可。
删除节点只有右子节点
由红黑树特性可知,此节点必为黑色,直接用右子节点的值取代它的值后,删除其右子节点即可。
删除节点没有子节点
该节点为红色,直接删除,若为黑色,则继续考虑其兄弟节点的颜色,如果兄弟节点为黑色
总结上面的3种情况可得到一个结论,只有删除节点为黑色时才会破坏红黑树原来的平衡,因在删除节点之前红黑树是平衡状态的,删除之后很明显的其兄弟节点分支必然比删除节点的分支多了一个黑色的节点,因此我们只需要改变兄弟节点的颜色即可,我们只讨论左节点,右节点对称。
一、删除节点的兄弟节点是红色(下面假设被删节点在左侧的情况)
将兄弟节点设为黑色,兄弟左子节点设为红色,以父节点为支点左旋转:
二、兄弟节点是黑色的,兄弟的两个子节点是红色的,将兄弟节点右子节点置为黑色,再将父节点左旋转
三、兄弟节点是黑色的,且兄弟节点的右子节点是红色,左子节点为空
以父节点为支点左旋转
四、兄弟节点是黑色的,且兄弟节点的左子节点是红色,右子节点为空
将兄弟节点置为红色,左子节点置为黑色,再将兄弟节点右旋转(转换成了三的情况),最后再将父节点左旋转
五、兄弟节点是黑色的,且无子节点,将兄弟节点置为红
若被删节点还有祖父节则过程如下
删除的Java代码示例:
/*
* 删除结点
*/
public boolean remove(T value) {
RBTNode node;
if ((node = search(root, value)) != null) { //找到键值对应的节点
remove(node);
return true;
}else {
return false; //表示没有找到该值对应的节点
}
}
/*
* 删除结点(node)
*/
private void remove(RBTNode node) {
RBTNode parent;
//情况1: 被删除节点的"左右孩子都不为空"的情况。
if ((node.left != null) && (node.right != null)) {
// 被删节点的后继节点。(称为"取代节点")
// 用它来取代"被删节点"的位置
RBTNode replace = node.right;
while (replace.left != null) { //这里我找的是比node大的数中最小的那个
replace = replace.left;
}
if(!replace.color){ //取代节点为红色,直接覆盖被取代节点值即可
node.value = replace.value;
replace.parent.left = null; //删除取代节点
}else{ //取代节点为黑色
node.value = replace.value;
parent = replace.parent;
parent.left = null; //删除取代节点
removeRebalance(parent); //从取代节点的父节点开始重新平衡,其实是转换成了情况4的处理逻辑
}
}else if (node.left != null) { //情况2:左子节点不为空,右子节点为空,此节点必为黑色,子节点必为红
node.value = node.left.value; //左子节点直接覆盖被当前节点值即可
node.left = null; //删除左子节点
} else if(node.right !=null){ //情况3:右子节点不为空,左子节点为空,此节点必为黑色,子节点必为红
node.value = node.right.value; //右子节点直接覆盖被当前节点值即可
node.right = null; //删除右子节点
}else{ //情况4:左右子节点都为空
if(node.parent == null){ //根节点情况
root = null;
}else if(!node.color){ //节点为红色情况,删除不会破坏平衡
if(node.parent.left == node){ //不是父节点左子节点必为右子节点
node.parent.left = null;
}else{
node.parent.right = null;
}
}else{ //节点为黑色情况
parent = node.parent;
removeRebalance(parent);
if(parent.left == node){
parent.left = null;
}else{
parent.right = null;
}
}
}
}
/*
* 在从红黑树中删除黑色的非空叶子节点之后(红黑树失去平衡),调用该方法;
* 目的是将它重新塑造成一颗红黑树。
*/
private void removeRebalance(RBTNode node) {
RBTNode brother;
RBTNode parent;
while (node != this.root) {
parent = node.parent;
if(parent.left == node){
brother = parent.right;
if(!brother.color){ //兄弟为红色,则侄子必全为黑
parent.left = null;
reverseColor(brother); //变为黑色
leftRotate(parent); //旋转parent使之平衡
break;
}else{ //兄弟为黑色,则侄子要么为空,要么为红
if(brother.right != null){
reverseColor(brother.right); //右侄子变为黑色
leftRotate(parent);
if(!parent.color){ //父节点为红
reverseColor(parent);
reverseColor(brother);
}
break;
}else if(brother.left != null){
reverseColor(brother.left);
rightRotate(brother);
leftRotate(parent);
if(!parent.color){ //父节点为红
reverseColor(parent);
reverseColor(brother);
}
}else{
reverseColor(parent.right);
node = parent;
}
}
}else{
brother = parent.left;
if(!brother.color){ //兄弟为红色,则侄子必全为黑
parent.right = null;
reverseColor(brother); //变为黑色
leftRotate(parent); //旋转parent使之平衡
break;
}else{ //兄弟为黑色,则侄子要么为空,要么为红
if(brother.left != null){
reverseColor(brother.left); //右侄子变为黑色
leftRotate(parent);
if(!parent.color){ //父节点为红
reverseColor(parent);
reverseColor(brother);
}
break;
}else if(brother.right != null){
reverseColor(brother.right);
rightRotate(brother);
leftRotate(parent);
if(!parent.color){ //父节点为红
reverseColor(parent);
reverseColor(brother);
}
}else{
reverseColor(parent.left);
node = parent;
}
}
}
}
}
最后推荐一个红黑树的演示网站:https://www.cs.usfca.edu/~galles/visualization/RedBlack.html