文章目录
目录
前言
大家好,今天带着大家手撕红黑树
一 . 红黑树的概念
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何 一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。
二 . 红黑树的性质
红黑树满足以下性质:
- 每个节点要么是红色,要么是黑色。
- 根节点是黑色。
- 每个叶子节点(NIL节点,空节点)是黑色。
- 如果一个节点是红色,则它的子节点必须是黑色。
- 从任一节点到其每个叶子节点的所有路径都包含相同数目的黑色节点。
思考:为什么满足上面的性质,红黑树就能保证:其最长路径中节点个数不会超过最短路径节点个数的两倍?
根据性质四可知: 两个红色节点无法相连,那么最长路径中红色节点的个数应该小于等于黑色节点的个数,又因为各个路径中黑色节点的个数相等,可以得出最长路径中节点个数不会超过最短路径节点个数的两倍。
三 . 红黑树节点的定义
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; color = COLOR.RED;// 新创建的节点默认是红色 } }public enum COLOR { RED,BLACK }
思考:在节点的定义中,为什么要将节点的默认颜色给成红色的?
不妨来看一下如果默认值给成黑色会发生什么?
当我们向红黑树中插入新节点时,如果将新节点默认设置为黑色,那么插入黑色节点可能会违反性质5(即从任意节点到其每个叶子节点的所有路径都包含相同数目的黑色节点),因为插入黑色节点会导致从插入节点到其叶子节点的黑色节点数量比其他路径少1,这就破坏了性质5。为了避免这种情况的发生,将插入节点默认设置为红色可以更容易地满足性质5。
另外,插入红色节点后,我们可以通过重新着色和旋转等操作来保持红黑树的性质,这样可以减少对树的调整次数,提高插入操作的效率。
四 . 红黑树的插入
红黑树的插入是在二叉搜索树的基础上增加了平衡限制条件,可以说是平衡树的plus版本,只是并没有向AVL树一样要求绝对的平衡(任意节点的左右子树高度差不超过1)
红黑树的插入可以分为两步:
- 1. 二叉搜索树的形式插入节点(完全照搬二叉搜索树)
- 2. 检测插入节点之后红黑树的性质是否遭到破坏
- 2.1 因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何性质,则不需要调整;
- 2.2 但当新插入节点的双亲节点颜色为红色时,就违反了性质4不能有连在一起的红色节点,此时需要对红黑树分情况来讨论
第一步: 以二叉搜索树的形式插入节点,可以去看看AVL树,因为后面右旋转的内容,插入的代码也有
第二步: 如果双亲节点的颜色是黑色代表已经平衡,反之分三种情况进行讨论
在这里做一个约定:child为当前节点,p(parent)为父节点,g(grandFather)为祖父节点,u(uncle)为叔叔节点
情况一: child为红,p为红,g为黑,u存在且为红
肉眼可见的是该树已然在插入node节点之后被调整成为了一颗红黑树,但是真的是这样吗?
即使我们将以g为根节点的这棵 '树' 调整为了红黑树但是我们并不能确定该树是否是一颗子树,换言之我们无法确认g是否有双亲节点,如果没有的话那么恭喜! 我们调整已然完成,但是是否存在这样一种可能? g存在双亲节点,如果我们将g节点的颜色改变确实是实现了一种平衡,不过这种平衡是一种局部平衡! g的改变有可能会影响到上面的红黑性质!
至此情况一算是分析完成! 部分代码
// 开始红黑树的调整
while(parent != null && parent.color == COLOR.RED){
RBTreeNode grandFather = parent.parent;
if(grandFather.left == parent) { // 为了定位uncle
RBTreeNode uncle = grandFather.right;
if(uncle != null && uncle.color == COLOR.RED){
// 情况一: uncle 存在且为红
parent.color = uncle.color = COLOR.BLACK;
grandFather.color = COLOR.RED;
// 继续向上调整
child = grandFather;
parent = child.parent;
}else{
}
}else{
}
}
}
分析了这么多,代码只有短短的几行,悲催
情况二: child为红,p为红,g为黑,u不存在或者为黑
抽象图
按照情况一的规则调整到这里,在调整过程中出现了情况二的情况,在这里就需要进行右旋!
此时即使g还有父亲节点也没关系,因为g的父节点的指向一直都是黑色! 最开始指向g,旋转之后指向parent,也就是情况一的第二种情况!!
情况三: child为红,p为红,g为黑,u不存在或者为黑
注意我这里并没有条件并没有写错只不过是把情况二拆分成了两份便于理解
上面的情况二用到了左旋,那么相必右旋也是有用武之地的!
观察上图中靠右边的树,是不是有一种似曾相识的感觉?
没错,和情况二不能说是一模一样,但也是大相径庭! 交换parent和child,按照情况二的步骤走!
至此插入完美结束! 代码
public boolean insert(int val){
RBTreeNode parent = null;
RBTreeNode child = root;
RBTreeNode node = new RBTreeNode(val);
if(child == null){
root = node;
root.color = COLOR.BLACK;
return true;
}
while(child != null){
if(child.val > val){
// 向右边迭代
parent = child;
child = child.left;
}else if(child.val < val){
// 向左边迭代
parent = child;
child = child.right;
}else{
return false;
}
}
// 正式插入节点
if(parent.val > val){
parent.left = node;
}else{
parent.right = node;
}
node.parent = parent;
child = node;
// 开始红黑树的调整
while(parent != null && parent.color == COLOR.RED){
RBTreeNode grandFather = parent.parent;
if(parent == grandFather.left) { // 为了定位uncle
RBTreeNode uncle = grandFather.right;
if(uncle != null && uncle.color == COLOR.RED){
// 情况一: uncle 存在且为红
parent.color = uncle.color = COLOR.BLACK;
grandFather.color = COLOR.RED;
// 继续向上调整
child = grandFather;
parent = child.parent;
}else{
// uncle不存在或者为黑
// 情况三 -> 情况二
if(child == parent.right){
rotateLeft(parent);
RBTreeNode tmp = child;
child = parent;
parent = tmp;
}
// 情况二
rotateRight(grandFather);
parent.color = COLOR.BLACK;
grandFather.color = COLOR.RED;
}
}else {
RBTreeNode uncle = grandFather.left;
if (uncle != null && uncle.color == COLOR.RED) {
// uncle 存在且为红
parent.color = uncle.color = COLOR.BLACK;
grandFather.color = COLOR.RED;
// 继续向上修改
child = grandFather;
parent = child.parent;
} else {
// uncle不存在或者为黑
// 情况三 -> 情况二
if (parent.left == child) {
rotateRight(parent);
RBTreeNode tmp = child;
child = parent;
parent = tmp;
}
// 情况二
rotateLeft(grandFather);
parent.color = COLOR.BLACK;
grandFather.color = COLOR.RED;
}
}
}
root.color = COLOR.BLACK;
return true;
}
注: 以上代码如果存在错误,麻烦评论区指正
五 . 红黑树的验证
红黑树的验证可以分为两步
1.检测是否满足二叉搜索树(中序遍历是否有序)
2.检测是否满足红黑树的性质
/**
* 校验红黑树的性质
* @param root
* @return
*/
public boolean isRedBlack(RBTreeNode root){
if(root.color != COLOR.BLACK){
System.out.println("不是红黑树,不满足根节点是黑色!");
return false;
}
if(!checkRed(root)){
return false;
}
return checkBlackNum(root, 0, getBlackNum(root));
}
/**
* 得到一条路径黑色节点的个数
* @param root
* @return
*/
private int getBlackNum(RBTreeNode root){
int ret = 0;
while(root != null && root.color == COLOR.BLACK){
root = root.left;
ret++;
}
return ret+1;
}
/**
* 校验各个路径上的黑色节点个数是否相同
* @param root 根节点
* @param pathBlackNum 路径上的黑色节点个数
* @param blackNum 一条路径上黑色节点的个数
* @return 是否是红黑树
*/
private 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){
System.out.println("不是红黑树,违反了各个路径上的黑色节点个数相同的性质!");
return false;
}
}
return checkBlackNum(root.left,pathBlackNum,blackNum)&&checkBlackNum(root.right,pathBlackNum,blackNum);
}
/**
* 校验是否有相连的红色节点
* @param root 根节点
* @return 是否是红黑树
*/
private boolean checkRed(RBTreeNode root) {
if(root == null){
// 空树也是红黑树
return true;
}
if(root.color == COLOR.RED){
if(root.parent.color == COLOR.RED){
System.out.println("不是红黑树,违反了没有两个连续的红色节点!");
return false;
}
}
return checkRed(root.left) && checkRed(root.right);
}
public void inorder(RBTreeNode root){
if(root == null) return;
inorder(root.left);
System.out.print(root.val+"->");
inorder(root.right);
}
public class Test { public static void main(String[] args) { int[] arr = {4,2,6,1,3,5,15,7,16,14}; RBTree rb = new RBTree(); for (int j : arr) { rb.insert(j); } rb.inorder(rb.root); System.out.println(rb.isRedBlack(rb.root)); } }
中序结果有序并且满足红黑树的性质,校验成功
六 . 红黑树的删除
1.替罪羊法
在讲解红黑树的删除之前,先带着大家看一下替罪羊法,我们以二叉搜索树的删除为例
二叉搜索树的删除的大致步骤
首先根据二叉搜索树的性质找到需要删除的节点cur 情况一: 如果cur没有一颗子树 cur = null; 情况二: 如果cur有一颗子树[左子树或者右子树] 情况三: cur有两颗子树[找替罪羊]
首先找到需要删除的节点
public boolean remove(int val){ if(root == null){ throw new RuntimeException("树中没有节点,删除失败"); } TreeNode cur = root; TreeNode parent = null; while(cur != null){ if(val < cur.val){ parent = cur; cur = cur.left; }else if(val > cur.val){ parent = cur; cur = cur.right; }else{ removeNode(parent,cur); return true; } } return false; }
情况一: 要删除的节点左右子树全为空.这是最简单的一种情况,直接将该节点置为空
情况二: 要删除的节点的左右子树不全为空[左子树为空或者是右子树为空],此时需要分类讨论
右子树为空时
左子树为空时
情况三: 要删除节点的左右子树都不为空,此时的解决方案是找替罪羊
替罪羊的找法有两种,无论是哪一种都可以解决问题,分别是左树最右,右树最左,我们这里以左树最右的方案来
注: 左树最右是指在cur的左子树中一直向右找,找到最右边的节点,这个节点就是所谓的替罪羊
代码给出
private void removeNode(TreeNode parent, TreeNode cur) {
if(cur.left == null && cur.right == null){
// cur节点是叶子节点,直接将该节点置空
cur = null;
}else if(cur.left == null || cur.right == null){
// cur节点有一颗子树
if(cur == root){
if(cur.left == null){
root = cur.right;
}else{
root = cur.left;
}
}else if(cur.left == null){
if(parent.left == cur){
parent.left = cur.right;
}else{
parent.right = cur.right;
}
}else{
if(parent.left == cur){
parent.left = cur.left;
}else{
parent.right = cur.left;
}
}
}else{
// cur的左右两边都不为空,找替罪羊
// 左树最右
TreeNode targetParent = cur;
TreeNode target = cur.left;
while(target.right != null){
targetParent = target;
target = target.right;
}
cur.val = target.val;
if(target == targetParent.right) {
targetParent.right = target.left;
}else{
targetParent.left = target.left;
}
}
}
至此替罪羊法介绍完毕,红黑树的删除不想太多涉及,试着写了一点,太麻烦了,算了大家想了解的话下面附上链接
2.红黑树的删除
https://www.cnblogs.com/fornever/archive/2011/12/02/2270692.html
七 . AVL树和红黑树的比较
红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O( ),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比 AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。
八 . 红黑树的应用
1. java集合框架中的:TreeMap、TreeSet底层使用的就是红黑树
2. C++ STL库 -- map/set、mutil_map/mutil_set
3. linux内核:进程调度中使用红黑树管理进程控制块,epoll在内核中实现时使用红黑树管理事件块
4. 其他一些库:比如nginx中用红黑树管理timer等
总结
以上就是本篇博客的主要内容了,我们下一篇博客见