目录
红黑树的删除
红黑树的删除相对于插入,会复杂很多。
我们分情况讨论
1. 删除节点为叶子节点
分两种情况,删除节点是红色节点和删除节点为黑色节点
1.1 删除节点为红色节点
如果是红色节点,我们可以直接删除,因为删除红色叶子节点并不会影响整体红黑树的结构
1.2 删除节点为黑色
假设左边黑色节点为要删除的叶子结点D,则一共有5种情况。如下图所示。因为左边支路上有一个黑色节点。所以右边支路上也要有一个黑色节点。
1.2.1 要删除的节点D是黑色,D的兄弟节点B也是黑色没有侄子节点
其实这里我们可以回想一下2-3-4树是怎么删除的,这里的情况就相当于2-3-4树删除的兄弟节点和自己都是2节点的情况,这个时候我们就要和父节点接过来的一个元素构成一个4节点,然后再执行删除操作。
在红黑树中我们不需要考虑父节点是一个2节点还是3、4节点,如果是2节点造成的空位,我们只需要交给递归的操作去平衡就可以了。
1.2.2 要删除的节点D是黑色,D的兄弟节点有一个红色的右子节点
这种情况可以对应到2-3-4的,兄弟节点是非2节点,这样我们可以从兄弟节点借一个元素过来,然后我们再删除。
1.2.3 要删除的节点D是黑色,D的兄弟节点有一个红色的左子节点
这种情况我们只需要对B进行一个右旋,就可以将问题转化为1.2.2的问题然后再进行操作即可
1.2.4 要删除的节点D是黑色,D的兄弟节点B有两个红色节点
处理方式和1.2.2相同
1.2.5 要删除的节点D是黑色,D的兄弟节点是红色且有两个黑色子节点
这种情况,我们根据红黑色定义可以推断出P肯定为黑色,我们只需要对P进行一个左旋
这个时候我们发现删除D的问题就转化为了1.2.1的情况,之后我们根据1.2.1的方式去进行处理即可
上面只是写了删除节点为左节点的情况,再进行删除右节点的时候思路也是一样的,只不过旋转的时候是反过来的,这里就没必要赘述了。如果还是不清楚,可以参考下方的代码,并且配合着2-3-4的删除思路来食用,效果更好
2. 删除节点为非叶子节点
2.1 删除节点只有一个子树
根据红黑树定义,可以知道这个子树只有一个元素,因为只要元素有两个以上,那么肯定就会被平衡。
所以这种情况就比较简单,我们只需要用这个唯一的后继节点的键值覆盖掉父节点,然后再将旧的后继节点删除即可
如下图,我们要删除节点2
2.2 删除节点有两个子树
寻找后继节点
这种情况我们就没办判断子树的高度是多少,那么我们怎么找后继节点呢?
一般就是寻找一个key最接近删除节点的节点作为后继节点进行替换,我们可以选择右子树中最接近的也可以是左子树中最接近的节点
而后继节点又存在两种情况
后继节点无子节点
无子节点的话,我们只需要按照2.1中的操作,用后继节点替换被删除节点,然后删除直接删除掉后继节点即可
后继节点有一个子节点
用于替换的节点也会出现有一个子节点的情况,譬如下图:
这里假如,我们要删除11,这个时候我去寻找右子树中最小的元素来替换它,这个时候会找到12,但是12是有一个右子点的。
那么这个后继元素的右子树会不会有多个元素呢?并不会,只要再添加一个元素无论是大于12还是小于12,大于12会破坏平衡,进行旋转变色以后,会重新定位后继节点,这个后继节点显然是不会有子节点的,如果是小于12,那么这个小于12的元素自然就会被选为后继节点而不是12.
那么这种情况,其实也很简单,先按照正常来,将被删除节点替换为后继节点,然后再删除后继节点,但是这个后继节点不能直接删除,这个问题会被转化为2.1中的删除问题,用2.1的方式删除掉后继节点即可。
代码
下面是具体的红黑树代码,加入了大量注释方便了理解
package datastructure;
import java.util.Scanner;
/**
* @Description 红黑树
* @Author weiyifei
* @date 2022/6/10
*/
public class RedBlackBST<Key extends Comparable<Key>, Value> {
private static final boolean Red = true;
private static final boolean Black = false;
private Node root;
private class Node {
Key key;
Value value;
Node left, right, parent;
int N;
boolean color;
public Node(Key key, Value value, Node parent, int n, boolean color) {
this.key = key;
this.value = value;
N = n;
this.color = color;
this.parent = parent;
}
}
public RedBlackBST() {
}
private boolean isRed(Node x) {
if (x == null) return false;
return x.color == Red;
}
//左旋
Node rotateLeft(Node h) {
Node x = h.right;
h.right = x.left;
if(h.right!=null){
h.right.parent = h;
}
x.parent = h.parent;
h.parent = x;
x.left = h;
if(x.left!=null){
x.left.parent = x;
}
if(x.parent!=null){
int cmp = x.parent.key.compareTo(x.key);
if(cmp>0) x.parent.left = x;
else x.parent.right = x;
}
x.N = h.N;
h.N = 1 + size(h.left) + size(h.right);
show();
return x;
}
//右旋
Node rotateRight(Node h) {
Node x = h.left;
h.left = x.right;
if(h.left!=null){
h.left.parent = h;
}
x.parent = h.parent;
h.parent = x;
x.right = h;
if(x.right!=null){
x.right.parent = x;
}
if(x.parent!=null){
int cmp = x.parent.key.compareTo(x.key);
if(cmp>0) x.parent.left = x;
else x.parent.right = x;
}
x.N = h.N;
h.N = 1 + size(h.left) + size(h.right);
show();
return x;
}
private void put(Key key, Value val) {
root = put(root, null, key, val);
root.color = Black;
}
private Node put(Node h, Node p, Key key, Value val) {
if (h == null) {
return new Node(key, val, p, 1, Red);
}
//当插入的时候,发现路径上有-4节点,
if (isRed(h.left) && isRed(h.right)) flipColors(h);
//递归搜索树,直到找到相同的key,修改,或者搜索到底层,进行插入。
int cmp = key.compareTo(h.key);
if (cmp < 0) h.left = put(h.left, h, key, val);
else if (cmp > 0) h.right = put(h.right, h, key, val);
else h.value = val;
//判断红黑,进行旋转,调整树的平衡
if (isRed(h.right) && isRed(h.right.right)) {
h.right.color = h.color;
h.color = Red;
h = rotateLeft(h);
}
if (isRed(h.right) && isRed(h.right.left)) {
//RL问题,先右旋,将问题转换为RR
h.right = rotateRight(h.right);
//变色
h.right.color = h.color;
h.color = Red;
//再左旋
h = rotateLeft(h);
}
if (isRed(h.left) && isRed(h.left.left)) {
h.left.color = h.color;
h.color = Red;
h = rotateRight(h);
}
if (isRed(h.left) && isRed(h.left.right)) {
//LR问题,先左旋,将问题转换为LL问题
h.left = rotateLeft(h.left);
//变色
h.left.color = h.color;
h.color = Red;
//再右旋
h = rotateRight(h);
}
h.N = size(h.left) + size(h.right) + 1;
return h;
}
private boolean del(Key key) {
if (null != key) {
if (null != root) {
return del(key, root, null);
}
}
return false;
}
private boolean del(Key key, Node cur, Node parent) {
if (null != cur) {
int cmp = key.compareTo(cur.key);
//如果key>当前节点,向右检索
if (cmp > 0) {
return del(key, cur.right, cur);
}
//如果key<当前节点,想左检索
if (cmp < 0) {
return del(key, cur.left, cur);
}
//当前节点就是要删除的节点
if (cmp == 0) {
//删除的节点有两个子节点
if (null != cur.left && null != cur.right) {
delTowChildren(cur);
return true;
}else {
//如果没有子节点,直接修复平衡,然后删除
if(null==cur.left&&null==cur.right){
delLeafFix(cur);
if(cur.key.compareTo(parent.key)>0){
parent.right = null;
}else {
parent.left = null;
}
return true;
}else { //如果有一个子节点
dleOneChildNode(cur);
return true;
}
}
}
}
return false;
}
//当被删除节点有两个子节点的情况下的处理
private void delTowChildren(Node cur) {
//获取后继节点用于替换被删除节点
Node replacement = successor(cur);
//替换节点的左右子节点都是null
if (null == replacement.right && null == replacement.left) {
delLeafNode(cur, replacement);
}else {//用于替换的节点,会存在拥有一个子节点的情况
cur.key = replacement.key;
cur.value = replacement.value;
dleOneChildNode(replacement);
}
}
private void dleOneChildNode(Node delNode) {
//使用后继节点进行代替
Node replacement = (null==delNode.left)?delNode.right:delNode.left;
delLeafNode(delNode,replacement);
}
private void delLeafNode(Node cur, Node replacement) {
cur.key = replacement.key;
cur.value = replacement.value;
//进行修复
delLeafFix(replacement);
//然后删除
if (replacement == replacement.parent.right) {
replacement.parent.right = null;
} else {
replacement.parent.left = null;
}
}
/**
* @return void
* @Description 删除叶子节点后的修复
* @Author weiyifei
*/
private void delLeafFix(Node delNode) {
//只处理删除节点是黑色的情况,如果是红色则直接删除即可
while (delNode != root && Black == delNode.color) {
Node par = delNode.parent;
Node bro = getBrother(delNode);
if (delNode.key.compareTo(par.key) > 0) { //被删除节点是右叶子节点
//如果叶子节点是黑色且不是root,那么他的兄弟节点必然存在,这是由红黑树5定律决定的
if (Red == bro.color) { //如果兄弟节点是红色,可以推断出其必有两个黑色子节点
bro.color = Black;
par.color = Red;
rotateRight(par);
} else {
if (null == bro.left && null == bro.right) {//如果兄弟节点也是叶子节点,那么直接让兄弟和父节点构成一个-3节点即可
bro.color = Red;
delNode = par;
} else {
if (null != bro.left && Red == bro.left.color) {
//两种情况,一种是兄弟节点红色, 且有两个节点
//另一种是兄弟节点黑色,只有左节点
//这两种的处理方式相同
bro.color = par.color;
par.color = Black;
bro.left.color = Black;
rotateRight(par);
break;
} else { //兄弟节点黑色, 但是只有右子节点
//这里对bro进行左旋加变色,将情况转换为上面的情况,再进行处理
bro.right.color = Black;
bro.color = Red;
rotateLeft(bro);
}
}
}
} else { //删除的是左节点,和上面方式相同
if (Red == bro.color) {
bro.color = Black;
par.color = Red;
rotateLeft(par);
} else {
if (null == bro.left && null == bro.right) {
bro.color = Red;
delNode = par;
} else {
if (null != bro.right && Red == bro.right.color) {
bro.color = par.color;
par.color = Black;
bro.right.color = Black;
rotateLeft(par);
break;
} else {
bro.left.color = Black;
bro.color = Red;
rotateRight(bro);
}
}
}
}
}
delNode.color = Black;
}
private void inOrderTraveral(){
inOrderTraveral(root);
}
//中序遍历
private void inOrderTraveral(Node node){
if(node==null){
return;
}
inOrderTraveral(node.left);
System.out.println(node.key+"");
inOrderTraveral(node.right);
}
public static void main(String[] args) {
RedBlackBST<Integer, Integer> bst = new RedBlackBST<>();
//使用create方法构造红黑树
// bst.creat(0,1,2,3,4,5,6,7,8,13,12,14,11);
// bst.show();
Scanner scanner = new Scanner(System.in);
while (true){
System.out.println("请选择操作");
System.out.println("1.插入\n2.删除\n3.中序遍历\n4.输出树形结构");
int i = scanner.nextInt();
switch (i){
case 1:
System.out.println("请输入要插入的key");
int num = scanner.nextInt();
bst.put(num,0);
System.out.println("插入完成");
System.out.println("当前树如下");
bst.show();
break;
case 2:
System.out.println("请输入要删除的key");
int key = scanner.nextInt();
bst.del(key);
System.out.println("删除之后的树");
bst.show();
break;
case 3:
System.out.println("中序遍历结果");
bst.inOrderTraveral();
break;
case 4:
System.out.println("树结构如下");
bst.show();
break;
default:
System.out.println("输入错误请重新选择");
}
}
}
private void creat(Value value,Key ... nums) {
for (Key num : nums) {
put(num,value);
show();
}
}
/**
* @return datastructure.RedBlackBST<Key, Value>.Node
* @Description 获取的兄弟节点
* @Author weiyifei
*/
private Node getBrother(Node node) {
if (null == node) {
return node;
}
Node par = node.parent;
if (null == par) {
return null;
}
int cmp = node.key.compareTo(par.key);
if (cmp > 0) return par.left;
else return par.right;
}
//寻找被删除节点的后继节点,即,查找红黑树中数据值大于该节点的最小节点
private Node successor(Node node) {
if (node == null) {
return null;
}
Node p = node.right;
while (null != p.left) {
p = p.left;
}
return p;
}
//颜色转换,专门用哦过来处理一个节点的两个红色节点的颜色转换问题
void flipColors(Node x) {
x.color = Red;
x.left.color = Black;
x.right.color = Black;
}
public int size() {
return size(root);
}
public int size(Node x) {
if (x == null) return 0;
else return x.N;
}
// 用于获得树的层数
public int getTreeDepth(Node root) {
return root == null ? 0 : (1 + Math.max(getTreeDepth(root.left), getTreeDepth(root.right)));
}
private void writeArray(Node currNode, int rowIndex, int columnIndex, String[][] res, int treeDepth) {
// 保证输入的树不为空
if (currNode == null) return;
// 先将当前节点保存到二维数组中
res[rowIndex][columnIndex] = currNode.key+""+(currNode.color==Red?"r":"b");
// 计算当前位于树的第几层
int currLevel = ((rowIndex + 1) / 2);
// 若到了最后一层,则返回
if (currLevel == treeDepth) return;
// 计算当前行到下一行,每个元素之间的间隔(下一行的列索引与当前元素的列索引之间的间隔)
int gap = treeDepth - currLevel - 1;
// 对左儿子进行判断,若有左儿子,则记录相应的"/"与左儿子的值
if (currNode.left != null) {
res[rowIndex + 1][columnIndex - gap] = "/";
writeArray(currNode.left, rowIndex + 2, columnIndex - gap * 2, res, treeDepth);
}
// 对右儿子进行判断,若有右儿子,则记录相应的"\"与右儿子的值
if (currNode.right != null) {
res[rowIndex + 1][columnIndex + gap] = "\\";
writeArray(currNode.right, rowIndex + 2, columnIndex + gap * 2, res, treeDepth);
}
}
public void show(){
show(root);
}
public void show(Node root) {
if (root == null) System.out.println("EMPTY!");
// 得到树的深度
int treeDepth = getTreeDepth(root);
// 最后一行的宽度为2的(n - 1)次方乘3,再加1
// 作为整个二维数组的宽度
int arrayHeight = treeDepth * 2 - 1;
int arrayWidth = (2 << (treeDepth - 2)) * 3 + 1;
// 用一个字符串数组来存储每个位置应显示的元素
String[][] res = new String[arrayHeight][arrayWidth];
// 对数组进行初始化,默认为一个空格
for (int i = 0; i < arrayHeight; i ++) {
for (int j = 0; j < arrayWidth; j ++) {
res[i][j] = " ";
}
}
// 从根节点开始,递归处理整个树
// res[0][(arrayWidth + 1)/ 2] = (char)(root.val + '0');
writeArray(root, 0, arrayWidth/ 2, res, treeDepth);
// 此时,已经将所有需要显示的元素储存到了二维数组中,将其拼接并打印即可
for (String[] line: res) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < line.length; i ++) {
sb.append(line[i]);
if (line[i].length() > 1 && i <= line.length - 1) {
i += line[i].length() > 4 ? 2: line[i].length() - 1;
}
}
System.out.println(sb.toString());
}
}
}
后记
红黑树的删除初看很复杂,如果我们只是闷头去背情况,肯定是摸不着头脑,但实际上我们发现,其内核本质上还是2-3-4的操作,,很多比较麻烦的,我们都是进行一一拆解,转化为最简单的情况,最后在进行处理。
一旦你理解了其中的逻辑,就会发现红黑树的删除也就那么回事儿。
参考链接
红黑树·删除操作,详细图解[CSDN]
数据结构:红黑树的删除[知乎]
图解:什么是红黑树?[知乎]