红黑树,本质上来说就是一棵二叉查找树,但它在二叉查找树的基础上增加了着色和相关的性质使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(log n)。红黑树相对于AVL树来说,牺牲了部分平衡性以换取插入/删除操作时少量的旋转操作,整体来说性能要优于AVL树。
image.png
红黑树本质上还是一颗二叉查找树,所以在插入/删除的操作上还是和普通二叉树插入的方式一致,只是在插入/删除完成后需要完成一些着色和旋转的Fix操作,来保证二叉树自平衡,由于Fix操作达到自平衡的程度和操作的差异,所以自平衡二叉树也衍生了很多种类。
修复情况有三种:
如果当前结点的父结点是红色且祖父结点的另一个子结点(叔叔结点)是红色 如下图所示:
Case1修复前
Case1修复后
当前结点的父结点是红色,叔叔结点是黑色,当前结点是其父结点的右子
Case2修复前
Case2修复后
当前结点的父结点是红色,叔叔结点是黑色,当前结点是其父结点的左子
Case3修复前
Case3修复后
插入Fixup代码如下:
/** From CLR */
private void fixAfterInsertion(TreeMapEntry<K,V> x) {
// 新插入节点先默认为红色
x.color = RED;
// 只有当插入节点的父节点是红色,才需要进行Fixup
while (x != null && x != root && x.parent.color == RED) {
// 如果是父节点是祖父节点的左子树
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
// 得到父节点的兄弟节点-Uncle节点
TreeMapEntry<K,V> y = rightOf(parentOf(parentOf(x)));
// 如果Uncle为红色的话,说明父节点、子节点、Uncle节点都是红色
if (colorOf(y) == RED) {
// 则将父节点和Uncle节点设置成黑色
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
// 将祖父节点修改成红色
setColor(parentOf(parentOf(x)), RED);
// 再检查祖父节点与其父节点的特性
x = parentOf(parentOf(x));
} else {
// 如果Uncle是黑色的话,因为原来没有叶子节点的时候,默认的nil是黑色的,如果加了一个红色节点,则会破坏性质5,导致到叶子节点的黑色节点数量不一致
// 如果插入的节点是右子树,则先将父节点左旋,也就是旋转后,将原有的父节点做为新插入节点
if (x == rightOf(parentOf(x))) {
x = parentOf(x);
rotateLeft(x);
}
// 将该节点的父节点设置成BLACK
setColor(parentOf(x), BLACK);
// 将祖父节点设置成红色
setColor(parentOf(parentOf(x)), RED);
// 再将祖父节点右转,因为Uncle节点必然是黑色的,所以当父节点着成红色、右旋后,就可以保证旋转后的树能够保持左右子树平衡
rotateRight(parentOf(parentOf(x)));
}
} else {
// 如果是父节点是祖父节点的右子树
// 得到父节点的兄弟节点,Uncle节点
TreeMapEntry<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;
}
删除的FixUp会有四种情况(N为待删除的节点) 而对于删除操作的Fixup只针对黑节点的删除,因为红色节点被删除,并不会影响红黑树的所有特性,只有当黑节点被删除的时候,可能会影响特性4或者特性5. 因为黑节点被删除后,可能该节点还有红色的子节点链接后,会导致两个红色节点,而且也会影响从根节点到每个叶子节点的黑色节点的个数。
而对于黑节点丢失的修复方案有以下两种,其中Sib为Sibling-兄弟节点:
PS:从Parent转的方案,有可能会把从根节点到叶子节点的黑节点数量减少,因为如果一直遍历直到根节点都没有红色节点的话,由于将Sib节点从黑变成了红,也就相当于整体的所有左右子树的黑色节点都减少1
直到遇到红色的Parent,将该节点着成黑色来填补-1的黑节点树或者遇到Root节点,将整体的从根到叶子节点黑节点数量-1
所以对于红黑树删除的Rebalance-FixUp操作对应的上面两种方案: Case1是为了平衡左右子树的高度,并且为将Sib为红色的情况转成Sib为黑色的情况 方案1从Parent的红色节点转成黑色节点对应着Case2 方案2从Sib拿红色节点转成黑色节点对应着Case3&Case4
为了将Sib为红色的的情况统一转成Sib为黑色的情况处理而执行。因为旋转完之后,可以保证需要调整的节点的Sib节点必然是黑色,因为红黑树不可能有两个相邻的红色节点
Case1修复前
Case1修复后
如果Sib的两个子节点都是黑色,就代表着没有红色的子节点可以转成黑色节点了,因为一旦把某个黑色节点给到另外一边,则会破坏性质5,导致从根节点到每个叶子节点的黑色节点数不一样。
对于这种情况的处理,选择将Sib节点(节点D)转成红色,并且对Parent节点递归进行重新调整:
直到遇到红色的Parent,将该节点着成黑色来填补-1的黑节点树或者遇到Root节点,将整体的从根到叶子节点黑节点数量-1
Case2修复前
Case2修复后
如果Sib的两个子节点都不是黑色的话,则说明可以通过旋转的方式将Sib的红色子节点拿过来填充删除的黑色节点。
而如果Sib的右节点为黑色的话,则说明左节点肯定为红色,不然就会走到Case2中。而做这一步调整只是为了保证,Sib的右节点是红色的。从而进入到Case4的修复中。
Case3的修复操作并不会影响当前树的黑节点数量,只是将左子树的红色节点,转移到了右子树,对于着色的操作,因为只调整Sib以及Sib的左右节点的颜色,所以在旋转后,并不会影响它们子树的红黑色平衡
Case3修复前
Case3修复后
通过Case1和Case3的着色&旋转,最终达到Case4的状态。也就是达到Sib为黑,并且Sib的右节点为红色,左节点无所谓,因为经过Case3的调整,Sib的左节点无论是红色/黑色都并不会影响黑节点的数量。
于是,通过旋转的方式将父节点转移到左子树,而将右子树的节点转移到父节点,也就可以理解成从右子树把一个红色节点转移到右子树,并且成为了黑色节点。就如图中的B节点
Case4修复前
Case4修复后