红黑树的结点增删改查效率非常优良,都为logn,其应用十分广泛:
1. Linux内核进程调度由红黑树管理进程控制块。
2. Epoll用红黑树管理事件块。
3. nginx服务器用红黑树管理定时器。
4. C++ STL中的map和set的底层实现为红黑树。
5. Java中的TreeMap和TreeSet由红黑树实现。
6. Java8开始,HashMap中,当一个桶的链表长度超过8,则会改用红黑树。
红黑树的操作比较复杂,实现起来比较麻烦,看了很多篇别人写的关于红黑树的博客,插入操作比较容易理解,但是删除操作都非常乱,本文不会讲解二叉排序树,左旋右旋等基础知识,将直接从红黑树的插入操作和删除操作开始讲解,并附上源码,算是自己对红黑树的一个总结。
想要理解红黑树的操作,以上性质必须滚瓜烂熟。
先定义红黑树这个类的成员属性:
public class RBTree<T extends Comparable<T>> {
private RBNode root;//根结点指针
private static final boolean RED = false;
private static final boolean BLACK = true;
class RBNode<T extends Comparable<T>> {//树的结点
boolean color;//红或黑
T key;
RBNode left;//左孩子指针
RBNode right;//右孩子指针
RBNode parent;//父结点指针,红黑树经常涉及到兄弟,叔叔,侄子,有个父结点指针方便操作。
public RBNode(boolean color, T key, RBNode left, RBNode right, RBNode parent) {
this.color = color;
this.key = key;
this.left = left;
this.right = right;
this.parent = parent;
}
}
//左旋
private void leftRotate(RBNode x) {
RBNode y = x.right;
x.right = y.left;
if(y.left != null)
y.left.parent = x;
y.parent = x.parent;
if(x.parent != null) {
if(x.parent.left == x)
x.parent.left = y;
else
x.parent.right = y;
}else {
this.root = y;
}
y.left = x;
x.parent = y;
}
//右旋
private void rightRotate(RBNode x) {
RBNode y = x.left;
x.left = y.right;
if(y.right != null)
y.right.parent = x;
y.parent = x.parent;
if(x.parent != null) {
if(x.parent.left == x)
x.parent.left = y;
else
x.parent.right = y;
}else {
this.root = y;
}
y.right = x;
x.parent = y;
}
}
看了N篇博客,只感觉下面这篇的删除操作讲得非常好,先把链接贴出来再说:红黑树删除操作
然后是我自己的理解。
首先红黑树是二叉排序树,按排序树的方式删除一个结点,有三种情况:
(1)如果待删除的结点是叶子结点,则直接删除即可。
(2)如果待删除的结点只有一个孩子,则将父结点的指针指向它的孩子。
(3)如果待删除的结点有两个孩子,则可以找它的后继,将值覆盖过来,之后情况转变成删除前驱或者后继结点,回到(1)和(2)。
那么再根据颜色的不同,接下来情况就非常多了,这就是红黑树删除操作麻烦的地方,切忌浮躁,务必静下心来仔细分析。
第三种情况:
此情况因为可以转变成前面两种情况,相当于已经解决了。
第二种情况(一个孩子):
此情况是个软柿子,可以先捏掉:
根据性质5,可以推断出的是,待删除结点必定是黑色(红色结点不可能只有一个孩子),且唯一的子树一定是一个红色孩子。所以这种情况可以在删除时就处理掉,用红色孩子顶替待删除结点,再将其涂成黑色。
变成:
第一种情况(叶子):
首先,如果待删除结点为红色,则直接结束了。
如果待删除结点为黑色,这种情况下,此条分支不可能通过涂色的方式弥补缺少的黑色,所以要判断其兄弟和侄子的状况,希望通过兄弟那边分支的旋转,来保持黑色的数量:
(1)如果兄弟是黑色。
这种情况下,一旦deleted被删除,左边就比右边少了一个黑色结点,parent红色或者黑色都有可能,那两个侄子要么为null,要么为红色。
同样先捏软柿子:
(2)如果兄弟是红色,那么可以先转换成黑兄的情况。
可以肯定的是,那两个侄子肯定不为null。
parent左旋一下,然后parent和brother交换颜色,之后brother指向最下面那个结点。这样一来,就变成了上面的黑兄情况。
到这里为止,删除操作就全部分析完,这当然只是左孩子位置的情况,对称情况的分析是一样的。
//删除结点
private void deleteNode(RBNode node) {
//replace表示删除之后顶替上来的结点
//parent为replace结点的父结点
RBNode replace = null, parent = null;
// 如果删除的结点左右孩子都有
if (node.left != null && node.right != null) {
RBNode succ = null;
for (succ = node.right; succ.left != null; succ = succ.left);//找到后继
node.key = succ.key;//覆盖值
deleteNode(succ);//递归删除,只可能递归一次
return;
} else {// 叶子或只有一个孩子的情况
// 如果删除的是根,则root指向其孩子(有一个红孩子或者为nil)
if (node.parent == null) {
// 如果有左孩子,那根就指向左孩子,没有则指向右孩子(可能有或者为NIL)
this.root = (node.left != null ? node.left : node.right);
replace = this.root;
if (this.root != null)
this.root.parent = null;
} else {// 非根情况
RBNode child = (node.left != null ? node.left : node.right);
if (node.parent.left == node)
node.parent.left = child;
else
node.parent.right = child;
if (child != null)
child.parent = node.parent;
replace = child;
parent = node.parent;
}
}
//如果待删除结点为红色,直接结束
if (node.color == BLACK)
deleteFixUp(replace, parent);
}
private void deleteFixUp(RBNode replace, RBNode parent) {
RBNode brother = null;
// 如果顶替结点是黑色结点,并且不是根结点。
//由于经过了上面的deleteNode方法,这里面parent是一定不为null的
while ((replace == null || replace.color == BLACK) && replace != this.root){
//左孩子位置的所有情况,
if (parent.left == replace) {
brother = parent.right;
// case1 红兄,brother涂黑,parent涂红,parent左旋,replace的兄弟改变了,变成了黑兄的情况
if (brother.color == RED) {
brother.color = BLACK;
parent.color = RED;
leftRotate(parent);
brother = parent.right;
}
// 经过上面,不管进没进if,兄弟都成了黑色
// case2 黑兄,且兄弟的两个孩子都为黑
if ((brother.left == null || brother.left.color == BLACK) && (brother.right == null || brother.right.color == BLACK)) {
// 如果parent此时为红,则把brother的黑色转移到parent上
if (parent.color == RED) {
parent.color = BLACK;
brother.color = RED;
break;
} else {// 如果此时parent为黑,即此时全黑了,则把brother涂红,导致brother分支少一个黑,使整个分支都少了一个黑,需要对parent又进行一轮调整
brother.color = RED;
replace = parent;
parent = replace.parent;
}
} else {
// case3 黑兄,兄弟的左孩子为红色
if (brother.left != null && brother.left.color == RED) {
brother.left.color = parent.color;
parent.color = BLACK;
rightRotate(brother);
leftRotate(parent);
// case4 黑兄,兄弟的右孩子为红色
} else if (brother.right != null && brother.right.color == RED) {
brother.color = parent.color;
parent.color = BLACK;
brother.right.color = BLACK;
leftRotate(parent);
}
break;
}
} else {//对称位置的情况,把旋转方向反回来
brother = parent.left;
// case1 红兄,brother涂黑,parent涂红,parent左旋,replace的兄弟改变了,变成了黑兄的情况
if (brother.color == RED) {
brother.color = BLACK;
parent.color = RED;
rightRotate(parent);
brother = parent.left;
}
// 经过上面,不管进没进if,兄弟都成了黑色
// case2 黑兄,且兄弟的两个孩子都为黑
if ((brother.left == null || brother.left.color == BLACK)
&& (brother.right == null || brother.right.color == BLACK)) {
// 如果parent此时为红,则把brother的黑色转移到parent上
if (parent.color == RED) {
parent.color = BLACK;
brother.color = RED;
break;
} else {// 如果此时parent为黑,即此时全黑了,则把brother涂红,导致brother分支少一个黑,使整个分支都少了一个黑,需要对parent又进行一轮调整
brother.color = RED;
replace = parent;
parent = replace.parent;
}
} else {
// case3 黑兄,兄弟的左孩子为红色,右孩子随意
if (brother.right != null && brother.right.color == RED) {
brother.right.color = parent.color;
parent.color = BLACK;
leftRotate(brother);
rightRotate(parent);
// case4 黑兄,兄弟的右孩子为红色,左孩子随意
} else if (brother.left != null && brother.left.color == RED) {
brother.color = parent.color;
parent.color = BLACK;
brother.left.color = BLACK;
rightRotate(parent);
}
break;
}
}
}
//这里可以处理到删除结点为只有一个孩子结点的情况,如果是根,也会将其涂黑。
if (replace != null)
replace.color = BLACK;
}
可以看到,红黑树的删除操作非常复杂的,情况较多,但是效率非常好。了解甚至手写红黑树是一种挑战也是一种很好的锻炼,并不光是为了笔试面试(估计也问不了这个),所谓练拳不练功,到老一场空,在繁忙的日常工作中,多去了解一点底层的东西可以加深对各种技术的理解。