概述
本文从java的源码出发分析红黑树调整算法,并不适合对红黑树毫无认知的同学,对红黑树和二叉树有一定认知后,能更清晰红黑树分析问题的逻辑。本文从原则上不带图分析,可以从其他文章参考。本文所有代码节选自java.util.TreeMap中。
红黑树特点
(1)每个节点或者是黑色,或者是红色。(红黑树定义)
(2)根节点是黑色。(终结位置)
(3)每个叶子节点(NIL)是黑色。(方便红黑树)算法实现)
(4)如果一个节点是红色的,则它的子节点必须是黑色的。(为插入服务)
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点(为删除服务)
以上为红黑树的定义,红黑树作为平衡二叉树的佼佼者,插入,删除效率更高,不需要遍历整棵树,同时查找效率下降很少,基于统计得出性能最高的平衡二叉树。
红黑树插入调整算法
上面这个是TreeMap的插入调整算法,说明:x是本节点,y是叔叔节点,左右两边类似,我只说明一边。最后一句也是了我们启发如果操作没有副作用,且操作很短,不如不用判断了直接操作。
private void fixAfterInsertion(Entry<K,V> x) {
//插入节点改为红色
x.color = RED;
//到达根节点则结束修复直接涂黑,并且存在双红情况才需要修复
while (x != null && x != root && x.parent.color == RED) {
//父亲和爷爷的关系,会影响旋转方向,得保证红色父亲和红色自己和黑色爷爷在一条直线上,再开始涂色旋转
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
//y是叔叔
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
//父亲和叔叔都是红色,爷爷肯定黑色,那么就把爷爷变成红色把问题丢给爷爷
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
//如果不在一条直线上,就通关旋转变成一条直线
if (x == rightOf(parentOf(x))) {
x = parentOf(x);
rotateLeft(x);
}
//在一条直线就变成黑色父亲两个红色儿子,问题解决。
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
} else {
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}
//循环到根节点,但是此时最顶端还是红色的问题没解决,就涂成黑色
//这样免去了判断
root.color = BLACK;
}
以下是看起来更加清楚的Ç的递归实现,可以把实现细节更清楚,我特地去掉了很多可以复用的地方。
//回归平衡
void makebanlence(Bitree &T){
//如果是根节点
if (getFatherNode(T) == NULL){
T->color = CBLACK;
return;
}
//如果父亲就是黑色,不用变
else if (getFatherNode(T)->color == CBLACK){
return;
}
/*当前节点的父节点是红色,且当前节点的祖父节点的另一个子节点(叔叔节点)也是红色。*/
else if (getFatherNode(T)->color == CRED &&getUncleNode(T)->color == CRED){
//把父亲和叔叔变黑,爷爷变红,把问题丢给爷爷
getFatherNode(T)->color = CBLACK;
getUncleNode(T)->color = CBLACK;
getGFatherNode(T)->color = CRED;
T = getGFatherNode(T);
//进入下一次判断
makebanlence(T);
}
//父节点是祖父结点的左子树
else if (getGFatherNode(T)->left == getFatherNode(T)){
/*当前节点的父节点是红色,叔叔节点是黑色,且当前节点是其父节点的右孩子。*/
if (getFatherNode(T)->color == CRED&&getUncleNode(T)->color == CBLACK&&getFatherNode(T)->right == T){
//此时连续的双红和爷爷不在同一条直线上
T = getFatherNode(T);
T = rotL(T)->left;
//进入下一次判断
makebanlence(T);
}
/*当前节点的父节点是红色,叔叔节点是黑色,且当前节点是其父节点的左孩子。*/
else if (getFatherNode(T)->color == CRED&&getUncleNode(T)->color == CBLACK&&getFatherNode(T)->left == T){
//把祖孙三代变成黑色父亲两个红色儿子,问题解决。
getFatherNode(T)->color = CBLACK;
getGFatherNode(T)->color = CRED;
T= rotR(getGFatherNode(T));
//进入下一次判断,这次纯属多余
makebanlence(T);
}
}
//父节点是祖父结点的右子树
else if (getGFatherNode(T)->right == getFatherNode(T)){
/*当前节点的父节点是红色,叔叔节点是黑色,且当前节点是其父节点的左孩子。*/
if (getFatherNode(T)->color == CRED&&getUncleNode(T)->color == CBLACK&&getFatherNode(T)->left == T){
T = getFatherNode(T);
T = rotR(T)->right;
//进入下一次判断
makebanlence(T);
}
/*当前节点的父节点是红色,叔叔节点是黑色,且当前节点是其父节点的右孩子。*/
else if (getFatherNode(T)->color == CRED&&getUncleNode(T)->color == CBLACK&&getFatherNode(T)->right == T){
getFatherNode(T)->color = CBLACK;
getGFatherNode(T)->color = CRED;
T = rotL(getGFatherNode(T));
//进入下一次判断
makebanlence(T);
}
}
}
红黑树删除调整算法
删除的调整算法相比插入的更加简单,因为把问题限定在了父亲以下的关系里面。
private void fixAfterDeletion(Entry<K,V> x) {
//只有补上的是黑色才需要调整,所有的x=root都可以退出
while (x != root && colorOf(x) == BLACK) {
if (x == leftOf(parentOf(x))) {
Entry<K,V> sib = rightOf(parentOf(x));
//像兄弟借了一个深度把,把从兄弟为红的问题全部转化为兄弟为黑
if (colorOf(sib) == RED) {
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateLeft(parentOf(x));
sib = rightOf(parentOf(x));
}
//本来被删除数值深度最多比兄弟大一,所以兄弟肯定有孩子,兄弟变红直接兄弟深度减一
if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
//如果一黑一红且左边红这样就逆着等下的旋转方向,所以把红色转到右边
if (colorOf(rightOf(sib)) == BLACK) {
setColor(leftOf(sib), BLACK);
setColor(sib, RED);
rotateRight(sib);
sib = rightOf(parentOf(x));
}
//保证父亲节点颜色不变,两个儿子都变为黑色,防止爷爷和父亲冲突,在DR所在深度增加1
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(rightOf(sib), BLACK);
rotateLeft(parentOf(x));
x = root;
}
} else { // symmetric
Entry<K,V> sib = leftOf(parentOf(x));
if (colorOf(sib) == RED) {
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateRight(parentOf(x));
sib = leftOf(parentOf(x));
}
if (colorOf(rightOf(sib)) == BLACK &&
colorOf(leftOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
if (colorOf(leftOf(sib)) == BLACK) {
setColor(rightOf(sib), BLACK);
setColor(sib, RED);
rotateLeft(sib);
sib = leftOf(parentOf(x));
}
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(leftOf(sib), BLACK);
rotateRight(parentOf(x));
x = root;
}
}
}
//解决根结点永远为黑色,和删了黑色节点却补上红色的结点
setColor(x, BLACK);
总结
双红情况为了插入算法服务,对应性质4.补上黑色情况为了删除算法服务,对应性质5。
无论是插入还是删除的调整算法,都是把复杂的可能性,变成最后几种情况,化繁为简,步步为营,这种思想不应该局限于红黑树,也是程序设计的基本逻辑。