1.红黑树的定义
红黑树(Red-Black Tree,简称RB树),是一种特殊的二叉查找树,所以他也满足二叉查找树的特征:任意一个节点的值大于右子节点的键值,小于左子节点的键值。除此之外,红黑树还具备很多其他特征:
1)每个节点都是红色或者黑色
2)根节点必定为黑色
3)每个叶节点(左右子节点都为null的节点)必定是黑色
4)如果一个节点是红色的,他的子节点必须是黑色的(反之则不一定),也就是说不允许有两个连续的红色节点,但可以有连续的黑色节点。
5)从根节点到叶节点或空节点的每条路径上,包含的黑色节点数相同,即黑色高度相同。
注意:执行插入操作时的新节点总是红色的,因为插入一个红色节点比插入一个黑色节点违反红黑树规则的可能性更小,如果插入的是黑色节点那必然将改变黑色高度,需要进行自修正,而插入一个红色节点,只有一半的可能遇到出现连续两个红色节点的情况。
红黑树示意图:
2.红黑树的自我修正
红黑树主要有三种方式对平衡进行修正:重新着色,左旋,右旋。
1)重新着色
新插入的节点为红色节点15,插入后有两个连续的红色节点,这时我们将其父节点及其兄弟节点重新着色成黑色,这样既保证了没有连续的红色节点也保证了黑色高度的统一。
2)右旋
如果对一个节点进行右旋,这个节点会向右和向下移动到他原来位置的右节点处,他的左节点将移动到他原来的位置,左节点的右子树将变成他的左子树。进行右旋的节点必须要有左节点。
3) 左旋
3.左旋右旋的具体实现
1)节点类
节点类和二叉树的节点类相似,增加了一个bool值来描述节点的颜色
public class RBNode<T extends Comparable<T>> {
boolean color;//颜色
T key;//关键值
RBNode<T> left;//左子节点
RBNode<T> right;//右子节点
RBNode<T> parent;//父节点
public RBNode(boolean color,T key,RBNode<T> parent,RBNode<T> left,RBNode<T> right){
this.color = color;
this.key = key;
this.parent = parent;
this.left = left;
this.right = right;
}
//获得节点的关键值
public T getKey(){
return key;
}
//打印节点的关键值和颜色信息
public String toString(){
return ""+key+(this.color == RED ? "R":"B");
}
}
2)左旋
/*************对红黑树节点x进行左旋操作 ******************/
/*
* 左旋示意图:对节点x进行左旋
* p p
* / /
* x y
* / \ / \
* lx y -----> x ry
* / \ / \
* ly ry lx ly
* 左旋做了三件事:
* 1. 将y的左子节点赋给x的右子节点,并将x赋给y左子节点的父节点(y左子节点非空时)
* 2. 将x的父节点p(非空时)赋给y的父节点,同时更新p的子节点为y(左或右)
* 3. 将y的左子节点设为x,将x的父节点设为y
*/
private void leftRotate(RBNode<T> x){
//1. 将y的左子节点赋给x的右子节点,并将x赋给y左子节点的父节点(y左子节点非空时)
RBNode<T> y = x.right;
x.right = y.left;
if(y.left != null){
y.left.parent = x;
}
//2. 将x的父节点p(非空时)赋给y的父节点,同时更新p的子节点为y(左或右)
y.parent = x.parent;
if(x.parent == null){
this.root = y;//如果x的父节点为空(即x为根节点),则将y设为根节点
}else{
if(x == x.parent.left){//如果x是左子节点
x.parent.left = y;//则也将y设为左子节点
}else{
x.parent.right = y;//否则将y设为右子节点
}
}
//3. 将y的左子节点设为x,将x的父节点设为y
y.left = x;
x.parent = y;
}
3) 右旋
/*************对红黑树节点y进行右旋操作 ******************/
/*
* 左旋示意图:对节点y进行右旋
* p p
* / /
* y x
* / \ / \
* x ry -----> lx y
* / \ / \
* lx rx rx ry
* 右旋做了三件事:
* 1. 将x的右子节点赋给y的左子节点,并将y赋给x右子节点的父节点(x右子节点非空时)
* 2. 将y的父节点p(非空时)赋给x的父节点,同时更新p的子节点为x(左或右)
* 3. 将x的右子节点设为y,将y的父节点设为x
*/
private void rightRotate(RBNode<T> y){
//1. 将y的左子节点赋给x的右子节点,并将x赋给y左子节点的父节点(y左子节点非空时)
RBNode<T> x = y.left;
y.left = x.right;
if(x.right != null){
x.right.parent = y;
}
//2. 将x的父节点p(非空时)赋给y的父节点,同时更新p的子节点为y(左或右)
x.parent = y.parent;
if(y.parent == null){
this.root = x;//如果y的父节点为空(即y为根节点),则旋转后将x设为根节点
}else{
if(y == y.parent.left){//如果y是左子节点
y.parent.left = x;//则将x也设置为左子节点
}else{
y.parent.right = x;//否则将x设置为右子节点
}
}
//3. 将x的左子节点设为y,将y的父节点设为y
x.right = y;
y.parent = x;
}
4.插入
将一个节点插入到红黑树中,需要执行哪些步骤呢?首先,将红黑树当作一颗二叉查找树,将节点插入;然后,将节点着色为红色;最后,通过"旋转和重新着色"等一系列操作来修正该树,使之重新成为一颗红黑树。详细描述如下:
第一步: 将红黑树当作一颗二叉查找树,将节点插入。
红黑树本身就是一颗二叉查找树,将节点插入后,该树仍然是一颗二叉查找树。也就意味着,树的键值仍然是有序的。此外,无论是左旋还是右旋,若旋转之前这棵树是二叉查找树,旋转之后它一定还是二叉查找树。这也就意味着,任何的旋转和重新着色操作,都不会改变它仍然是一颗二叉查找树的事实。
好吧?那接下来,我们就来想方设法的旋转以及重新着色,使这颗树重新成为红黑树!
第二步:将插入的节点着色为"红色"。
为什么着色成红色,而不是黑色呢?为什么呢?在回答之前,我们需要重新温习一下红黑树的特性:
(1) 每个节点或者是黑色,或者是红色。
(2) 根节点是黑色。
(3) 每个叶子节点是黑色。 [注意:这里叶子节点,是指为空的叶子节点!]
(4) 如果一个节点是红色的,则它的子节点必须是黑色的。
(5) 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
将插入的节点着色为红色,不会违背"特性(5)"!少违背一条特性,就意味着我们需要处理的情况越少。接下来,就要努力的让这棵树满足其它性质即可;满足了的话,它就又是一颗红黑树了。o(∩∩)o…哈哈
第三步: 通过一系列的旋转或着色等操作,使之重新成为一颗红黑树。
第二步中,将插入节点着色为"红色"之后,不会违背"特性(5)"。那它到底会违背哪些特性呢?
对于"特性(1)",显然不会违背了。因为我们已经将它涂成红色了。
对于"特性(2)",显然也不会违背。在第一步中,我们是将红黑树当作二叉查找树,然后执行的插入操作。而根据二叉查找数的特点,插入操作不会改变根节点。所以,根节点仍然是黑色。
对于"特性(3)",显然不会违背了。这里的叶子节点是指的空叶子节点,插入非空节点并不会对它们造成影响。
对于"特性(4)",是有可能违背的!
那接下来,想办法使之"满足特性(4)",就可以将树重新构造成红黑树了。
5.删除
上面探讨完了红-黑树的插入操作,接下来讨论删除,红-黑树的删除和二叉查找树的删除是一样的,只不过删除后多了个平衡的修复而已。我们先来回忆一下二叉搜索树的删除:
①、如果待删除的节点没有子节点,那么直接删除即可。
②、如果待删除的节点只有一个子节点,那么直接删掉,并用其子节点去顶替它。
③、如果待删除的节点有两个子节点,这种情况比较复杂:首先找出它的后继节点,然后处理“后继节点”和“被删除节点的父节点”之间的关系,最后处理“后继节点的子节点”和“被删除节点的子节点”之间的关系。每一步中也会有不同的情况。
实际上,删除过程太复杂了,很多情况下会采用在节点类中添加一个删除标记,并不是真正的删除节点。详细的删除我们这里不做讨论。