说在前头:本人为大二在读学生,书写文章的目的是为了对自己掌握的知识和技术进行一定的记录,同时乐于与大家一起分享,因本人资历尚浅,发布的文章难免存在一些错漏之处,还请阅读此文章的大牛们见谅与斧正。若在阅读时有任何的问题,也可通过评论提出,本人将根据自身能力对问题进行一定的解答。
前言
在前面的章节中,我们探讨了二叉搜索树,且针对二叉搜索树的弊端(在数据有序时,最坏的情况下树结构会退化成链表)介绍了二叉平衡树中的AVL树,这一章节我们继续来探讨另一种二叉平衡树——红黑树。
红黑树是一种特化的AVL树(平衡二叉树),都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。它虽然是复杂的,但它的最坏情况运行时间也是非常良好的,并且在实践中是高效的:它可以在O(log n)时间内做查找,插入和删除,这里的n是树中元素的数目。
01—红黑规则
当插入(或者删除)一个节点时,必须遵循一定的规则,它们被称为红黑规则。如果遵循这些规则,树就是平衡的:(新插入的节点默认为红色)
-
每一个节点不是黑色就是红色
-
根总是黑色的
-
如果节点是红色的,则它的子节点必须是黑色的,反之则不一定成立
-
从根到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(称为黑色高度,即两边的黑色高度需要保持一致)
02—颜色变换
我们继续引入上图中的红黑树,并为其插入新数据8,此时,数据8应该为10的左子节点(如下图),插入后结构显得不那么平衡了,但仍然遵守四条红黑规则,所以插入后的数据并未打破该结构的平衡。
但是当我们再插入一个数据6时,就会出现下面的情况
此时的结构明显违反了第三条红黑规则(红节点的子节点都应该为黑色),为了不让6节点破坏我们第三条的红黑规则,我们可以将6节点的颜色从红色转为黑色(如下图)
此时的结构已经遵守了第三条红黑规则(红节点的子节点为黑色),但新的问题又来了,我们为了让新的节点不破环规则三,对其进行了颜色的变换,这样使得我们破坏了第四个红黑规则(所有叶子节点的查找路径黑节点数目相同)。因为查找叶子节点6时,路径下有三个黑节点;查找叶子节点80时,路径下只有两个黑节点,两边黑色高度不一致,明显已经不遵守第四条红黑规则。
通过上面的实验不难得知,一棵树如果超过了两层不平衡(一边的子树比另一边的子树高两层以上),是不可能满足红黑规则的,因为如果一条路径上的节点数比另一条路径上的节点数多一个以上,那它或者有更多的黑色节点,或者有两个相邻的红色节点,都会违背红黑规则。
03—旋转
为了平衡一棵树,需要重新手动地排列节点,如果大部分节点都在根的左侧,就需要把一些节点移到右侧,称为左旋;如果大部分节点都在根的右侧,就需要把一些节点移到左侧,成为右旋。这里所说的选左旋与右旋是相对于一棵子树的根节点而言的。
下面我们来看看一个右旋的简单例子:(旋转前)
(旋转后)
在进行左右旋时,我们除了对节点进行旋转外,我们还需要对树的结构进行考虑,旋转的前后都需要遵循基本的二叉树规则和红黑规则(下面我们来举例一个较为复杂的右旋转)。
(旋转前)
(旋转后)
04—插入
插入或删除操作,都有可能改变红黑树的平衡性,利用颜色变化与旋转这两大法宝就可应对所有情况,将不平衡的红黑树变为平衡的红黑树。
在进行颜色变化或旋转的时候,往往要涉及祖孙三代节点:
-
X:表示操作的基准节点
-
P:代表X的父节点
-
G:代表X的父节点的父节点
05—插入情况一(X为外侧节点)
当基准点x在外侧的情况时,我们需要采取下面三个步骤:
-
改变G的颜色
-
改变P的颜色
-
以G为中心进行向X上升的旋转
(旋转后)
06—插入情况二(X为内侧节点)
当X为内侧的节点时,会比在外侧复杂一点,我们需要将X从内侧节点通过颜色的变化和旋转变更为外侧节点,然后在进行旋转即可。主要可分为下面的四个步骤:
-
改变G的颜色
-
改变X的颜色
-
以P为中心向X上升的方向旋转
-
以G为中心向X上升的方向旋转
(初始状态)
(将内侧节点旋转为外侧节点)
(插入成功)
07—删除
与插入操作一样,我们在删除一个节点的时候,也有可能破坏一颗红黑树的结构,我们需要在规则被破坏时,对树进行一定的调整。
删除操作在大体上分为三类情况:
-
需要删除的节点为叶子节点
-
需要删除的节点有一个子节点
-
需要删除的节点有两个子节点
08—需删除的节点为叶子节点
当需要删除的节点为叶子节点时,即需要删除的节点没有子节点,也会出现多种情况:
①需删除叶子节点为红色:(如下图,当我们需要删除30或者50时,只需要将40对应的子节点置为null即可)
②需要删除的节点为黑色:
②-①.需要删除的黑色节点父节点为红色:
②-①-①.需要删除的节点的兄弟节点没有左子树:(如下图)
需要将30节点删除后,围绕40做左旋转,删除成功(如下图)
②-①-②.需要删除的节点的兄弟节点存在左子树:(如下图)
在删除节点40时,我们需要围绕其兄弟节点做一次右旋转(如下图)
👇👇👇
②-②.需要删除的黑色节点父节点为黑色(兄弟节点为红色):
②-②-①.需要删除的节点的左侄节点存在右节点:(如下图)
此时我们需要将左侄节点的右子节点涂黑,并且围绕左侄节点的父节点做右旋转(如下图)
最后我们只需要围绕节点55做左旋转即可得到最终的红黑树(如下图)
②-②-②.需要删除的节点的左侄节点没有右节点:(如下图)
此时当我们删除节点50时,将左子侄几点涂红,左侄节点的左子节点涂黑,并且围绕左侄节点做右旋转得到下面的图(如下)
经过上述的操作后,左侄节点拥有了一个右子节点,这样我们就将情况②-②-②改变为了②-②-①,我们只需要再进行一次②-②-①的操作即可得到标准的红黑树。
②-②-③.需要删除的节点的左侄节为叶子节点:
针对这种情况,我们只需要对左侄节点涂红,及其父节点涂黑,最后围绕其爷节点左旋转即可得到标准的红黑树(如下图)
②-③.需要删除的黑色节点父节点为黑色(兄弟节点为黑色):
②-③-①.兄弟节点存在右节点(如下图)
此时候将右侄节点涂黑,并围绕待删除节点的父节点进行左旋即可(如下图)
②-③-②.兄弟节点没有右节点(如下图)
这时需要将左侄节点涂黑,并围绕待删除的节点的兄弟节点左右旋转(如下图)
此时只需要做个简单的左旋转即可(如下图)
②-③-③.兄弟节点没有子节点(如下图)
如果我们删除了节点60,整颗树无法遵守第四条红黑规则,因为节点60的删除,导致70左边的黑色高度只有1,但右边为2,两边黑色高度不一致。此时,我们可以使节点75更改颜色为红色。(如下图)
08—需删除的节点存在一个子节点
如上图,如果需要删除节点60,左边的黑色高度减一,此时我们可以将节点55涂黑,代替原先的节点60时,树就可以重新遵守红黑规则。(如下图)
09—需删除的节点存在两个子节点
当需要删除的节点存在两个子节点时,我们又该怎么处理呢?前面我们强调过红黑树继承了二叉搜索树的基本属性,这时我们可以回想一下我们处理二叉搜索树的具体删除操作——寻找待删除节点的后继节点代替删除的节点。需要注意的是,因为红黑树相对二叉搜索树增加了红黑规则,所以寻找到后继节点后,并非直接与待删除节点直接替换,他们仅仅只需要替换数值即可。这样删除节点的操作变成了删除其后继节点。(如上图,待删除节点是60,其后继节点是63,待删除节点与后继节点交换数值,从而转变为删除后继节点,如下图)
👇👇👇
10—具体代码
节点类:
package com.bosen.www;
/**
* <p>节点类</p>
* @author Bosen 2021/6/17 23:37
*/
public class Node {
private boolean isBlack;// 红黑标志(true为黑色,false为红色)
private int data; // 数据
private Node parent; // 父节点
private Node left; // 左子节点
private Node right; // 右子节点
public Node(boolean isBlack, int data) {
this.isBlack = isBlack;
this.data = data;
}
@Override
public String toString() {
return "[" + (isBlack?"黑":"红") + ":" + data + "]";
}
public boolean isBlack() {
return isBlack;
}
public void setBlack(boolean black) {
isBlack = black;
}
public Node getParent() {
return parent;
}
public void setParent(Node parent) {
this.parent = parent;
}
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
public Node getLeft() {
return left;
}
public void setLeft(Node left) {
this.left = left;
}
public Node getRight() {
return right;
}
public void setRight(Node right) {
this.right = right;
}
}
红黑树类:
package com.bosen.www;
/**
* <p>红黑树</p>
* @author Bosen 2021/6/17 23:36
*/
public class RedBlackTree {
/*
* 根节点
*/
private Node root;
public Node getRoot() {
return root;
}
/*
* 插入
*/
public void insert(int data) {
if (root == null) {
// 红黑树为空
root = new Node(true, data);
return;
}
Node node = new Node(false, data);
Node nodeParent = null;
Node temp = root;
// 看成二叉搜索树将数据节点插入,后面再修正
while (temp != null) {
nodeParent = temp;
if (data < temp.getData()) {
temp = temp.getLeft();
} else {
temp = temp.getRight();
}
}
node.setParent(nodeParent);
if (data < nodeParent.getData()) {
nodeParent.setLeft(node);
} else {
nodeParent.setRight(node);
}
// 修正节点
insertFixUp(node);
root.setBlack(true);
}
/*
* 删除
*/
public void delete(int data) {
// 定位待删除的节点
Node node = select(data);
Node parent, child;
boolean isBlack;
// 拥有两个节点
if(node.getLeft() != null && node.getRight() != null) {
Node replace = node.getRight();
// 定位后继节点
while (replace.getLeft() != null) {
replace = replace.getLeft();
}
if (node.getParent() == null) {
root = replace;
} else {
if (node.getParent().getLeft() == node) {
node.getParent().setLeft(replace);
} else {
node.getParent().setRight(replace);
}
}
// 后继节点的右子节点
child = replace.getRight();
parent = replace.getParent();
// 保存后继节点的颜色
isBlack = replace.isBlack();
if (parent == node) {
// 被删除节点为后继节点的父节点
parent = replace;
} else {
if (child != null) {
// child不为空
child.setParent(parent);
parent.setLeft(child);
}
replace.setRight(node.getRight());
node.getRight().setParent(replace);
}
replace.setParent(node.getParent());
replace.setBlack(node.isBlack());
replace.setLeft(node.getLeft());
node.getLeft().setParent(replace);
if (isBlack) {
// 删除修正
deleteFixUp(child, parent);
}
node = null;
return;
}
// 拥有一个节点
if (node.getLeft() !=null) {
// 拥有左子节点
child = node.getLeft();
} else {
// 拥有右子节点
child = node.getRight();
}
parent = node.getParent();
// 保存"取代节点"的颜色
isBlack = node.isBlack();
if (child!=null) {
child.setParent(parent);
}
// "node节点"不是根节点
if (parent!=null) {
if (parent.getLeft() == node)
parent.setLeft(child);
else
parent.setRight(child);
} else {
root = child;
}
if (isBlack) {
deleteFixUp(child, parent);
}
node = null;
}
/*
* 搜索节点
*/
public Node select(int data) {
Node temp = root;
Node node = null;
while (temp != null) {
node = temp;
if (data == node.getData()) {
return node;
}
if (data < temp.getData()) {
temp = temp.getLeft();
} else {
temp = temp.getRight();
}
}
return node;
}
/*
* 删除-修正红黑树
*/
public void deleteFixUp(Node node, Node parent) {
Node other;
while ((node==null || node.isBlack()) && (node != root)) {
if (parent.getLeft() == node) {
other = parent.getRight();
if (!other.isBlack()) {
// Case 1: x的兄弟w是红色的
other.setBlack(true);
parent.setBlack(false);
leftRotate(parent);
other = parent.getRight();
}
if ((other.getLeft()==null || other.getLeft().isBlack()) &&
(other.getRight()==null || other.getRight().isBlack())) {
// Case 2: x的兄弟w是黑色,且w的俩个孩子也都是黑色的
other.setBlack(false);
node = parent;
parent = node.getParent();
} else {
if (other.getRight()==null || other.getRight().isBlack()) {
// Case 3: x的兄弟w是黑色的,并且w的左孩子是红色,右孩子为黑色。
other.getLeft().setBlack(true);
other.setBlack(false);
rightRotate(other);
other = parent.getRight();
}
// Case 4: x的兄弟w是黑色的;并且w的右孩子是红色的,左孩子任意颜色。
other.setBlack(parent.isBlack());
parent.setBlack(true);
other.getRight().setBlack(true);
leftRotate(parent);
node = root;
break;
}
} else {
other = parent.getLeft();
if (!other.isBlack()) {
// Case 1: x的兄弟w是红色的
other.setBlack(true);
parent.setBlack(false);
rightRotate(parent);
other = parent.getLeft();
}
if ((other.getLeft()==null || other.getLeft().isBlack()) &&
(other.getRight()==null || other.getRight().isBlack())) {
// Case 2: x的兄弟w是黑色,且w的俩个孩子也都是黑色的
other.setBlack(false);
node = parent;
parent = node.getParent();
} else {
if (other.getLeft()==null || other.getLeft().isBlack()) {
// Case 3: x的兄弟w是黑色的,并且w的左孩子是红色,右孩子为黑色。
other.getRight().setBlack(true);
other.setBlack(false);
leftRotate(other);
other = parent.getLeft();
}
// Case 4: x的兄弟w是黑色的;并且w的右孩子是红色的,左孩子任意颜色。
other.setBlack(parent.isBlack());
parent.setBlack(true);
other.getLeft().setBlack(true);
rightRotate(parent);
node = root;
break;
}
}
}
if (node!=null) {
node.setBlack(true);
}
}
/*
* 插入-修正红黑树
*/
public void insertFixUp(Node node) {
Node parent, uncle, grandpa;
// 父节点存在且为红色
while ((parent = node.getParent()) != null && !parent.isBlack()) {
grandpa = parent.getParent();
grandpa.setBlack(false);
if (parent == grandpa.getLeft()) {
uncle = grandpa.getRight();
if (uncle!=null && !uncle.isBlack()) {
// 叔叔节点为红色
parent.setBlack(true);
uncle.setBlack(true);
} else {
// 叔叔节点为黑色
if (parent.getLeft() == node) {
// node为父节点左节点
parent.setBlack(true);
rightRotate(grandpa);
} else {
// node为父节点右节点
node.setBlack(true);
leftRotate(parent);
rightRotate(grandpa);
}
}
} else {
// 父节点为爷爷节点的右节点
uncle = grandpa.getLeft();
if (!uncle.isBlack()) {
// 叔叔节点为红色
parent.setBlack(true);
uncle.setBlack(true);
} else {
// 叔叔节点为黑色
if (parent.getLeft() == node) {
// node为父节点左节点
node.setBlack(true);
rightRotate(parent);
leftRotate(grandpa);
} else {
// node为父节点右节点
parent.setBlack(true);
rightRotate(grandpa);
}
}
}
node = grandpa;
}
}
/*
* 左旋
*/
public void leftRotate(Node p) {
Node x = p.getRight();
// 将x的左孩子设为p的右孩子
p.setRight(x.getLeft());
if (p.getLeft() != null) {
x.getLeft().setParent(p);
}
// 将p的父节点设置为x的父节点
x.setParent(p.getParent());
if (x.getParent() == null) {
root = x; // 设置x为根节点
} else {
if (p.getParent().getLeft() == p) {
// x设为左节点
x.getParent().setLeft(x);
} else {
// x设为右节点
x.getParent().setRight(x);
}
}
// x的左孩子为p
x.setLeft(p);
// p的父节点设置为x
p.setParent(x);
}
/*
* 右旋
*/
public void rightRotate(Node p) {
Node x = p.getLeft();
// 将x的右孩子设为p的左孩子
p.setLeft(x.getRight());
if (p.getRight() != null) {
x.getRight().setParent(p);
}
// 将p的父节点设置为x的父节点
x.setParent(p.getParent());
if (x.getParent() == null) {
root = x; // 设置x为根节点
} else {
if (p.getParent().getRight() == p) {
// x设为右节点
x.getParent().setRight(x);
} else {
// x设为左节点
x.getParent().setLeft(x);
}
}
// x的右孩子为p
x.setRight(p);
// p的父节点设置为x
p.setParent(x);
}
/*
* 中序遍历
*/
public void display(Node node) {
if (node != null) {
display(node.getLeft());
System.out.print(node + "\t");
display(node.getRight());
}
}
}
👇扫描二维码关注