红黑树的概念
红黑树和AVL树一样,也是一种能够保持相对平衡的二叉搜索树 ,但是和AVL树不一样的是,它并没有平衡因子,而是给每个节点分了两种颜色Red 和Black ,通过对任意一条从根到叶子上各个节点的着色方式的限制,确保红黑树没有一条路径会比其他路径长出两倍,以达到相对平衡的目的

想要构造一棵红黑树需要满足以下性质
- 最长路径的长度最多是最短路径的2倍
- 每个结点不是红色就是黑色
- 根节点是黑色的
- 如果一个节点是红色的,则它的两个孩子结点是黑色的(没有2个连续的红色节点)
- 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点 (从根节点开始每条路径上的黑色节点个数相同)
- 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
满足后5个性质就可以保证红黑树的最长路径最多是最短路径的2倍
其实主要是性质4和5,可以思考一下一棵红黑树的最短的路径就是该路径上的节点都是黑色的时候 (++因为每条路径的黑节点个数相同++ ),假设最短路径是3个黑节点,所以最长的路径黑节点个数也是3个,那么这条路径怎么样才能最长?也就是红节点个数达到最多时可以使这条路径的节点最长。
不过*++一条路径上不能出现连续的两个红节点++* ,所以想要时路径最长就是每个黑节点后面都跟一个红节点 ,又因为最短路径黑节点是3个,所以最长路径上的节点个数就是黑节点+红节点=3+3=6个,这样就可以达到红黑树的最长路径最多是最短路径的2倍

最短路径时间复杂度为O(logN),最长路径复杂度为O(2*logN),相差并不是很大
实现红黑树的插入
节点定义
java
public enum COLOR {
RED,BLACK
}
static class RBtreeNode{
public RBtreeNode left;
public RBtreeNode right;
public RBtreeNode parent;
public int val;
public COLOR color;
public RBtreeNode(int val){
this.val = val;
//创建的新节点要是红色
this.color = COLOR.RED;
}
}
//新增的节点必须是红色的,如果是黑色的就会破坏每条路径黑节点个数相同,导致需要再新增节点以至于引起其他问题,如果插入红节点导致有两个连续的红节点,也只需要通过改变节点颜色或者旋转就可以恢复平衡。
节点插入
红黑树的插入可以分为两步插入和检查
- 插入:因为红黑树也是二叉搜索树,所以按照二叉搜索树的比当前节点小就插左边,比当前节点大的就插右边
- 检查:和AVL树一样当插入新节点后可能会破坏原来树的结构,因为新节点颜色默认颜色是红色,所以当新节点的父节点是是黑色时 ,此时插入节点并不会影响树的结构也就不需要调整。但是当父节点是红色时此时插入就会违反,不能有两个连续的红色节点的性质,就需要对树进行调整
插入代码
java
public Boolean insert(int val){
RBrtree.RBtreeNode node = new RBrtree.RBtreeNode(val);
//如果根节点为空直接插入
if (root == null) {
root = node;
root.color = COLOR.BLACK;
return true;
}
RBrtree.RBtreeNode parent = null;
RBrtree.RBtreeNode cur = root;
while (cur != null) {
if (val > cur.val) {
parent = cur;
cur = cur.right;
} else if (val < cur.val) {
parent = cur;
cur = cur.left;
} else {
return false;
}
}
//cur == null
if (val > parent.val) {
parent.right = node;
} else if (val < parent.val) {
parent.left = node;
}
node.parent = parent;
cur = node;
调整的情况可以分为三种:
//cur是当前节点,parent父节点,granderFather是祖父节点,uncle是叔叔节点
//以下案例是以叔叔节点在右侧为例
情况一
cur为红,parent为红,granderFather为黑,uncle存在且为红

//g代表祖父节点,u代表叔叔节点
可以看到此时新插入的cur影响到了树的平衡,不过只需要修改树的颜色即可,把parent和u改成黑色,把g改成红色,此时结点的颜色就恢复平衡了。但是g可能是其他节点的子树或者根节点
- 根节点:因为红黑树的根节点必须是黑色,所以要再把根节点改成黑色
- 其他节点的子树:因为刚刚的祖父节点被改成了红色,所以其上面的节点就可能会出现两个红色节点连续的情况,就需要接着向上检查

如果只修改parent的话就会不满足所有路径上的黑色节点树相同

如果不将g节点修改成红色的话有可能会不满足所有路径上的黑色节点树相同

情况一代码
java
//红黑调整
while (parent != null && parent.color == COLOR.RED){
RBtreeNode granderFather = parent.parent;
if(parent == granderFather.left){
RBtreeNode uncle = granderFather.right;
//情况一
if(uncle != null && uncle.color == COLOR.RED){
parent.color = COLOR.BLACK;
uncle.color = COLOR.BLACK;
granderFather.color = COLOR.RED;
cur = granderFather;
parent = granderFather.parent;
情况二
cur为红,parent为红,granderFather为黑,uncle不存在/为黑,cur在parent左边

- uncle存在:uncle存在的话,则其一定是黑色,那么原来cur的节点一定是黑色,上图中的红色是被调整的,因为uncle存在了且为黑色,此时必须满足路径上的黑色节点个数一样,所以这张图是一张在调整过程中的图的局部
- uncle不存在:uncle不存在的话,则cur一定是新插入的节点,因为如果不是的话,则cur或者parent一定有一个要是黑色的,这就相当于在这条路径上插入了一个黑色节点,就会使这条路径的黑节点个数多一个
不过两种情况的解决方法是一样的,这里以uncle存在为例,下图是在向上调整的过程中遇到的情况

调整方法:此时不能通过简单的调整颜色来解决问题了,需要将g节点先右旋,然后再调整颜色
1.右旋g节点

2.调整颜色,将p节点改成黑色,g节点改成红色

此时红黑树就又平衡了
代码(右旋的详解在博主的AVL树讲解中有)
java
public void rotateRight(RBrtree.RBtreeNode parent) {
RBrtree.RBtreeNode pParetn = parent.parent;//记录parent的父节点
RBrtree.RBtreeNode subL = parent.left;
RBrtree.RBtreeNode subLR = subL.right;
parent.left = subLR;
if (subLR != null) {
subLR.parent = parent;
}
subL.right = parent;
parent.parent = subL;
//检查当前parent是不是根节点
//是根节点的话直接让subL为根节点
if (parent == root) {
root = subL;
root.parent = null;
} else {//不是根节点,就判断parent是其父节点的左孩子还是右孩子
if (parent == pParetn.left) {//parent是左孩子,就让subL也是左孩子
pParetn.left = subL;
} else {
pParetn.right = subL;
}
subL.parent = pParetn;
}
}
//情况二
rotateRight(granderFather);
granderFather.color = COLOR.RED;
parent.color = COLOR.BLACK;
//g如果是根节点需要调整根节点指向,如果是其他节点子树需要调整父子关系
情况三
cur为红,parent为红,granderFather为黑,uncle不存在/为黑,cur在parent右边

情况三和情况二类似只是cur的位置不同,不过处理方法有些不同
1.左旋parent节点

看以看到情况三经过一次右旋变换后的结构和情况二一样,所以之后我们就可以使用情况二的方法来对红黑树进行调整
2.右旋g,并调整颜色

全部调整代码
java
//红黑调整
while (parent != null && parent.color == COLOR.RED){
RBtreeNode granderFather = parent.parent;
if(parent == granderFather.left){
RBtreeNode uncle = granderFather.right;
//情况一
if(uncle != null && uncle.color == COLOR.RED){
parent.color = COLOR.BLACK;
uncle.color = COLOR.BLACK;
granderFather.color = COLOR.RED;
cur = granderFather;
parent = granderFather.parent;
}else {
//uncle不存在/uncle为黑色
//情况三
if(cur == parent.right){
rotateLeft(parent);
RBtreeNode tmp = cur;
cur = parent;
parent = tmp;
}
//情况二
rotateRight(granderFather);
granderFather.color = COLOR.RED;
parent.color = COLOR.BLACK;
}
}else {
//parent == granderFather.right
RBtreeNode uncle = granderFather.left;
if(uncle != null && uncle.color == COLOR.RED){
//如果uncle存在且为红色
parent.color = COLOR.BLACK;
uncle.color = COLOR.BLACK;
granderFather.color = COLOR.RED;
cur = granderFather;
parent = granderFather.parent;
}else {
//uncle不存在/uncle为黑色
//情况三
if(cur == parent.left){
rotateRight(parent);
RBtreeNode tmp = cur;
cur = parent;
parent = tmp;
}
//情况二
rotateLeft(granderFather);
granderFather.color = COLOR.RED;
parent.color = COLOR.BLACK;
}
}
}
root.color = COLOR.BLACK;
return true;
//注意在调整的过程中有可能会把根节点改成红色,所以要在每次调整完后都要将根节点改成黑色
插入总结
红黑树的插入主要会遇到三种需要调整的情况
cur为红,parent为红,granderFather为黑,uncle存在且为红
解决方法:将叔叔和双亲节点改为黑色,祖父节点改为红色如果祖父的双亲节点的颜色是红色,需要继续往上调整
cur为红,parent为红,granderFather为黑,uncle不存在/为黑,cur在parent左边
解决方法:右旋granderFather,然后调整granderFather和parent的颜色
cur为红,parent为红,granderFather为黑,uncle不存在/为黑,cur在parent右边
解决方法:左旋parent节点,右旋granderFather节点,调整颜色
红黑树的验证
验证红黑树主要从三个方面:
- 验证根节点为黑色
- 验证树中没有连续的两个红色节点
- 验证每条路径上的黑节点个数都相同
验证树中没有连续的两个红色节点
遍历红黑树的每个节点,当遇到红色节点时就判断该节点的父节点是不是红色
验证每条路径上的黑节点个数都相同需要事先计算出每条路径上应该有多少黑色节点,只需要一直向左遍历得到最左边路径的黑节点数,然后判断其他路径上的黑节点数和该路径是否都相同即可
代码
java
/**
* 检验红黑树
*/
public Boolean isRBtree(RBtreeNode root){
if(root == null){
return true;
}
//根节点为红
if(root.color == COLOR.RED){
System.out.println("根节点是红色");
return false;
}
//检查有没有两个连续的红色节点
if(checkRedNode(root) == false){
return false;
}
//检查所有路径上的黑色节点个数是不是都一样
//计算每条路径上应该有的的黑节点个数
int blackNum = 0;
RBtreeNode cur = root;
while (cur != null){
if(cur.color == COLOR.BLACK){
blackNum++;
}
cur = cur.left;
}
if(checkBlackNum(root,0,blackNum) == false){
return false;
}
return true;
}
private Boolean checkRedNode(RBtreeNode root){
if(root == null){
return true;
}
//如果该节点是红颜色就检查其父节点是不是红色
if(root.color == COLOR.RED){
RBtreeNode parent = root.parent;
if(parent.color == COLOR.RED){
System.out.println("有两个连续的红色节点");
return false;
}
}
return checkRedNode(root.left) && checkRedNode(root.right);
}
/**
*
* @param root 根节点
* @param placeBlackNum 实际路径的黑节点个数
* @param blackNum 事先计算好的每条路经上应该有的黑色节点数
* @return
*/
private Boolean checkBlackNum(RBtreeNode root,int placeBlackNum,int blackNum){
if(root == null){
return true;
}
if(root.color == COLOR.BLACK){
placeBlackNum++;
}
//一条路径走完
if(root.left == null && root.right == null){
if(placeBlackNum != blackNum){
System.out.println("存在路径上的黑节点个数与其他的路径不同");
return false;
}
return true;
}
return checkBlackNum(root.left,placeBlackNum,blackNum)
&& checkBlackNum(root.right,placeBlackNum,blackNum);
}
}
红黑树和AVL数都是高效的平衡二叉搜索树,时间复杂读为O(logN),不过红黑树相较于AVL数并没有追求绝对的平衡,只是最长路径最多是最短路径的2倍而已,但是这也使红黑树降低了插入和旋转的次数,在经常进行增删的结构中性能比AVL数更好,而且相较于AVL数更简单一些
以上就是博主对红黑树知识的分享,在之后的博客中会陆续分享有关数据结构的其他知识,如果有不懂的或者有其他见解的欢迎在下方评论或者私信博主,也希望可以多多支持博主!!🥰🥰