文章目录
- 前言
- 什么是红黑树
- 红黑树的性质
- 红黑树结点的定义
- 红黑树的插入
- 情况一
- 情况二
- 情况三
- 插入代码总结
- 验证是否为红黑树
- 红黑树的删除
前言
前面我们学习了 AVL 树——高度平衡的二叉搜索树,AVL 树保证了结点的左右子树的高度差的绝对值不超过 1,也就是结点的左右子树的高度是绝对平衡的,虽然这种结构的查询速度非常的快,但是因为它要保证左右子树的绝对平衡,所以对 AVL 树进行增加或者删除操作的时候,就需要进行多次旋转,而对树进行旋转也是需要时间的,所以 AVL 树只适合存储一些静态的不经常变化的数据。那么要想保证查询速度,也要对数据进行增加和删除操作的话,就需要使用另一个数据结构——红黑树。
什么是红黑树
红黑树是一种二叉搜索树,但在每个节点上增加了一个存储位表示节点的颜色,可以是 Red 或者 Black。通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是接近平衡的。
红黑树的性质
为了保证红黑树的查找速率以及增加和删除的速度,红黑树具有以下性质:
- 从根节点到叶子节点的路径中,最长路径最多是最短路径的二倍
- 每个节点不是红色就是黑色
- 根节点是黑色的
- 如果一个节点是红色的,则如果它的两个孩子节点是黑色的(一条路径上不存在两个连续的红色节点)
- 对于每个节点,从该结点到其所有后代节点的简单路径上,均包含相同数目的黑色节点(黑色节点的数量包括 NULL 节点)
- 每个叶子节点都是黑色的(此处的叶子结点指的是空结点)
这里对一些性质进行演示:
为什么从根节点到叶子节点的路径中,最长路径最多是最短路径的二倍?
这个性质是通过性质4、5得来的:
红黑树结点的定义
首先通过一个枚举类来表示颜色:
public enum COLOR {
RED, BLACK;
}
红黑树节点的定义:
class RBTreeNode {
public RBTreeNode left;
public RBTreeNode right;
public RBTreeNode parent;
public rbtree.COLOR color = rbtree.COLOR.RED; //结点的颜色
public int val;
public RBTreeNode(int val) {
this.val = val;
this.color = COLOR.RED;
}
}
在这里我们将节点的颜色默认设置为了红色,这是为什么呢?
因为红黑树的性质:从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点。将节点默认设置为红色,插入时不会违反红黑树的性质,因为红色的节点不会影响路径上黑色节点的数量。而如果默认颜色设置为黑色,每次插入新节点都可能违反红黑树的性质,需要频繁调整树的结构,导致效率降低。因此,将红黑树的结点默认颜色设置为红色是为了保持树的平衡,提高插入操作的效率。
红黑树的插入
因为红黑树也是属于特殊的二叉搜索树,所以在插入数据的时候,还是按照二叉搜索树插入的做法一样,当数据插入之后,我们需要做的就是检测新节点插入之后,红黑树的性质是否被破坏。
这里的破坏性质通常是指:因为新插入的节点的颜色默认是红色,如果新插入的节点的双亲节点的颜色是黑色的话,就没有破坏红黑树的性质,不需要做出修改;而如果插入的节点的双亲结点也是红色的话,就不符合红黑树的性质——红色结点的左右孩子节点的颜色都是黑色(在一条路径中不存在两个连续的红色节点),此时就需要对红黑树的结构进行修改。
这里我们将插入节点后需要调整红黑树结构的情况给列举出来:
这里我们约定:cur 节点为当前节点,p为父节点,g为祖父节点,u为叔叔节点
情况一
当 cur 为红色,p 为红色,g 为黑色,u 存在且为红色:
这里 cur 是新插入的节点,插入之后 p 为红色,cur 为红色,一条路径上存在两个连续的红色节点,此时就需要对红黑树的结构进行调整。这种情况还是比较容易解决的:这种情况,我们需要将 p 和 u 都改为黑色,并且为了保证路径上黑色节点的数量不变,还需要将 g 节点的颜色改为 RED,这样就没有破坏红黑树的性质。
修改 p、u 和 g 的颜色之后,还没有结束,因为红黑树的性质中还有一条性质就是:根节点的颜色必须为黑色,所以我们在进行上面的操作了之后,不管根节点为啥颜色,都需要进行 root.color = COLOR.BLACK
的操作。
通过代码体现就是这样:
while (parent != null && parent.color == COLOR.RED) {
RBTreeNode grandfather = parent.parent; //grandfather不可能为null,因为如果parent为红色,那么就一定存在父亲节点,因为红黑树的根节点是黑色
RBTreeNode uncle = null;
if (grandfather.left == parent) {
uncle = grandfather.right;
}else {
uncle = grandfather.left;
}
if (uncle != null && uncle.color == COLOR.RED) {
//情况一
parent.color = COLOR.BLACK;
uncle.color = COLOR.BLACK;
grandfather.color = COLOR.RED;
//当grandfather的节点颜色变为了红色之后,可能又会破坏其他树的结构,所以需要继续向上调整
cur = grandfather;
parent = cur.parent;
}
}
root.color = COLOR.BLACK; //将根节点的颜色修改为黑色
情况二
当 cur 为红色,p 为红色,g 为黑色,u 不存在或者 u 存在且为黑色:
这种情况往往不是刚插入时候造成的,而是因为在调整的过程中出现的:
所以这种情况只可能在向上调整的过程中才会出现:
对于这种情况的解决方式就是对 g 的左右子树进行右旋操作之后,将 p 的颜色改为黑色,g 的颜色改为红色:
这是 u 不存在的情况:
同样的,这里是右旋,将上面的情况进行镜像处理,就需要进行左旋操作了:
所以当 cur 为红色,p 为红色,g 为黑色。u不存在或者 u 存在且颜色为黑色的做法就可以总结为:
- 当 p 为 g 的左孩子,cur 为 p 的左孩子的时候:
- (1)将 g 节点的左右子树进行右旋操作
- (2)将 g 节点的颜色修改为红色,p 节点的颜色修改为黑色
- 当 p 为 g 的右孩子,cur 为 p 的右孩子的时候:
- (1)将 g 节点的左右子树进行左旋操作
- (2) 将 g 节点的颜色修改为红色,p 节点的颜色修改为黑色
通过代码体现就是这样:
while (parent != null && parent.color == COLOR.RED) {
RBTreeNode grandfather = parent.parent; //grandfather不可能为null,因为如果parent为红色,那么就一定存在父亲节点,因为红黑树的根节点是黑色
RBTreeNode uncle = null;
if (grandfather.left == parent) {
uncle = grandfather.right;
if (uncle != null && uncle.color == COLOR.RED) {
//情况一
parent.color = COLOR.BLACK;
uncle.color = COLOR.BLACK;
grandfather.color = COLOR.RED;
//当grandfather的节点颜色变为了红色之后,可能又会破坏其他树的结构,所以需要继续向上调整
cur = grandfather;
parent = cur.parent;
}else {
//else 就表示uncle为空或者uncle不为空且uncle颜色为黑色的情况
if (cur == parent.left) {
rotateRight(grandfather);
grandfather.color = COLOR.RED;
parent.color = COLOR.BLACK;
}
}
}else {
uncle = grandfather.left;
if (uncle != null && uncle.color == COLOR.RED) {
//情况一
parent.color = COLOR.BLACK;
uncle.color = COLOR.BLACK;
grandfather.color = COLOR.RED;
//当grandfather的节点颜色变为了红色之后,可能又会破坏其他树的结构,所以需要继续向上调整
cur = grandfather;
parent = cur.parent;
}else {
//else 就表示uncle为空或者uncle不为空且uncle颜色为黑色的情况
if (cur == parent.right) {
rotateLeft(grandfather);
grandfather.color = COLOR.RED;
parent.color = COLOR.BLACK;
}
}
}
}
root.color = COLOR.BLACK;
return true;
右旋操作:
public void rotateRight(RBTreeNode parent) {
RBTreeNode subL = parent.left;
RBTreeNode subLR = subL.right;
parent.left = subLR;
if (subLR != null) {
subLR.parent = parent;
}
RBTreeNode pParent = parent.parent;
if (root == parent) {
root = subL;
subL.parent = null;
}else {
if (pParent.left == parent) {
pParent.left = subLR;
}else {
pParent.right = subLR;
}
subLR.parent = pParent;
}
subL.right = parent;
parent.parent = subL;
}
左旋操作:
public void rotateLeft(RBTreeNode parent) {
RBTreeNode subR = parent.right;
RBTreeNode subRL = subR.left;
RBTreeNode pParent = parent.parent;
parent.right = subRL;
if (subRL != null) {
subRL.parent = parent;
}
if (root == parent) {
root = subR;
root.parent = pParent;
}else {
if (pParent.left == parent) {
pParent.left = subR;
else {
pParent.right = subR;
}
subR.parent = pParent;
}
subR.left = parent;
parent.parent = subR;
}
情况三
第三种情况还是 cur 为红色,p 为红色,g 为黑色,u 不存在或者 u 存在且为黑色,但是呢?这种情况不是和第二种情况一样,p 位于 g 的左侧,cur 位于 p 的左侧、p 位于 g 的右侧,cur 位于 p 的右侧这种相同方向的情况,而是 p 位于 g 的左侧,cur 位于 p 的右侧、p 位于 g 的右侧,cur 位于 g 的左侧:
同样这种情况也是在调整的过程中才会出现的:
当出现这种情况的时候,通过右旋 g 节点的左右子树,然后修改 p 和 g 的颜色是无法解决的:
当出现这种情况的时候,需要先对 p 节点的左右子树进行左旋操作:
对 p 的左右子树进行左旋操作之后,就变成了第二类情况,接下来对 g 的左右子树进行右旋操作之后,将 g 节点的颜色修改为红色、p 节点的颜色修改为黑色就可以达到目的了。
这是当 p 为 g 的左子树,cur 为 p 的右子树的情况,对于 p 为 g 的右子树,cur 为 p 的左子树的解决方法是类似的:
通过代码展示就是这样的:
当 p 为 g 的左子树,cur 为 p 的右子树:
rotateLeft(parent);
RBTreeNode tmp = parent;
parent = cur;
cur = tmp;
rotateRight(grandfather);
grandfather.color = COLOR.RED;
parent.color = COLOR.BLACK;
当 p 为 g 的右子树,cur 为 p 的左子树:
rotateRight(parent);
RBTreeNode tmp = parent;
parent = cur;
cur = tmp;
rotateLeft(grandfather);
grandfather.color = COLOR.RED;
parent.color = COLOR.BLACK;
插入代码总结
public class RBTree {
class RBTreeNode {
public RBTreeNode left;
public RBTreeNode right;
public RBTreeNode parent;
public rbtree.COLOR color; //结点的颜色
public int val;
public RBTreeNode(int val) {
this.val = val;
this.color = COLOR.RED;
}
}
private RBTreeNode root;
public boolean insert(int data) {
RBTreeNode node = new RBTreeNode(data);
if (root == null) {
root = node;
node.color = COLOR.BLACK;
return true;
}
RBTreeNode cur = root, parent = null; //parent节点记录cur节点的双亲节点
while (cur != null) {
if (cur.val < data) {
parent = cur;
cur = cur.right;
}else if (cur.val > data) {
parent = cur;
cur = cur.left;
}else {
return false; //二叉搜索树中不存在重复的元素
}
}
if (data > parent.val) {
parent.right = node;
}else {
parent.left = node;
}
node.parent = parent;
cur = node;
while (parent != null && parent.color == COLOR.RED) {
RBTreeNode grandfather = parent.parent; //grandfather不可能为null,因为如果parent为红色,那么就一定存在父亲节点,因为红黑树的根节点是黑色
RBTreeNode uncle = null;
if (grandfather.left == parent) {
uncle = grandfather.right;
if (uncle != null && uncle.color == COLOR.RED) {
//情况一
parent.color = COLOR.BLACK;
uncle.color = COLOR.BLACK;
grandfather.color = COLOR.RED;
//当grandfather的节点颜色变为了红色之后,可能又会破坏其他树的结构,所以需要继续向上调整
cur = grandfather;
parent = cur.parent;
}else {
//else 就表示uncle为空或者uncle不为空且uncle颜色为黑色的情况
if (cur == parent.right) {
rotateLeft(parent);
RBTreeNode tmp = parent;
parent = cur;
cur = tmp;
}
rotateRight(grandfather);
grandfather.color = COLOR.RED;
parent.color = COLOR.BLACK;
}
}else {
uncle = grandfather.left;
if (uncle != null && uncle.color == COLOR.RED) {
//情况一
parent.color = COLOR.BLACK;
uncle.color = COLOR.BLACK;
grandfather.color = COLOR.RED;
//当grandfather的节点颜色变为了红色之后,可能又会破坏其他树的结构,所以需要继续向上调整
cur = grandfather;
parent = cur.parent;
}else {
//else 就表示uncle为空或者uncle不为空且uncle颜色为黑色的情况
if (cur == parent.left) {
rotateRight(parent);
RBTreeNode tmp = parent;
parent = cur;
cur = tmp;
}
rotateLeft(grandfather);
grandfather.color = COLOR.RED;
parent.color = COLOR.BLACK;
}
}
}
root.color = COLOR.BLACK;
return true;
}
public void rotateRight(RBTreeNode parent) {
RBTreeNode subL = parent.left;
RBTreeNode subLR = subL.right;
parent.left = subLR;
if (subLR != null) {
subLR.parent = parent;
}
RBTreeNode pParent = parent.parent;
if (root == parent) {
root = subL;
subL.parent = null;
}else {
if (pParent.left == parent) {
pParent.left = subLR;
}else {
pParent.right = subLR;
}
subLR.parent = pParent;
}
subL.right = parent;
parent.parent = subL;
}
public void rotateLeft(RBTreeNode parent) {
RBTreeNode subR = parent.right;
RBTreeNode subRL = subR.left;
RBTreeNode pParent = parent.parent;
parent.right = subRL;
if (subRL != null) {
subRL.parent = parent;
}
if (root == parent) {
root = subR;
root.parent = pParent;
}else {
if (pParent.left == parent) {
pParent.left = subR;
}else {
pParent.right = subR;
}
subR.parent = pParent;
}
subR.left = parent;
parent.parent = subR;
}
}
验证是否为红黑树
验证是否为红黑树,首先需要验证是否为二叉搜索树,然后验证每个路径上的黑色节点的数量是否相等,还需要验证在一条路径中是否存在两个连续的红色节点:
检验是否为二叉搜索树:
private int prev = Integer.MIN_VALUE;
public boolean isBinarySearchTree(RBTreeNode root) {
if (root == null) return true;
boolean l = isBinarySearchTree(root.left);
if (!l) return false;
if (prev < root.val) {
prev = root.val;
return isBinarySearchTree(root.right);
}else return false;
}
校验所有路径中的黑色节点的数量是否相等:
//先计算出红黑树中其中一条路径中黑色节点的数量
public int blackNum(RBTreeNode root) {
if (root == null) return 0;
int count = 0;
RBTreeNode cur = root;
if (root.left != null) {
while (cur != null) {
if (cur.color == COLOR.BLACK) count++;
cur = cur.left;
}
}else if (root.right != null){
while (cur != null) {
if (cur.color == COLOR.BLACK) count++;
cur = cur.right;
}
}else {
count = root.color == COLOR.BLACK ? 1 : 0;
}
return count;
}
//根据传入的路径中黑色节点的数量判断是否所有路径上的黑色节点的数量相同
public boolean checkBlackNum(RBTreeNode root, int pathBlackNum, int blackNum) {
if (root == null) return true;
if (root.color == COLOR.BLACK) pathBlackNum++;
if (root.left == null && root.right == null) {
if (pathBlackNum == blackNum) return true;
else return false;
}
return checkBlackNum(root.left, pathBlackNum, blackNum)
&& checkBlackNum(root.right, pathBlackNum, blackNum);
}
判断一条路径上是否存在两个连续的红色节点:
public boolean checkRedColor(RBTreeNode root) {
if (root == null) return true;
if (root.color == COLOR.RED) {
if (root.left != null && root.left.color == COLOR.RED) return false;
if (root.right != null && root.right.color == COLOR.RED) return false;
}
return checkRedColor(root.left) && checkRedColor(root.right);
}
整理接口:
public boolean checkRBTree(RBTreeNode root) {
int blackNum = blackNum(root);
return isBinarySearchTree(root) && checkBlackNum(root,0,blackNum)
&& checkRedColor(root);
}
红黑树的删除
红黑树的删除操作主要涉及以下几个步骤:
- 定位节点:找到要删除的节点。如果要删除的节点有两个子节点,则需要找到该节点的后继节点(通常是右子树中的最小节点)来替代要删除的节点。
- 执行删除:执行标准的二叉搜索树(BST)的删除操作。这涉及到将后继节点的值复制到当前节点,并删除后继节点。如果后继节点有子节点,这些子节点将被转移到被删除节点的位置。
- 修复红黑树性质:删除节点后,可能会破坏红黑树的性质。因此,需要通过一系列的旋转和颜色更改操作来修复这些性质。这些操作包括左旋、右旋以及重新着色节点,以确保满足红黑树的五大特征。
- 处理特殊情况:在删除操作中,可能会遇到一些特殊情况,例如要删除的节点是黑色且拥有两个红色子节点,或者要删除的节点是根节点等。这些情况需要特殊的处理方式来确保红黑树的性质得到维护。
这里我就不为大家详细介绍了,大家下去可以自己了解了解。