红黑树
参考自:
https://www.cnblogs.com/skywang12345/p/3624343.html
https://blog.csdn.net/m0_37589327/article/details/78518324#commentBox
本项目github地址:
https://github.com/TheRven/RBTree.git
二叉查找树的性质
- 若任意节点的左子树不为空,则左子树上所有节点的值均小于它的根节点的值
- 若任意节点的右子树不为空,则右子树上所有节点的值均大于它的根节点的值
- 任意节点的左右子树也为二叉查找树
- 没有键值相同的节点
红黑树的基本概念
红黑树,是一种二叉查找树,但是在每个节点上增加一个存储位置表示节点的颜色,可以是RED或者是BLACK
- 每个节点要么是黑的,要么是红的
- 根节点是黑的
- 每个叶节点(叶节点是指树尾端NIL指针或NULL节点)都是黑的
- 如果一个节点是红的,那么它的两个子节点都是黑的(红色节点不能连续)
- 对于任意节点而言,其到叶节点数尾端NIL指针的每条路径都包含相同数目的黑节点
红黑树虽然本质上是一颗二叉查找树,但它在二叉查找树的基础上增加了着色和相关的性质使得红黑树相对平衡,从而保证红黑数的查找、插入、删除的时间复杂度最坏为O(logn)
红黑树节点的类作为内部类实现
private class Node<T> {
private T data;
private boolean color;
Node<T> left;
Node<T> right;
Node<T> parent;
public Node(T data, boolean color, Node<T> left, Node<T> right, Node<T> parent) {
this.data = data;
this.color = color;
this.left = left;
this.right = right;
this.parent = parent;
}
public String getColor(){
return this.color ? "黑色" : "红色";
}
}
树的旋转
当对红黑树进行插入和删除操作时,对树的修改可能破坏了红黑树的性质。为了继续保持红黑树的性质,可以通过对节点进行重新着色,以及对树进行相关的旋转操作,来保持平衡
左旋
左旋的代码实现
private void leftRotate(Node<T> x){
if (x == null) {
return;
}
// 对x节点进行左旋操作
// 1.将x的右节点设置为y
Node<T> y = x.right;
if (y == null) {
return;
}
// 2.将y的左子节点移动到x的右子节点
x.right = y.left;
if (y.left != null) {
y.left.parent = x;
}
// 3.将x的父节点设置为y的父节点
y.parent = x.parent;
if (x.parent == null) {
// 如果x的父节点为null,说明是根节点
this.root = y;
} else {
// 有父节点,则对父节点的子节点进行设置
if (x.parent.left == x) {
x.parent.left = y;
} else {
x.parent.right = y;
}
}
// 4.将x设置为y的子节点
x.parent = y;
y.left = x;
}
右旋
private void rightRotate(Node<T> y){
if (y == null) {
return;
}
Node<T> x = y.left;
if (x == null) {
return;
}
y.left = x.right;
if (x.right != null) {
x.right.parent = y;
}
x.parent = y.parent;
if (y.parent == null) {
this.root = x;
} else {
if (y.parent.left == y) {
y.parent.left = x;
} else {
y.parent.right = x;
}
}
y.parent = x;
x.right = y;
}
红黑树的添加
- 将红黑树作为一颗二叉查找树进行插入
- 将插入的节点颜色设置为红色
这样做的目的,至少保证插入后依然存在红黑树的第五特性 - 通过一系列的着色和旋转操作,使之重新成为一颗红黑树
public void insert(T value){
if (value == null) {
return;
}
// 将插入的节点着色为红色
Node<T> node = new Node<>(value, RED, null, null, null);
// 将红黑树作为一颗二叉查找树插入
this.root = insert(node, this.root);
// 通过一系列的旋转着色操作,使其重新成为一颗红黑树
insertFixUp(node);
}
(1)作为二叉树进行插入
private Node<T> insert(Node<T> node, Node<T> root) {
if (node == null) {
return null;
}
if (root == null) {
root = node;
return root;
}
int result = node.data.compareTo(root.data);
if (result > 0) {
node.parent = root;
root.right = insert(node, root.right);
} else if (result < 0) {
node.parent = root;
root.left = insert(node, root.left);
}
return root;
}
(2)调整节点,使之成为一颗红黑树
private void insertFixUp(Node<T> node) {
if (node == null) {
return;
}
fixUp1(node);
}
- 如果插入的节点是根节点,也就是说初始的红黑树为空,直接将该节点标记为黑色
private void fixUp1(Node<T> node){
if (node.parent == null) {
node.color = BLACK;
return;
} else {
fixUp2(node);
}
}
- 如果插入的不是根节点,但是父节点为黑色,此时不需要做任何改动
private void fixUp2(Node<T> node) {
if (isBlack(node.parent)) {
return;
} else {
fixUp3(node);
}
}
- 如果插入的节点的父节点为红色,进行调整。
a. 如果叔叔节点存在且为红色
private void fixUp3(Node<T> node) {
// 如果插入节点的父亲节点是红色的,那么它的祖父节点必然存在且是黑色的
Node<T> parent = node.parent;
Node<T> gParent = parent.parent;
// 获取当前节点的叔叔节点
Node<T> uncle = gParent.left == parent ? gParent.right : gParent.left;
// 如果当前的叔叔节点存在且为红色
if (uncle != null && isRed(uncle)) {
parent.color = BLACK;
uncle.color = BLACK;
gParent.color = RED;
// 此时祖父节点变为红色,可能产生冲突,需要调整,进行递归判断
fixUp1(gParent);
} else {
fixUp4(node);
}
}
b. 如果叔叔节点为黑色或者无叔叔节点:
在正常的红黑树的插入中,不可能存在叔叔节点为黑色,父亲节点为红色的情况
但是在fixUp3中递归的过程中可能会造成这种特殊情况的出现,需要进行调整
(1) 叔叔节点为黑色
(2)无叔叔节点
private void fixUp4(Node<T> node) {
Node<T> parent = node.parent;
Node<T> gParent = parent.parent;
// 目前存在四种情况,
// 1. 父节点为左子节点
// 当前节点为左子节点
// 当前节点为右子节点
// 2. 父节点为右子节点
// ....
// 把删除节点、父亲节点、祖父节点三个节点调整到一条直线上
if (gParent.left == parent && parent.right == node) {
leftRotate(parent);
node = node.left;
} else if (gParent.right == parent && parent.left == node) {
rightRotate(parent);
node = node.right;
}
// 重新指向新的parent和gParent
parent = node.parent;
gParent = parent.parent;
// 调整父节点颜色黑色,祖父节点颜色为红色,在进行旋转
parent.color = BLACK;
gParent.color = RED;
if (gParent.left == parent && parent.left == node) {
rightRotate(gParent);
} else if (gParent.right == parent && parent.right == node) {
leftRotate(gParent);
}
}
红黑树的删除
- 先把红黑树看做二叉查找树进行删除
- 把当前节点删除后的替换节点作为判断条件,进行调整
二叉树的删除
二叉树的删除可以看做三种情况
- 删除的节点没有子节点
- 删除的节点只有单个子节点
- 删除的节点有两个子节点
先讨论第三种情况,我们可以把第三种情况化作前两种,找到当前节点右子节点的最小节点,把最小节点的值赋给要删除的节点,然后把删除的节点重置为最小节点,此时最小节点一定符合前两种情况
第一种情况下,如果删除的节点为根节点,则置根节点为null;如果删除的节点不是根节点,直接删除掉这个节点。这种情况下的替换节点都为null
第二种情况下,直接将单个子节点替换掉要删除的节点,此时替换节点为这个子节点
private void deleteNode(Node<T> node) {
// replace 表示删除后顶替上来的节点
// parent 表示replace的父节点
Node<T> parent = null, replace = null;
// 根据二叉树的删除逻辑
// 共有三种情况
// 1. 删除的节点没有子节点
// 2. 删除的节点有单子节点
// 3. 删除的节点有双子节点
if (node.left != null && node.right != null) {
// 此为第三种情况,可以转换成前两种情况
Node<T> minNode = findMinNode(node.right);
// 将最小节点的值赋给要删除的节点,删除最小节点
node.data = minNode.data;
// 此处转换成了前两种情况,进行一次递归
deleteNode(minNode);
return;
} else {
// 前两种情况,单节点或没有子节点
if (node.parent == null) {
// 如果node节点是根节点
// 找一个子节点替换根节点,此时即使没有子节点也无所谓,直接把root置为null
this.root = node.left == null ? node.right : node.left;
replace = this.root;
if (this.root != null) {
this.root.parent = null;
}
} else {
// node不是根节点
Node<T> child = node.left == null ? node.right : node.left;
if (node.parent.left == node) {
node.parent.left = child;
} else {
node.parent.right = child;
}
if (child != null) {
child.parent = node.parent;
}
replace = child;
// 使用node.parent是因为child有可能是NIL节点
parent = node.parent;
}
}
// 如果要删除的节点为红色,直接结束即可
// 因为走到这一步的node只是单子节点和无子节点的
// 单子节点不可能node为红色
// 无子节点中红色直接删除不需要调整,黑色需要进行调整
if (node.color == BLACK) {
deleteFixUp(replace, parent);
}
}
private Node<T> findMinNode(Node<T> t) {
if (t == null) {
return null;
} else if (t.left == null) {
return t;
}
return findMinNode(t.left);
}
红黑树的调整
删除完节点后,红黑树的结构被破坏,接下来讨论如何修改红黑树的结构
在上面的判断中,需要删除的节点只有在单子节点或者无子节点(删除节点为黑色)的情况下才需要进行调整
传入的参数为replace(替代节点)和parent(替代节点的父节点)
先说最简单的情况,单子节点情况:
在单子节点的情况下,待删除的节点只能是黑色的,子节点只能是红色的,此时把replace颜色改为黑色即可
最复杂的情况是无子节点的情况,无子节点的替代节点只会是NIL
分为以下四种:
-
replace的兄弟节点为红色
-
replace的兄弟节点为黑色,且兄弟节点的两个孩子节点都为黑色或者为NIL
a. 如果父亲节点为红色
b. 如果父亲节点为黑色此时需要替换兄弟节点为红色,但是这种情况会使父节点分支少了一个黑色节点,所以将父节点设置为replace向上进一步调整
3. 兄弟节点为黑色,兄弟节点的左子节点为红色,右节只能为红色或者NIL
4. 兄弟节点为黑色,兄弟节点的右子节点为红色,左子节点为NIL
调整的代码实现
private void deleteFixUp(Node<T> replace, Node<T> parent) {
// 设置replace节点的兄弟节点为brother
Node<T> brother = null;
// 如果replace节点为空,并且不是根节点,这是需要调整的情况,即此while循环调整无子节点的情况
// 这里面replace.color == BLACK的判断是条件是为了后续的循环调整
while ((replace == null || replace.color == BLACK) && replace != this.root) {
if (parent.left == replace) {
brother = parent.right;
// 判断几个兄弟节点的情况
// case 1 : 兄弟节点为红色,那么parent必然为黑色
if (brother.color == RED) {
// 此时交换brother和parent的颜色,对P进行左旋
brother.color = BLACK;
parent.color = RED;
leftRotate(parent);
// 左旋完以后重新设置brother
brother = parent.right;
}
// case 1 走完后,brother会变为黑色
// case 2 : 兄弟节点为黑色,且兄弟的两个孩子都为黑色
if ((brother.left == null || brother.left.color == BLACK)
&& (brother.right == null || brother.right.color == BLACK)) {
// 如果父节点为红色,则替换父节点和兄弟节点的颜色
if (parent.color == RED) {
brother.color = RED;
parent.color = BLACK;
break;
} else {
// 父节点为黑色,此时替换兄弟节点的颜色为红色
// 这种情况下,会使父节点分支上少了一个黑色节点,所以将replace设置为父节点,继续调整
brother.color = RED;
// 就是这一步,所以会导致replace是黑色,不然传进这个方法的replace全是NIL
replace = parent;
parent = replace.parent;
}
} else {
// case 3 : 此时当兄弟节点为黑色,左子节点为红色
if (brother.left != null && brother.left.color == RED) {
brother.left.color = parent.color;
parent.color = BLACK;
rightRotate(brother);
leftRotate(parent);
} else if (brother.right != null && brother.right.color == RED) {
// case 4 : 兄弟节点为黑色,右子节点为红色
brother.color = parent.color;
parent.color = BLACK;
brother.right.color = BLACK;
leftRotate(parent);
}
break;
}
} else {
// 对称情况
brother = parent.left;
// case 1 : 红兄
if (brother.color == RED) {
brother.color = BLACK;
parent.color = RED;
rightRotate(parent);
brother = parent.left;
}
// case 2 : 黑兄,且兄弟节点的两个子节点均为黑色
if ((brother.left == null || brother.left.color == BLACK)
&& (brother.right == null || brother.right.color == BLACK)) {
// 判断父节点的颜色
if (parent.color == RED) {
// 父节点红色,替换BP颜色
brother.color = RED;
parent.color = BLACK;
break;
} else {
// 父节点为黑色,将兄弟节点置为红色,此时将父节点作为replace进行循环
brother.color = RED;
replace = parent;
parent = replace.parent;
}
} else {
// case 3 : 黑兄,兄弟右孩子为红色
if (brother.right != null && brother.right.color == RED) {
brother.right.color = parent.color;
parent.color = BLACK;
leftRotate(brother);
rightRotate(parent);
} else if (brother.left != null && brother.left.color == RED) {
// case 4 : 黑兄,兄弟左孩子为红色
brother.color = parent.color;
parent.color = BLACK;
brother.left.color = BLACK;
rightRotate(parent);
}
return;
}
}
}
// 处理单子节点的情况
if (replace != null) {
replace.color = BLACK;
}
}