JAVA数据结构与算法(八)java实现红黑树
红黑树的性质
红黑树是一种自平衡二叉树,红黑树和AVL树一样都对插入时间、删除时间和查找时间提供了最好可能的最坏情况担保。
红黑树需要满足的五条性质:
性质一:节点是红色或者是黑色;
在树里面的节点不是红色的就是黑色的,没有其他颜色。
性质二:根节点是黑色;
根节点总是黑色的。它不能为红。
性质三:每个叶子节点是黑色。 [注意:这里叶子节点,是指为空的叶子节点!]
实现的时候NIL节点是个空节点用null表示,并且是黑色的
性质四:每个红色节点的两个子节点都是黑色的(也就是说不存在两个连续的红色节点);
连续的两个节点不能是连续的红色,连续的两个节点的意思就是父节点与子节点不能是连续的红色
性质五:从任一节点到其每个叶子节点的所有路径都包含相同数目的黑色节点;
从根节点到每一个NIL节点的路径中,都包含了相同数量的黑色节点。
基本定义
public class RedBlackTree <K extends Comparable<K>,V> {
private static final boolean RED=true;
private static final boolean BLACK=true;
class Node{
private K key;
private V value;
private Node leftChild;
private Node rightChild;
private boolean color;
public Node(K key,V value){
this.key=key;
this.value=value;
leftChild=null;
rightChild=null;
color=RED;
}
}
每次变化完需要遵守性质,所以会用到颜色翻转方法,和判断颜色方法
//判断节点node的颜色
public boolean isRed(Node node){
if(node==null){
return BLACK;
}return node.color;
}
//颜色翻转
private void flipColors(Node node){
node.color=RED;
node.leftChild.color=BLACK;
node.rightChild.color=RED;
}
为了维持树的平衡,所以还是会用到左转右旋
左旋转
// 对节点进行向左旋转操作,返回旋转后的根节点x
// y x
// / \ / \
//T1 x y z
// / \ ----> / \ / \
// T2 z T1 T2 T3 T4
// / \
// T3 T4
// 对节点进行向右旋转操作,返回旋转后的根节点x
// y x
// / \ / \
// x T4 z y
// / \ ----------> / \ / \
// z T3 T1 T2 T3 T4
// / \
// T1 T2
添加
将一个节点插入到红黑树中,需要执行哪些步骤呢?首先,将红黑树当作一颗二叉查找树,将节点插入;然后,将节点着色为红色;最后,通过"旋转和重新着色"等一系列操作来修正该树,使之重新成为一颗红黑树。详细描述如下:
第一步: 将红黑树当作一颗二叉查找树,将节点插入。
红黑树本身就是一颗二叉查找树,将节点插入后,该树仍然是一颗二叉查找树。也就意味着,树的键值仍然是有序的。此外,无论是左旋还是右旋,若旋转之前这棵树是二叉查找树,旋转之后它一定还是二叉查找树。这也就意味着,任何的旋转和重新着色操作,都不会改变它仍然是一颗二叉查找树的事实。
好吧?那接下来,我们就来想方设法的旋转以及重新着色,使这颗树重新成为红黑树!
第二步:将插入的节点着色为"红色"。
为什么着色成红色,而不是黑色呢?为什么呢?在回答之前,我们需要重新温习一下红黑树的特性:
(1) 每个节点或者是黑色,或者是红色。
(2) 根节点是黑色。
(3) 每个叶子节点是黑色。 [注意:这里叶子节点,是指为空的叶子节点!]
(4) 如果一个节点是红色的,则它的子节点必须是黑色的。
(5) 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
将插入的节点着色为红色,不会违背"特性(5)"!少违背一条特性,就意味着我们需要处理的情况越少。接下来,就要努力的让这棵树满足其它性质即可;满足了的话,它就又是一颗红黑树了。o(∩∩)o…哈哈
第三步: 通过一系列的旋转或着色等操作,使之重新成为一颗红黑树。
第二步中,将插入节点着色为"红色"之后,不会违背"特性(5)"。那它到底会违背哪些特性呢?
对于"特性(1)",显然不会违背了。因为我们已经将它涂成红色了。
对于"特性(2)",显然也不会违背。在第一步中,我们是将红黑树当作二叉查找树,然后执行的插入操作。而根据二叉查找数的特点,插入操作不会改变根节点。所以,根节点仍然是黑色。
对于"特性(3)",显然不会违背了。这里的叶子节点是指的空叶子节点,插入非空节点并不会对它们造成影响。
对于"特性(4)",是有可能违背的!
那接下来,想办法使之"满足特性(4)",就可以将树重新构造成红黑树了。
//向红黑树中添加新的元素(key,value)
public void add(K key,V value){
root=add(root,key,value);
root.color=BLACK; //最终根节点为BLACK
}
//向以node为根的红黑树中插入元素(key,value),递归算法
//返回插入新节点后的红黑树的根
private Node add(Node node, K key, V value) {
if(node==null){
size++;
return new Node(key,value);
}
if(key.compareTo(node.key)<0){
node.leftChild=add(node.leftChild,key,value);
}else if(key.compareTo(node.key)>0){
node.rightChild=add(node.rightChild,key,value);
}else{
node.value=value;
}
if(isRed(node.rightChild)&&!isRed(node.leftChild)){
node=leftRotate(node);
}
if(isRed(node.leftChild)&&isRed(node.leftChild.leftChild)){
node=rightRotate(node);
}
if(isRed(node.leftChild)&&isRed(node.rightChild)){
flipColors(node);
}
return node;
}
删除
将红黑树内的某一个节点删除。需要执行的操作依次是:首先,将红黑树当作一颗二叉查找树,将该节点从二叉查找树中删除;然后,通过"旋转和重新着色"等一系列来修正该树,使之重新成为一棵红黑树。详细描述如下:
第一步:将红黑树当作一颗二叉查找树,将节点删除。
这和"删除常规二叉查找树中删除节点的方法是一样的"。分3种情况:
① 被删除节点没有儿子,即为叶节点。那么,直接将该节点删除就OK了。
② 被删除节点只有一个儿子。那么,直接删除该节点,并用该节点的唯一子节点顶替它的位置。
③ 被删除节点有两个儿子。那么,先找出它的后继节点;然后把“它的后继节点的内容”复制给“该节点的内容”;之后,删除“它的后继节点”。在这里,后继节点相当于替身,在将后继节点的内容复制给"被删除节点"之后,再将后继节点删除。这样就巧妙的将问题转换为"删除后继节点"的情况了,下面就考虑后继节点。 在"被删除节点"有两个非空子节点的情况下,它的后继节点不可能是双子非空。既然"的后继节点"不可能双子都非空,就意味着"该节点的后继节点"要么没有儿子,要么只有一个儿子。若没有儿子,则按"情况① "进行处理;若只有一个儿子,则按"情况② "进行处理。
第二步:通过"旋转和重新着色"等一系列来修正该树,使之重新成为一棵红黑树。
因为"第一步"中删除节点之后,可能会违背红黑树的特性。所以需要通过"旋转和重新着色"来修正该树,使之重新成为一棵红黑树。
//返回以node为根的树的最小值所在节点
public Node mininum(Node node){
if(node.leftChild==null){
return node;
}return mininum(node.leftChild);
}
//删除以最小节点并返回删除后新的树的根
public Node removeMin(Node node){
if(node.leftChild==null){
Node rightNode=node.rightChild;
node.rightChild=null;
size--;
return rightNode;
}
node.leftChild=removeMin(node.leftChild);
return node;
}
//删除健为Key的节点
public V remove(K key){
Node node=getNode(root, key);
if(node!=null){
return node.value;
}
return null;
}
public Node remove(Node node,K key){
if(node==null){
return null;
}
if(key.compareTo(node.key)<0){
node.leftChild=remove(node.leftChild,key);
return node;
}else if(key.compareTo(node.key)>0){
node.rightChild=remove(node.rightChild,key);
return node;
}else{
//待删除节点的左子树为空
if(node.leftChild==null){
Node rightNode=node.rightChild;
node.rightChild=null;
size--;
return rightNode;
}
//待删除节点的右子树为空
if(node.rightChild==null){
Node leftNode=node.leftChild;
node.leftChild=null;
size--;
return leftNode;
}
//待删除节点的左右字数均不为空
//找到待删除节点右子树的最小节点并代替待删除节点的位置
Node successor=mininum(node.rightChild);
successor.rightChild=removeMin(node.rightChild);
successor.leftChild=node.leftChild;
node.leftChild=node.rightChild=null;
return successor;
}
}