带你搞懂红黑树的插入和删除

文章目录

  • 1. 红黑树
    • 1.1 红黑树的概念
    • 1.2 红黑树的性质
    • 1.3 红黑树节点的定义
    • 1.4 红黑树的插入
      • 找到插入的位置
      • 调节平衡
    • 1.5 红黑树的删除
      • 删除节点
      • 平衡调整
    • 1.6 红黑树和AVL树的比较

1. 红黑树

1.1 红黑树的概念

红黑树也是一种二叉搜索树,但是在每一个节点上增加了一个存储位表示节点的颜色,可以是Red或者Black

通过颜色的定义,加以对任何一条从根到叶子的路径上各个节点的颜色的一些限制,确保了红黑树没有一条路径会比其他路径长出两倍,因此是接近平衡的

1.2 红黑树的性质

  1. 每个节点不是黑色就是红色
  2. 根节点是黑色的
  3. 如果一个节点是红色的,那么他的两个孩子节点是黑色的(也就是说不可能出现连续两个红色的节点)
  4. 对于每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点
  5. 每个叶子节点都是黑色的(此处的叶子节点指的是空节点(nil))

问题:

  1. 为什么一定需要nil节点作为黑子叶子节点?

举个例子:

对于这个树来说,为了满足性质4,此时貌似每条路径上都包含相同数量的黑色节点?? 但是实际上不是!!

对于这条路径来说,实际上是只有一个黑色节点的

为了更加直观,我们将nil节点补上

就能很直观的感受到,不满足性质4,因此不是一颗二叉树

  1. 为什么满足了上面的这几个性质,就能确保红黑树没有一条路径会比其他路径长出两倍

假设每条路径上有n个黑色节点,那么最差情况下,会出现的最短的路径是路径上的所有节点都是黑色节点

可以很直观的看到,由于性质3的约束,导致红色黑色节点只能交替出现,才是最长的,因此最长的情况最多是最短情况的两倍

1.3 红黑树节点的定义

    static class RBTreeNode {private int val;private RBTreeNode left;private RBTreeNode right;private RBTreeNode parent;private Color color = Color.RED; // 节点的颜色默认为红色public RBTreeNode(int val) {this.val = val;}}

为什么要将节点的颜色默认为红色??

为了保证性质4,如果插入的节点默认为黑色,我们就需要在其他所有路径上都添加一个黑色节点,较为麻烦

1.4 红黑树的插入

找到插入的位置

public class RBTree {static class RBTreeNode {private int val;private RBTreeNode left;private RBTreeNode right;private RBTreeNode parent;private Color color = Color.RED;public RBTreeNode(int val) {this.val = val;}}private RBTreeNode root;public boolean insert(int val) {if(root == null) {root = new RBTreeNode(val);root.color = Color.BLACK;return true;}// 找到节点插入的位置(类似于二叉搜索树)RBTreeNode cur = root;RBTreeNode parent = null;while (cur != null) {if(cur.val > val) {parent = cur;cur = cur.left;}else if(cur.val < val) {parent = cur;cur = cur.right;}else {return false;}}// 插入新的节点RBTreeNode newNode = new RBTreeNode(val);if(parent.val > val) {parent.left = newNode;}else {parent.right = newNode;}newNode.parent = parent;// 下面开始调节颜色cur = newNode;}
}

调节平衡

  1. 当插入节点(cur)的双亲节点(parent)为红色,并且叔叔节点(uncle)存在且为红色

此时为了维护性质3[如果一个节点是红色的,那么他的两个孩子节点是黑色的(**也就是说不可能出现连续两个红色的节点)],**需要将parent和uncle变为黑色

但是如果此时grandparents节点上面还有节点:需要继续向上调整:

此时就会导致我们已经调整的两条路径,比其他路径多出一个黑色节点

,因此我们需要将grandparents节点变为红色,接着继续向上调整

  1. 当parent为黑色,并且uncle不存在或者为黑色

这种情况会出现在我们向上调整的路上

可以很明显的看到,左树偏高了,出现了两个红色节点连接在一块的情况,但是与情况1不同的是,此时的uncle节点是黑色的

此时我们就需要对grandparents进行右旋操作:

  1. 实际上是情况二的特殊情况,当cur是parent.right的时候:

此时需要先对parent进行左旋操作

可以发现,此时就变成情况二了接着进行情况二的讨论即可

上述的情况还存在对应的相反情况,但是处理原理一致

代码:

public boolean insert(int val) {if(root == null) {root = new RBTreeNode(val);root.color = Color.BLACK;return true;}// 找到节点插入的位置(类似于二叉搜索树)RBTreeNode cur = root;RBTreeNode parent = null;while (cur != null) {parent = cur;if(cur.val > val) {cur = cur.left;}else if(cur.val < val) {cur = cur.right;}else {return false;}}// 插入新的节点RBTreeNode newNode = new RBTreeNode(val);if(parent.val > val) {parent.left = newNode;}else {parent.right = newNode;}newNode.parent = parent;// 下面开始调节颜色cur = newNode;while (parent != null && parent.color == Color.RED) {RBTreeNode grandParent = parent.parent;// 因为parent.color == red,因此grandparent不为空if(parent == grandParent.left) {RBTreeNode uncle = grandParent.right;if(uncle != null && uncle.color == Color.RED) {// 情况1uncle.color = Color.BLACK;parent.color = Color.BLACK;grandParent.color = Color.RED;// 继续向上调整cur = grandParent;parent = cur.parent;}else {// 如果是情况3,先把情况3变成情况2if(cur == parent.right) {rotateLeft(parent);RBTreeNode tmp = cur;cur = parent;parent = tmp;}// 情况2rotateRight(grandParent);parent.color = Color.BLACK;grandParent.color = Color.RED;}}else {RBTreeNode uncle = grandParent.left;if(uncle != null && uncle.color == Color.RED) {uncle.color = Color.BLACK;parent.color = Color.BLACK;grandParent.color = Color.RED;cur = grandParent;parent = cur.parent;}else {if(parent.left == cur) {rotateRight(parent);RBTreeNode tmp = cur;cur = parent;parent = tmp;}rotateLeft(grandParent);parent.color = Color.BLACK;grandParent.color = Color.RED;}}}// 统一让根节点变黑色this.root.color = Color.BLACK;return true;}private void rotateLeft(RBTreeNode node) {RBTreeNode subR = node.right;RBTreeNode subRL = subR.left;RBTreeNode pParent = node.parent;node.parent = subR;subR.left = node;node.right = subRL;if(subRL != null) {subRL.parent = node;}if(root == node) {root = subR;root.parent = null;}else {if(pParent.left == node) {pParent.left = subR;subR.parent = pParent;}else {pParent.right = subR;subR.parent = pParent;}}}private void rotateRight(RBTreeNode node) {RBTreeNode subL = node.left;RBTreeNode subLR = subL.right;RBTreeNode pParent = node.parent;node.parent = subL;subL.right = node;node.left = subLR;if(subLR != null) {subLR.parent = node;}if(root == node) {root = subL;subL.parent = null;}else {if(pParent.left == node) {pParent.left = subL;subL.parent = pParent;}else {pParent.right = subL;subL.parent = pParent;}}}

1.5 红黑树的删除

实际上**如果先不考虑删除的节点对性质的破坏,**那么此时删除一个节点就跟普通二叉搜索树的删除是一致的

即直接删除或者间接找替换节点删除

删除节点

  1. 删除的节点左树右树都不为空 --> 替换删除

接着将替换的节点当做删除的节点,再次进行删除讨论即可

  1. 删除的节点左树右树都为空

先不考虑调整平衡的问题,那直接将remove.parent的子节点指向空即可

但是如果要删除的是根节点,需要对根节点进行重新赋值

  1. 删除的节点只有一个节点(左子节点或者右子节点),那么此时由于性质5(对于每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点),这个子节点一定是红色节点,而删除的节点一定是黑色的节点

直接用红色节点代替要删除的节点,改变红色节点为黑色即可,连平衡都不用调整

 public BRTreeNode remove(int val) {BRTreeNode node = searchNode(val);if(node == null) {return null;// 不能删除}removeNode(node);return node;}private BRTreeNode searchNode(int val) {if(root == null) {return null;}BRTreeNode cur = root;while (cur != null) {if(cur.val > val) {cur = cur.left;}else if (cur.val < val) {cur = cur.right;}else {return cur;}}return null;}private void removeNode(BRTreeNode node) {if(node.left != null && node.right != null) {BRTreeNode replaceNode = null;// 左右数都不为空 -> 查找替换节点replaceNode = node.right;while (replaceNode.left != null) {replaceNode = replaceNode.left;}node.val = replaceNode.val;// 递归调整替罪羊removeNode(replaceNode);}else if(node.left == null && node.right == null) {if(node.parent == null) {// 左树右树都是空 -> 根节点 -> 直接置空根节点this.root = null;}else {// 叶子节点if(node.color == Color.RED) {// 如果是红色节点 -> 直接删除if(node == node.parent.left) {node.parent.left = null;}else {node.parent.right = null;}}else {// 黑色节点,要进行平衡处理// 先将节点删除,记录下兄弟节点和父亲节点,后面平衡调整要用到BRTreeNode parent = node.parent;// 双亲节点BRTreeNode brother = node == parent.left ? parent.right : parent.left;// 兄弟节点if(node == parent.left) {parent.left = null;}else {parent.right = null;}keepBalanceRemove(parent,brother);}}}else {// 只有一个子节点 -> 子节点一定是红色BRTreeNode parent = node.parent;BRTreeNode son;if(node.left != null) {son = node.left;}else {son = node.right;}son.parent = parent;if(parent != null) {if(node == parent.left) {parent.left = son;}else {parent.right = son;}}else {son.color = Color.BLACK;this.root = son;}son.color = Color.BLACK;}}

平衡调整

兄弟节点 --> brother, 父亲节点 --> parent

  1. 兄弟节点是黑色的

    a. 兄弟节点的子节点全部都是黑色的(或者全为空)

    ​ i. 父亲节点是红色

因为parent的左边,被我们删除了一个黑色节点,因此通过上述的变化,就可以让parent右边也少一个黑色节点,同时原本经过parent的路径黑色节点数都不会变(上图所示都是1),平衡调整结束

​ ii.父亲节点是黑色

因为parent的左边,被我们删除了一个黑色节点,因此通过上述的变化,就可以让parent右边也少一个黑色节点

但是经过parent节点的路径将减少了一个黑色节点,如果parent没有父亲节点,就不会有任何影响,平衡结束,但是如果parent是别的节点的子节点,就需要进行平衡调整

因此需要将parent作为新的删除节点,向上检索进行讨论,直至parent为null,如下所示:

​ b. brother的子节点不全为黑色

这种情况需要分为4种(实际上是2种以及其镜像的情况) 进行讨论

​ i. brother是parent的右节点,并且brother的右节点是红色

此时相当于给原本少了黑色节点的这一边添加了一个黑色节点

为什么说是交换parent和brother的节点?? 因为这里的parent颜色是任意的,上面是红色,同样可以是黑色

​ ii.brother是parent的左节点,并且brother的左节点是红色

​ 与上一种情况是镜像情况

​ iii.brother是parent的右节点,并且brother的左节点是红色

​ 通过上述操作就可以转变成我们刚刚讨论的ii情况了

​ iv.brother是parent的左节点,并且brother的右节点是红色

​ 上述情况的镜像情况:

  1. 兄弟节点是红色的

    a.右边情况

​ b. 左边情况

代码展示:

private void keepBalanceRemove(BRTreeNode parent, BRTreeNode brother) {if(parent == null) {return; // 不需要调整}else if(brother.color == Color.BLACK){// 兄弟节点是黑色if((brother.left == null && brother.right == null) || (brother.left != null && brother.left.color == Color.BLACK && brother.right != null && brother.right.color == Color.BLACK)) {// 子节点全部是黑色节点if(parent.color == Color.RED) {brother.color = Color.RED;parent.color = Color.BLACK;}else {brother.color = Color.RED;BRTreeNode tmp = parent;parent = parent.parent;if(parent != null) {brother = tmp == parent.left ? parent.right : parent.left;}keepBalanceRemove(parent,brother); // 继续向上调整}} else {// 不全为黑色if(brother == parent.left && brother.left != null && brother.left.color == Color.RED) {// 全部往左偏,兄弟节点的left为红色// 交换parent和brother的颜色Color tmpColor = parent.color;parent.color = brother.color;brother.color = tmpColor;// 将brother.left.color 变为黑色brother.left.color = Color.BLACK;// 右旋rotateRight(parent);}else if(brother == parent.right && brother.right != null && brother.right.color == Color.RED) {// 全部往右偏,兄弟节点的right为红色// 交换parent和brother的颜色Color tmpColor = parent.color;parent.color = brother.color;brother.color = tmpColor;// 将brother.right.color 变为黑色brother.right.color = Color.BLACK;// 旋rotateLeft(parent);}else if(brother == parent.left && brother.right != null && brother.right.color == Color.RED) {// 全部往左偏,兄弟节点的left为黑色// 交换brother和brother.right的颜色Color tmpColor = brother.color;brother.color = brother.right.color;brother.right.color = tmpColor;rotateLeft(brother);keepBalanceRemove(parent,parent.left);}else if(brother == parent.right && brother.left != null && brother.left.color == Color.RED) {Color tmpColor = brother.color;brother.color = brother.left.color;brother.left.color = tmpColor;rotateRight(brother);keepBalanceRemove(parent,parent.right);}}}else if(brother.color == Color.RED) {// 兄弟节点为红色if(parent.left == brother) {Color tmpColor = parent.color;parent.color = brother.color;brother.color = tmpColor;rotateRight(parent);keepBalanceRemove(parent,parent.left);}else if(brother == parent.right){Color tmpColor = parent.color;parent.color = brother.color;brother.color = tmpColor;rotateLeft(parent);keepBalanceRemove(parent,parent.right);}}}

1.6 红黑树和AVL树的比较

红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是$ O(log_2N) $

红黑树不追求绝对平衡,只是确保了最长路径的长度不超过最短路径的两倍,因此降低了插入和旋转的次数

所以在经常进行插入和删除的情况下,AVL树更优秀

实际应用有

  1. java中的TreeSet和TreeMap底层使用的就是红黑树
  2. Linux内核中的进程调度中使用了红黑树管理进程块,epoll在内核中实现的时候使用红黑树管理事件块

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/885066.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

揭秘全向轮运动学:机动艺术与上下位机通信的智慧桥梁

✨✨ Rqtz 个人主页 : 点击✨✨ &#x1f308;Qt系列专栏:点击 &#x1f388;Qt智能车上位机专栏: 点击&#x1f388; 本篇文章介绍的是有关于全向轮运动学分析&#xff0c;单片机与上位机通信C代码以及ROS里程计解算的内容。 目录 大纲 ROS&#xff08;机器人操作系统&…

移远通信推出八款天线新品,覆盖5G、4G、Wi-Fi和LoRa领域

近日&#xff0c;全球领先的物联网整体解决方案供应商移远通信宣布&#xff0c;再次推出八款高性能天线新品&#xff0c;进一步丰富其天线产品阵容&#xff0c;更好地满足全球客户对高品质天线的更多需求。具体包括5G超宽带天线YECT005W1A和YECT004W1A、5G天线YECT028W1A、4G天…

【设计模式系列】桥接模式(十三)

一、什么是桥接模式 桥接模式&#xff08;Bridge Pattern&#xff09;是一种结构型设计模式&#xff0c;其核心目的是将抽象部分与实现部分分离&#xff0c;使它们可以独立地变化。这种模式主要用于处理那些在设计时无法确定实现细节的场合&#xff0c;或者需要在多个实现之间…

Java多态和继承(下篇)

今天接着学习多态和继承 目录 1 继承1.1 再谈初始化1.2 protect关键字1.3 继承方式1.4 final 关键字1.5 组合 2 多态2.1 多态的概念2.2 多态实现条件2.3 重写2.4 向上转型和向下转型2.4.1 向上转型2.4.2 向下转型 2.5 多态的优缺点2.6 避免在构造方法中使用重写的方法 总结 1 继…

动态规划理论基础和习题【力扣】【算法学习day.25】

前言 ###我做这类文档一个重要的目的还是给正在学习的大家提供方向&#xff08;例如想要掌握基础用法&#xff0c;该刷哪些题&#xff1f;&#xff09;我的解析也不会做的非常详细&#xff0c;只会提供思路和一些关键点&#xff0c;力扣上的大佬们的题解质量是非常非常高滴&am…

数据结构之顺序表(C语言)

1 线性表 线性表是n个具有相同特性的数据元素的有限序列&#xff0c;是一种在实际中广泛应用的数据结构&#xff0c;常见的线性表有&#xff1a;顺序表、链表、栈、队列、字符串等。 线性表在逻辑上是线性结构&#xff0c;也就说是连续的一条直线。但是在物理结构上并不一定是…

Qt——窗口

一.窗口概述 Qt 窗口是通过 QMainWindow 类来实现的。 QMainWindow是一个为用户提供主窗口程序的类&#xff0c;继承QWidget类&#xff0c;并且提供一个预定义的布局。包含一个菜单栏&#xff08;menu bar&#xff09;&#xff0c;多个工具栏&#xff08;tool bars&#xff0…

长亭那个检测能力超强的 WAF,出免费版啦

告诉你们一个震撼人心的消息&#xff0c;那个检测能力超强的 WAF——长亭雷池&#xff0c;他推出免费社区版啦&#xff0c;体验地址见文末。 八年前我刚从学校毕业&#xff0c;在腾讯做安全研究&#xff0c;看到宇森在 BlackHat 上演讲的议题 《永别了&#xff0c;SQL 注入》 …

漏洞分析 | Spring Framework路径遍历漏洞(CVE-2024-38816)

漏洞概述 VMware Spring Framework是美国威睿&#xff08;VMware&#xff09;公司的一套开源的Java、JavaEE应用程序框架。该框架可帮助开发人员构建高质量的应用。 近期&#xff0c;网宿安全演武实验室监测到Spring Framework在特定条件下&#xff0c;存在目录遍历漏洞&…

tp接口 入口文件 500 错误原因

一、描述 二、可能的原因 1、runtime目录没权限 2、关闭了Tp记录日志的功能 3、关闭debug调试模式 4、关闭了debug模式还是报错 一、描述 Thinkphp项目本地正常&#xff0c;上传到线上后静态文件访问正常&#xff0c;访问tp接口报500错误。 经调试发现&#xff0c;在php入…

第07章 运算符的使用

一、算数运算符 算术运算符主要用于数学运算&#xff0c;其可以连接运算符前后的两个数值或表达式&#xff0c;对数值或表达式进行加 &#xff08;&#xff09;、减&#xff08;-&#xff09;、乘&#xff08;*&#xff09;、除&#xff08;/&#xff09;和取模&#xff08;%&a…

十七 MyBatis的注解式开发

十七、MyBatis的注解式开发 mybatis中也提供了注解式开发方式&#xff0c;采用注解可以减少Sql映射文件的配置。 当然&#xff0c;使用注解式开发的话&#xff0c;sql语句是写在java程序中的&#xff0c;这种方式也会给sql语句的维护带来成本。 官方是这么说的&#xff1a; 使…

用 Python 写了一个天天酷跑(附源码)

Hello&#xff0c;大家好&#xff0c;给大家说一下&#xff0c;我要开始装逼了 这期写个天天酷跑玩一下叭&#xff01; 制作一个完整的“天天酷跑”游戏涉及很多方面&#xff0c;包括图形渲染、物理引擎、用户输入处理、游戏逻辑等。由于Python是一种高级编程语言&#xff0c;…

Kettle——CSV文件转换成excel文件输出

1.点击—文件—新建—转换 拖入两个组件&#xff1a; 按shift&#xff0b;鼠标左击建立连接&#xff0c;并点击主输出步骤&#xff0c; 点击CSV文件输入&#xff0c;选择浏览的csv文件&#xff0c;然后点击确定 同样&#xff0c;Excel也同上&#xff0c;只是要删除这个xls 并…

高效管理iPhone存储:苹果手机怎么删除相似照片

在使用iPhone的过程中&#xff0c;我们经常会遇到存储空间不足的问题&#xff0c;尤其是当相册中充满了大量相似照片时。这些照片不仅占用了宝贵的存储空间&#xff0c;还可能使iPhone出现运行卡顿的情况。因此&#xff0c;我们迫切需要寻找苹果手机怎么删除相似照片的方法&…

用示例来看C2Rust工具的使用和功能介绍

C2Rust可以将C语言的源代码转换成Rust语言的源代码。下面是一个简单的C语言代码示例&#xff0c;以及使用c2Rust工具将其转换为Rust安全代码的过程。 C语言源代码示例 // example.c #include <stdio.h>int add(int a, int b) {return a b; }int main() {int result a…

赛普EAP平台 Download.aspx 任意文件读取漏洞复现

0x01 产品描述&#xff1a; ‌赛普EAP平台‌是一款专门为房地产企业打造的数字化管理系统&#xff0c;旨在帮助企业实现业务流程的优化、管理效率的提升和客户体验的改善。该系统集成了项目管理、销售管理、客户关系管理、财务管理、报表分析等多个模块&#xff0c;能够满足企业…

前端三件套-css

一、元素选择器 元素选择器&#xff1a;利用标签名称。p,h1-h6...... 行内样式&#xff08;内联样式&#xff09;&#xff1a;例如<p style"color:red;font-size:50px"> id选择器&#xff1a;针对某一个特定的标签来使用。以#定义。 class&#xff08;类&a…

服务器被攻击排查记录

起因 我的深度学习的所有进程突然被killed&#xff0c;我以为是检修&#xff0c;后面发现好像简单的python代码可以正常运行。但是我的训练进程一启动就会被killed 第一时间没有用htop查看cpu&#xff0c;用top看着挺正常的&#xff0c;但是后面看htop&#xff0c;全是绿的&a…

项目实战:基于Linux的Flappy bird游戏开发

一、项目介绍 项目总结 1.按下空格键小鸟上升&#xff0c;不按小鸟下落 2.搭建小鸟需要穿过的管道 3.管道自动左移和创建 4.小鸟撞到管道游戏结束 知识储备 1.C语言 2.数据结构-链表 3.Ncurses库 4.信号机制 二、Ncurses库介绍 Ncurses是最早的System V Release 4.0 (SVr4)中…