辅助方法
由于红黑树的结点有颜色,所以要有一些方法来操作颜色,并且红黑树要用到兄弟结点,所以把获取某个结点的兄弟结点也抽象成一个方法,方便写代码的时候专注于红黑树的实现逻辑
辅助方法的代码
package com.plasticine.tree;
import java.util.Comparator;
public class RBTree<E> extends BBST<E> {
private static final boolean RED = false;
private static final boolean BLACK = true;
public RBTree() {
this(null);
}
public RBTree(Comparator<E> comparator) {
super(comparator);
}
// ========================================= 辅助方法 =========================================
/**
* 给红黑树的结点上色
*
* @param node 普通的二叉树结点
* @param color 颜色
*/
private Node<E> color(Node<E> node, boolean color) {
if (node == null) return null;
((RBNode<E>) node).color = color;
return node;
}
/**
* 把红黑树的结点染成红色
*
* @param node 普通结点
*/
private Node<E> red(Node<E> node) {
return this.color(node, RED);
}
/**
* 把红黑树的结点染成黑色
*
* @param node 普通结点
*/
private Node<E> black(Node<E> node) {
return this.color(node, BLACK);
}
/**
* 返回一个结点的颜色
*
* @param node 目标结点
* @return 目标结点的颜色
*/
private boolean colorOf(Node<E> node) {
return ((RBNode<E>) node).color;
}
/**
* 判断一个结点是否是红色的
*
* @param node 目标结点
* @return 是否是红色
*/
private boolean isRED(Node<E> node) {
return this.colorOf(node) == RED;
}
/**
* 判断一个结点是否是黑色的
*
* @param node 目标结点
* @return 是否是黑色
*/
private boolean isBLACK(Node<E> node) {
return this.colorOf(node) == BLACK;
}
/**
* 获取某个节点的兄弟结点
*
* @param node 目标结点
* @return 目标结点的兄弟结点
*/
private Node<E> sibling(Node<E> node) {
if (node.isLeftChild()) return node.parent.right;
if (node.isRightChild()) return node.parent.left;
return null;
}
// ========================================= 辅助方法 =========================================
/**
* 红黑树的结点
*/
private static class RBNode<E> extends Node<E> {
boolean color = RED;
public RBNode(E element, Node<E> parent) {
super(element, parent);
}
}
}
添加结点
情况分析
- 有12种可能,如下图所示
由于红黑树可以类比成4阶B树,所以都可以把红色结点提上来,和黑色结点组成一个B树的结点,那么添加结点的时候就只有以上的12种情况,分别是17、33、50、72、88的左右均可添加,以及46的左、76的右这两种情况
-
按照父结点的颜色划分,可以分为两种情况
-
父结点是黑色结点 -- 有四种可能
像这种情况,直接把结点插进去即可,
afterAdd
不需要做任何操作 -
父结点是红色结点
而像这种情况,就又可以分为两种情况考虑,区分的标准是叔父节点(即父结点的兄弟结点)的颜色
-
叔父节点是红色
如图中的10、20、30、36这四种情况,叔父节点都是红色的,这个时候只需要进行两步操作
- 把父结点和叔父节点染成黑色
- 把祖父节点染成红色,并提到祖父节点的父结点那一层(也就是发生了上溢),这里提上去的过程可以看成把25插入到25的上一层中这种情况,即递归调用
afterAdd
,因为再往上一层插无非也就是那12种情况中的一种罢了,不会有别的可能
-
叔父节点是黑色(虚拟想象出的null结点视为黑色)
如图中的48、52、60、74这四种情况
-
以祖父结点出发,根据当前插入结点相对于祖父节点的位置来区分,这点和
AVL树
的左旋转右旋转情况一致,分为LL、LR、RL、RR四种LL --> 60 LR --> 74 RL --> 48 RR --> 52
-
-
-
继承关系重整
-
由于AVL树和红黑树都用到了旋转的操作,并且他们都有共同的特性 -- 平衡,所以可以抽象成一个平衡二叉搜索树这个类,记为
BBST(Balanced Binary Search Tree)
BBST代码
package com.plasticine.tree; import java.util.Comparator; public class BBST<E> extends BinarySearchTree<E> { public BBST() { this(null); } public BBST(Comparator<E> comparator) { super(comparator); } /** * 左旋转 * * @param grand 左旋转操作的 grand 结点 */ protected void rotateLeft(Node<E> grand) { Node<E> parent = grand.right; Node<E> child = parent.left; // 左旋转 grand.right = child; parent.left = grand; afterRotate(grand, parent, child); } /** * 右旋转 * * @param grand 右旋转操作的 grand 结点 */ protected void rotateRight(Node<E> grand) { Node<E> parent = grand.left; Node<E> child = parent.right; // 右旋转 grand.left = child; parent.right = grand; afterRotate(grand, parent, child); } /** * 旋转后的共同操作 --> 无论左旋转还是右旋转都是一样的 */ protected void afterRotate(Node<E> grand, Node<E> parent, Node<E> child) { // 更新整颗子树的根节点的 left/right 指向 if (grand.isLeftChild()) { grand.parent.left = parent; } else if (grand.isRightChild()) { grand.parent.right = parent; } else { root = parent; } // 更新 grand parent child 的父结点 // parent parent.parent = grand.parent; // child if (child != null) child.parent = grand; // grand grand.parent = parent; } /** * 统一旋转操作 */ protected void rotate(Node<E> r, Node<E> b, Node<E> c, Node<E> d, Node<E> e, Node<E> f) { // 构造 a b c b.right = c; if (c != null) c.parent = b; // 构造 e f g f.left = e; if (e != null) e.parent = f; // 构造 b d f if (r.isLeftChild()) r.parent.left = d; else if (r.isRightChild()) r.parent.right = d; else root = d; d.left = b; d.right = f; d.parent = r.parent; b.parent = d; f.parent = d; } }
- AVL树继承自BBST后要做出一点修改,因为原本的旋转代码中,涉及到更新高度的部分,而高度是AVL树特有的,红黑树并没有,所以抽象之后要把更新高度的代码去掉,让AVL树重写旋转的代码,用super调用完父类的方法后,加上更新高度的部分即可
AVL树重写的部分
/** * AVL 树在旋转后要进行更新高度的操作 */ @Override protected void afterRotate(Node<E> grand, Node<E> parent, Node<E> child) { super.afterRotate(grand, parent, child); // 更新高度 --> 先后更新 grand parent updateHeight(grand); updateHeight(parent); } /** * AVL 树统一旋转操作之后要更新高度 */ @Override protected void rotate(Node<E> r, Node<E> b, Node<E> c, Node<E> d, Node<E> e, Node<E> f) { super.rotate(r, b, c, d, e, f); updateHeight(b); updateHeight(f); updateHeight(d); }
添加结点代码实现
/**
* 红黑树添加结点后的处理 -- 用于维护红黑树保持红黑树的 5 条性质
*
* @param node 新添加的结点
*/
@Override
protected void afterAdd(Node<E> node) {
Node<E> parent = node.parent;
// 添加的是根节点 -- 根节点需要染成黑色
if (parent == null) {
black(node);
return;
}
// 父结点是黑色结点 -- 有四种可能
if (isBLACK(parent)) return; // 不需要做任何操作,直接插入即可
// 父结点是红色结点
Node<E> uncle = sibling(parent); // 叔父节点
Node<E> grand = parent.parent; // 祖父节点
// 1. 叔父节点是红色 --> 发生上溢
if (isRED(uncle)) {
// 把父结点和叔父节点染成黑色
black(parent);
black(uncle);
// 祖父节点染成红色
red(grand);
// 祖父节点插入到上一层
afterAdd(grand);
return;
}
// 2. 叔父节点是黑色 --> 旋转,需要先判断是哪种旋转情况
if (parent.isLeftChild()) { // L
if (node.isLeftChild()) { // LL
black(parent);
} else { // LR
rotateLeft(parent);
black(node);
}
rotateRight(grand);
} else { // R
if (node.isLeftChild()) { // RL
rotateRight(parent);
black(node);
} else { // RR
black(parent);
}
rotateLeft(grand);
}
}
删除结点
删除结点时的情况分析
-
由于是在二叉搜索树删除某个结点后才进行红黑树的相应调整,而二叉搜索树删除结点的时候,只会删除度为1或0的结点,如果度为2,最终删除的实际上还是度为1或0的结点,又由于红黑树等价于4阶B树,所以红黑树删除结点时遇到的情况也就三种,如下图所示
删除红色结点
- 直接删除即可,不会影响红黑树的五条性质
删除有一个红色子结点的黑色结点
将该红色子结点染成黑色,作为新的B树结点即可
-
这里就需要对
afterRemove()
进行重构了,因为这种情况删除时要用到用于替换被删除结点的红色结点,所以要给该方法传入用于替换的那个结点,也就是图中删除46时,50用来替换它,那么需要把50染成黑色,而原本的afterRemove()
方法只传入了46,并没有传入50,因此没办法对50进行操作/** * 删除结点后,为了维护红黑树的 5 条性质而作出的额外操作 * * @param node 被删除的结点 * @param replacement 用于替换被删除结点的结点 */ @Override protected void afterRemove(Node<E> node, Node<E> replacement) { // 删除的是红色结点 --> 直接删除即可,不用做额外的处理 if (isRED(node)) return; // 能来到这说明被删除的结点是黑色的,那么就要分该黑色结点是否有子结点 if (isRED(replacement)) { // 把用于替换的结点染成黑色即可 black(replacement); return; } }
删除黑色叶子结点
分两种可能
-
被删除结点的
sibling
是黑色的,如删除图中的88这个结点,它的sibling
是76,为黑色sibling
有多余结点可以借出来
这种情况好处理,由于删除88后,88所在的B树结点发生了下溢(因为4阶B树非根节点里的结点个数是1~3个,而删除88后变为了0个),而
sibling
结点76这一个B树结点有两个结点,可以“借”一个给88这个B树结点,所以通过旋转即可完成- 图中这种情况属于LR,对76左旋转后再对80右旋转
- 旋转后的中心节点变为了78,78要继承自原本中心节点的颜色,原本的中心节点是80,所以旋转后78仍然为红色
- 为了保持红黑树的性质,还需要在旋转后把左右B树结点的中心节点染成黑色
还有其他情况,如LL、RR、RL等,只是在旋转操作时有区别,整体的操作还是一致的,就不解释了
-
sibling
没有多余结点可以借出来父结点是红色
比如这种情况下,删除88结点,它的兄弟结点是76,没有多余结点可以借出来,又因为88发生下溢,所以需要把父结点,也就是80拉下来和
sibling
合并,也就是80(父结点)变为黑色,76(兄弟结点)变为红色即可,处理后如下图所示父结点是黑色
[图片上传失败...(image-e624d7-1630068498177)]
删除88,把父结点80拉下来后,76要变成红色结点,如下图所示
这时原本80位置的B树结点就发生了下溢,没关系,只用递归地去调用一次
afterRemove()
,把80作为被删除结点看待,传入即可
-
被删除结点的
sibling
是红色的,如下图所示
删除88这个结点,从红黑树的角度看,它的
sibling
是红色结点55,而从B树的角度来看,88的sibling
是76如果是在B树的角度看的话,直接套用前面的
sibling
是黑色结点的情况即可结局-
所以现在关键就是要在红黑树的角度来看,让76成为88的
sibling
,也就是要实现“我的侄子变成我的兄弟”这种骚操作,那么怎么进行这种B树父结点转成红黑树兄弟节点的操作呢?-
从被删除结点88的父结点出发,往左再往左,也就是
LL
的情况,对80进行右旋转后就可以让76变为80的子结点,这样88和76就是兄弟结点的关系了,如下图所示 这时候就回到了前面的兄弟结点是黑色的情况了,直接复用那部分的代码即可,不用写新的代码
-
代码
/**
* 删除结点后,为了维护红黑树的 5 条性质而作出的额外操作
*
* @param node 被删除的结点
* @param replacement 用于替换被删除结点的结点
*/
@Override
protected void afterRemove(Node<E> node, Node<E> replacement) {
// 删除的是红色结点 --> 直接删除即可,不用做额外的处理
if (isRED(node)) return;
// 能来到这说明被删除的结点是黑色的,那么就要分该黑色结点是否有子结点
// 有一个红色子结点
if (isRED(replacement)) {
// 把用于替换的结点染成黑色即可
black(replacement);
return;
}
Node<E> parent = node.parent;
if (parent == null) return; // 删除的是根节点
// 来到这说明删除的是黑色叶子结点 --> 要根据 sibling 的颜色来处理
// 获取 sibling
/*
父结点的左边是空的说明被删除结点 node 是左子节点
父结点的左边非空,那可能是下溢传递时的递归调用的情况,这时候直接判断传进来的结点是左还是右即可
*/
boolean isLeft = parent.left == null || node.isLeftChild();
Node<E> sibling = isLeft ? parent.right : parent.left;
// 左和右的情况是对称的
if (isLeft) { // 被删除结点在左边,兄弟结点在右边
if (isRED(sibling)) { // 兄弟结点是红色 --> 把兄弟结点转成黑色的情况,再由后面统一处理
black(sibling);
red(parent);
rotateLeft(parent);
// 旋转之后兄弟结点发生变化,需要更新
sibling = parent.right;
}
// 能来到这说明兄弟结点一定是黑色的,即便是红色也已经被转成黑色的情况了
// 判断兄弟结点有无多余结点可以借给我
if (isBLACK(sibling.left) && isBLACK(sibling.right)) { // 兄弟结点没有多余结点
boolean parentIsBlack = isBLACK(parent);
// 将父结点和兄弟结点合并解决下溢
black(parent);
red(sibling);
if (parentIsBlack) { // 父结点和兄弟结点合并后仍会发生下溢
afterRemove(parent, null);
}
} else { // 兄弟结点至少有一个红色子结点
// 判断兄弟结点左子节点是否是黑色,是的话则先要让兄弟结点左旋转
if (isBLACK(sibling.right)) {
rotateRight(sibling);
sibling = parent.right; // 左旋转之后 sibling 引用要更新
}
// 统一处理父结点右旋转
// 把旋转后的中心节点染成和父结点一样的颜色
color(sibling, colorOf(parent));
black(parent);
black(sibling.right);
rotateRight(parent);
}
} else { // 被删除结点在右边,兄弟结点在左边
if (isRED(sibling)) { // 兄弟结点是红色 --> 把兄弟结点转成黑色的情况,再由后面统一处理
black(sibling);
red(parent);
rotateRight(parent);
// 旋转之后兄弟结点发生变化,需要更新
sibling = parent.left;
}
// 能来到这说明兄弟结点一定是黑色的,即便是红色也已经被转成黑色的情况了
// 判断兄弟结点有无多余结点可以借给我
if (isBLACK(sibling.left) && isBLACK(sibling.right)) { // 兄弟结点没有多余结点
boolean parentIsBlack = isBLACK(parent);
// 将父结点和兄弟结点合并解决下溢
black(parent);
red(sibling);
if (parentIsBlack) { // 父结点和兄弟结点合并后仍会发生下溢
afterRemove(parent, null);
}
} else { // 兄弟结点至少有一个红色子结点
// 判断兄弟结点左子节点是否是黑色,是的话则先要让兄弟结点左旋转
if (isBLACK(sibling.left)) {
rotateLeft(sibling);
sibling = parent.left; // 左旋转之后 sibling 引用要更新
}
// 统一处理父结点右旋转
// 把旋转后的中心节点染成和父结点一样的颜色
color(sibling, colorOf(parent));
black(parent);
black(sibling.left);
rotateRight(parent);
}
}
}