红黑树(red-black-tree)是许多“平衡”搜索树的一种,它可以保证在最坏情况下基本动态集合操作的时间复杂度为O(lgn)。除遍历外,其余的方法的时间复杂度都为O(lgn),如INSERT, SEARCH, MAXIMUM, MINIMUM, DELETE等。本章 将依次介绍一些比较重要的方法,并赋予其Java代码的实现。详细的红黑树理论,可以参考《算法导论》中P174-192。
注:下面几篇博文值得借鉴,本文在此基础上做了一些修改和完善。
首先,定义RBNode的属性
class RBNode{
public RBNode leftNode; //左子结点
public RBNode rightNode; //右子结点
public RBNode parent; //父结点
public int data; //元数据
public boolean color; //颜色
public RBNode(int data) {
this.data=data;
}
下面,将依次介绍几个重要方法的Java实现。
(1) 左旋 LeftRotate
/*
* 对红黑树的节点(x)进行左旋转
*
* px px
* / /
* x y
* / \ --(左旋)-. / \
* lx y x ry
* / \ / \
* ly ry lx ly
*
*
*/
public void leftRotate(RBNode x) {
RBNode y = x.rightNode;
//System.out.println(y.data);
// 如果y的左孩子非空,将 “x” 设为 “y的左孩子的父亲”
// 将 “y的左孩子” 设为 “x的右孩子”;
if (y.leftNode != null) {
y.leftNode.parent = x;
}
x.rightNode=y.leftNode;
RBNode px = x.parent;
y.parent=px;
if(px==null) {
root = y;
}else {
if(px.leftNode==x) {
px.leftNode=y;
}else {
px.rightNode=y;
}
}
// 将 “x” 设为 “y的左孩子”
y.leftNode=x;
// 将 “x的父节点” 设为 “y”
x.parent=y;
}
(2)右旋 RightRotate
/*
* 对红黑树的节点(y)进行右旋转
*
* py py
* / /
* y x
* / \ --(右旋)-. / \
* x ry lx y
* / \ / \
* lx rx rx ry
*
*/
public void rightRotate(RBNode y) {
RBNode x = y.leftNode;
if(x.rightNode!=null) {
x.rightNode.parent=y;
}
y.leftNode=x.rightNode;
RBNode py = y.parent;
x.parent=py;
if(py==null) {
root = x;
}else {
if(py.leftNode==y) {
py.leftNode=x;
}else {
py.rightNode=x;
}
}
// 将 “y” 设为 “x的右孩子”
y.parent=x;
// 将 “y的父节点” 设为 “x”
x.rightNode=y;
}
(3)插入 Insert
第一步: 将红黑树当作一颗二叉查找树,将节点插入。
红黑树本身就是一颗二叉查找树,将节点插入后,该树仍然是一颗二叉查找树。也就意味着,树的键值仍然是有序的。此外,无论是左旋还是右旋,若旋转之前这棵树是二叉查找树,旋转之后它一定还是二叉查找树。这也就意味着,任何的旋转和重新着色操作,都不会改变它仍然是一颗二叉查找树的事实。
那接下来,我们就来想方设法的旋转以及重新着色,使这颗树重新成为红黑树!
第二步:将插入的节点着色为"红色"。
为什么着色成红色,而不是黑色呢?为什么呢?在回答之前,我们需要重新温习一下红黑树的特性:
(1) 每个节点或者是黑色,或者是红色。
(2) 根节点是黑色。
(3) 每个叶子节点是黑色。 [注意:这里叶子节点,是指为空的叶子节点!]
(4) 如果一个节点是红色的,则它的子节点必须是黑色的。
(5) 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
将插入的节点着色为红色,不会违背"特性(5)"!少违背一条特性,就意味着我们需要处理的情况越少。接下来,就要努力的让这棵树满足其它性质即可;满足了的话,它 就又是一颗红黑树了。
第三步: 通过一系列的旋转或着色等操作,使之重新成为一颗红黑树。
第二步中,将插入节点着色为"红色"之后,不会违背"特性(5)"。那它到底会违背哪些特性呢?
对于"特性(1)",显然不会违背了。因为我们已经将它涂成红色了。
对于"特性(2)",显然也不会违背。在第一步中,我们是将红黑树当作二叉查找树,然后执行的插入操作。而根据二叉查找数的特点,插入操作不会改变根节点。所以,根 节点仍然是黑色。
对于"特性(3)",显然不会违背了。这里的叶子节点是指的空叶子节点,插入非空节点并不会对它们造成影响。
对于"特性(4)",是有可能违背的!
那接下来,想办法使之"满足特性(4)",就可以将树重新构造成红黑树了。
/*
* 红黑树的插入操作的时间复杂度为O(lgn),因为正常的二叉树的插入时间复杂度为O(lgn),而inserFix的时间复杂度也为O(lgn), 所以综述总的时间复杂度为O(lgn)
*/
public void insert(RBNode node) {
RBNode current = root;
RBNode parent = root;
// 1. 将红黑树当作一颗二叉查找树,将节点添加到二叉查找树中。
if(root == null) {
root = node;
root.parent=null;
}else {
while(true) {
parent = current ;
if(current.data>node.data) {
current = current.leftNode;
if(current==null) {
parent.leftNode=node;
node.parent=parent;
break;
}
}else {
current = current.rightNode;
if(current==null) {
parent.rightNode=node;
node.parent=parent;
break;
}
}
}
}
// 2. 设置节点的颜色为红色
node.color=RED;
// 3. 将它重新修正为一颗二叉查找树
insertFixUp(node);
}
(4)插入修改 InsertFixUp
private void insertFixUp(RBNode node) {
RBNode parent =null;
RBNode gparent = null;
// 若“父节点存在,并且父节点的颜色是红色”
while((parent=node.parent)!=null && parent.color==RED) {
gparent = parent.parent;
//若“父节点”是“祖父节点的左孩子”
if(gparent!=null && parent == gparent.leftNode) {
RBNode uncle = gparent.rightNode;
// Case 1条件:叔叔节点是红色
if(uncle !=null && uncle.color==RED) {
uncle.color=BLACK;
parent.color=BLACK;
gparent.color=RED;
node = gparent;
continue;
}
// Case 2条件:叔叔是黑色,且当前节点是右孩子
if( (uncle==null || uncle.color==BLACK) && (parent.rightNode==node)) {
node = parent;
leftRotate(parent);
continue;
}
// Case 3条件:叔叔是黑色,且当前节点是左孩子。
parent.color=BLACK;
gparent.color=RED;
rightRotate(gparent);
}else {
//若“z的父节点”是“z的祖父节点的右孩子”
RBNode uncle = gparent.leftNode;
// Case 1条件:叔叔节点是红色
if(uncle !=null && uncle.color==RED) {
uncle.color=BLACK;
parent.color=BLACK;
gparent.color=RED;
node = gparent;
continue;
}
// Case 2条件:叔叔是黑色,且当前节点是左孩子
if( (uncle==null || uncle.color==BLACK) && (parent.leftNode==node)) {
node = parent;
rightRotate(parent);
continue;
}
// Case 3条件:叔叔是黑色,且当前节点是右孩子。
parent.color=BLACK;
gparent.color=RED;
leftRotate(gparent);
}
}
root.color=BLACK;
}
(5)查找 Search
public RBNode search(int value) {
RBNode current = root;
while(current !=null) {
if(current.data==value) {
break;
}
if(current.data>value) {
current = current.leftNode;
}else{
current = current.rightNode;
}
}
return current;
}
(6)寻找中序后继结点 Successor
//找到中序后继结点C,并解除C节点与其父结点的关系
public RBNode getSuccessor2(RBNode node) {
RBNode parent = node;
RBNode grandParent = node;
RBNode current = node.rightNode;
while(current!=null) {
grandParent = parent;
parent = current;
current = current.leftNode;
}
if(parent!=node.rightNode) {
grandParent.leftNode=parent.rightNode;
if(parent.rightNode!=null) {
parent.rightNode.parent=grandParent;
}
}else {
grandParent.rightNode=parent.rightNode;
if(parent.rightNode!=null) {
parent.rightNode.parent=grandParent;
}
}
return parent;
}
(7)删除 REMOVE
从排序树中删除节点的思路是一样的,首先找到要删除的节点,并做如下处理:
- 如果该节点不存在非空子节点,则直接删除它
- 如果该节点存在一个非空子节点,则用其非空子节点替换其位置即可
- 如果该节点有两个非空子节点,则可以找到该节点的前驱或者后继,然后更换两个节点的元素值,再将前驱或者后继当做被删除的节点(由于任意一个节点的前驱或者后继都必定至多只有一个非空子节点,因而删除这样的节点就可以按照前两种情形进行处理)
红黑树也采取了类似的思路,假设要删除的节点为A,则先找到节点A,然后:
- 如果节点A有两个非空子节点,则找到节点A的前驱(也可以是后继),然后记要删除的元素B=A的前驱
- 否则只要A有一个空子节点,就记要删除的元素B=A
- 记变量X为B的第一个非空子节点(先检查左孩子,后检查右孩子),如果B的两个孩子都为空,则记X为空
- 如果B和A不同,则将B的内容拷贝到A
- 删除节点B
- 如果被删除节点B的颜色为红色,则删除结束,否则从节点X处开始对删除节点后的红黑树进行调整
以下图为例:
- 如果要删除节点75,则A=75,B=75,X=85
- 如果要删除节点25,则A=25,B=29,X=NULL
- 如果要删除节点40,则A=B=40,X=NULL
显然,在这种处理方式下,如果B的颜色为红色,则删除它后删除操作即完成了,因为:
- B是红色的,因而它不是根
- B是红色的,它的父节点必然是黑色的,因而删除它不会引入两个红色的连续节点
- B是红色的,而且它至多有一个非空子节点,因而删除它不会导致红黑树的任意路径上的黑节点数目变化
如果B的颜色为黑色,则需要从节点X处开始对删除节点后的红黑树进行调整。调整则参考《算法导论》中的4种Case。
/*
* 红黑树的删除操作的时间复杂度为O(lgn),因为正常的二叉树的删除时间复杂度为O(lgn),而removeFix的时间复杂度也为O(lgn)
所以综述总的时间复杂度为O(lgn)*/
public int rbRemove(int value) {
//定义结点A,B,X
RBNode A,B,X,parent;
boolean color;
//先找到被删除的结点
A = search(value);
if(A ==null) {
return -1;
}
//如果被删除结点的左子节点和右子节点都不为空,找到中序后继结点,然后记要删除的元素B=A的后继
if(A.leftNode!=null && A.rightNode!=null) {
//取得该节点的中序后继结点successor
B=getSuccessor2(A);
if(B==A.rightNode) {
parent = B;
}else {
parent = B.parent;
}
color=B.color;
//赋值X,X为B的第一个非空子节点(先检查左孩子,后检查右孩子),如果B的两个孩子都为空,则记X为空
if(B.leftNode!=null) {
X=B.leftNode;
}else if(B.rightNode!=null){
X=B.rightNode;
}else {
X=null;
}
if(A==root) {
//B的左子节点为根节点的左节点
B.leftNode=root.leftNode;
root.leftNode.parent=B; //将根节点的右节点赋给B的右节点
B.rightNode=root.rightNode;
root.rightNode.parent=B; //替换根节点
root=B;
}else {
//A在左边
if(A.parent.leftNode==A) {
A.parent.leftNode=B;
B.parent=A.parent; //将A节点的右节点赋给B的右节点
B.leftNode=A.leftNode;
A.leftNode.parent=B; //将A节点的右节点赋给B的右节点
B.rightNode=A.rightNode;
if(A.rightNode!=null) {
A.rightNode.parent=B;
}
}else {
//A在右边
A.parent.rightNode=B;
B.parent=A.parent;
//将A节点的右节点赋给B的右节点
B.leftNode=A.leftNode;
A.leftNode.parent=B; //将A节点的右节点赋给B的右节点
B.rightNode=A.rightNode;
if(A.rightNode!=null) {
A.rightNode.parent=B;
}
}
}
}else {
//否则只要被删除结点有一个空节点,记B=A
B=A;
parent=B.parent;
color = B.color;
//赋值X,X为B的第一个非空子节点(先检查左孩子,后检查右孩子),如果B的两个孩子都为空,则记X为空
if(B.leftNode!=null) {
X=B.leftNode;
}else if(B.rightNode!=null){
X=B.rightNode;
}else {
X=null;
}
//删除B
//首先如果B为一个叶子结点
if(B.leftNode==null && B.rightNode==null) {
//如果B为根节点
if(B==root) {
root =null;
}else {
//B在左边
if(B.parent.leftNode==B) {
B.parent.leftNode=null;
B.parent=null;
}else {
//B在右边
B.parent.rightNode=null;
B.parent=null;
}
}
}else {
//删除的结点为只有一个子节点的父节点
if(B==root) {
//如果删除的结点为root
if(root.leftNode!=null) {
root = root.leftNode;
root.leftNode.parent=null;
}else {
root = root.rightNode;
root.rightNode.parent=null;
}
}else {
//如果删除的不是root结点
//如果B在左边
if(B.parent.leftNode==B) {
//左子节点不为空
if(B.leftNode!=null) {
B.parent.leftNode=B.leftNode;
B.leftNode.parent=B.parent;
}else {
//右子节点不为空
B.parent.leftNode=B.rightNode;
B.rightNode.parent=B.parent;
}
}else {
//如果B在右边
if(B.leftNode!=null) {
B.parent.rightNode=B.leftNode;
B.leftNode.parent=B.parent;
}else {
B.parent.rightNode=B.rightNode;
B.rightNode.parent=B.parent;
}
}
}
}
}
//如果被删除节点B的颜色为红色,则删除结束,否则从节点X处开始对删除节点后的红黑树进行调整
if(color==BLACK) {
removeFix(X,parent);
}
return A.data;
}
(8)删除的调整 RemoveFix
public void removeFix(RBNode node,RBNode parent) {
//如果N结点为根节点或者N的颜色是红色,就将其颜色设为黑色
RBNode s;
if(node!=null && (node == root || node.color==RED)) {
node.color=BLACK;
return ;
}
while((node==null || node.color==BLACK) && node!=root) {
if(parent.leftNode==node) {
s=parent.rightNode;
//case1: 如果兄弟结点是红色
if(s.color==RED) {
s.color=BLACK;
parent.color=RED;
leftRotate(parent);
s=parent.rightNode;
}
//case2: node的兄弟s是黑色,且s的俩个孩子也都是黑色的
if((s.leftNode==null || s.leftNode.color==BLACK ) && (s.rightNode==null || s.rightNode.color==BLACK )) {
s.color=RED;
node = parent;
}else {
//case3: node的兄弟s是黑色的,并且s的左孩子是红色,右孩子为黑色。
if(s.rightNode==null ||s.rightNode.color==BLACK) {
s.leftNode.color=BLACK;
s.color=RED;
rightRotate(s);
s=parent.rightNode;
}
// Case 4: node的兄弟s是黑色的;并且s的右孩子是红色的,左孩子任意颜色。
s.color=parent.color;
parent.color=BLACK;
s.rightNode.color=BLACK;
leftRotate(parent);
node=root;
}
}else {
s=parent.leftNode;
//case1:如果兄弟结点是红色
if(s.color==RED) {
s.color=BLACK;
parent.color=RED;
rightRotate(parent);
s=parent.leftNode;
} //case2: node的兄弟s是黑色,且s的俩个孩子也都是黑色的
if((s.leftNode==null || s.leftNode.color==BLACK ) && (s.rightNode==null || s.rightNode.color==BLACK )) {
s.color=RED;
node = parent;
}else {
//case3: node的兄弟s是黑色的,并且s的左孩子是红色,右孩子为黑色。
if(s.rightNode==null ||s.rightNode.color==BLACK) {
s.rightNode.color=BLACK;
s.color=RED;
leftRotate(s);
s=parent.rightNode;
}
// Case 4: x的兄弟w是黑色的;并且w的右孩子是红色的,左孩子任意颜色。
s.color=parent.color;
parent.color=BLACK;
s.leftNode.color=BLACK;
rightRotate(parent);
node=root;
}
}
}
//node.color=BLACK; //这里和前面的第一个if意思一致
}
(9)前序遍历FrontIterator
public void FrontIterator(RBNode node) {
if(node==null) {
return ;
}else {
System.out.print(node.data+"("+node.color+")"+" ");
FrontIterator(node.leftNode);
FrontIterator(node.rightNode);
}
}
(10)中序遍历MiddleIterator
public void MiddleIterator(RBNode node) {
if(node==null) {
return ;
}else {
MiddleIterator(node.leftNode);
System.out.print(node.data+" ");
MiddleIterator(node.rightNode);
}
}
(11)后序遍历LastIterator
public void LastIterator(RBNode node) {
if(node==null) {
return ;
}else {
LastIterator(node.leftNode);
LastIterator(node.rightNode);
System.out.print(node.data+" ");
}
}
(12)找最大值Maximum
public RBNode getMaximum() {
RBNode current = root;
while(current.rightNode!=null) {
current = current.rightNode;
}
return current;
}
(13)找最小值Minimum
public RBNode getMinimum() {
RBNode current = root;
while(current.leftNode!=null) {
current = current.leftNode;
}
return current;
}
(14)将数组转化成红黑树
public void generateArrayToRBTree(int[] arr) {
for(int i=0;i<arr.length;i++) {
insert(new RBNode(arr[i]));
}
}
(15)测试方法
public static void main(String[] args) {
RBTree tree = new RBTree();
//int[] arr = {11,2,14,1,7,5,8,4,15};
//41,38,31,12,19,8
int[] arr2 = {41,38,31,12,19,8};
//测试插入方法
tree.generateArrayToRBTree(arr2);
tree.FrontIterator(tree.root);
System.out.println();
//测试删除方法
// int a = tree.rbRemove(7);
// System.out.println(a);
// tree.FrontIterator(tree.root);
// System.out.println();
}