【C++】——红黑树(手撕红黑树,彻底弄懂红黑树)

目录

前言

一  红黑树简介

二  为什么需要红黑树

三  红黑树的特性

四  红黑树的操作

4.1  变色操作

4.2  旋转操作

4.3 插入操作

4.4  红黑树插入代码实现

  4.5   红黑树的删除

五 红黑树迭代器实现

总结


前言

我们之前都学过ALV树,AVL树的本质就是一颗平衡二叉树,它的作用就是查找,插入和删除节点,最坏的时间复杂度都是O(logn)的,同时维护的高度差都是小于等于1的,但是也就是因为这个原因才被红黑树所替代

一  红黑树简介

红黑树是一种自平衡的二叉查找树,是一种高效的查找树。它是由 Rudolf Bayer 于1978年发明,在当时被称为平衡二叉 B 树(symmetric binary B-trees)。后来,在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的红黑树。红黑树具有良好的效率,它可在 O(logN) 时间内完成查找、增加、删除等操作。

二  为什么需要红黑树

对于二叉搜索树,如果插入的数据是随机的,那么它就是接近平衡的二叉树,平衡的二叉树,它的操作效率(查询,插入,删除)效率较高,时间复杂度是O(logN)。但是可能会出现一种极端的情况,那就是插入的数据是有序的(递增或者递减),那么所有的节点都会在根节点的右侧或左侧,此时,二叉搜索树就变为了一个链表,它的操作效率就降低了,时间复杂度为O(N),所以可以认为二叉搜索树的时间复杂度介于O(logN)和O(N)之间,视情况而定。那么为了应对这种极端情况,红黑树就出现了,它是具备了某些特性的二叉搜索树,能解决非平衡树问题,红黑树是一种接近平衡的二叉树(说它是接近平衡因为它并没有像AVL树的平衡因子的概念,它只是靠着满足红黑节点的5条性质来维持一种接近平衡的结构,进而提升整体的性能,并没有严格的卡定某个平衡因子来维持绝对平衡)。还有一点就是,AVL树需要大量的旋转,相比较红黑树来说效率有所减低

     

三  红黑树的特性

首先,红黑树也是一个二叉搜索树,也就是右边大,左边小,红黑树保证最长路径不超过最短路径的二倍,因而近似平衡(最短路径就是全黑节点,最长路径就是一个红节点一个黑节点,当从根节点到叶子节点的路径上黑色节点相同时,最长路径刚好是最短路径的两倍。它同时满足以下特性:

1.根结点肯定树黑色的

2.不能出现连续的红色节点

3.从一节点到叶子节点到所有路径黑色节点的数量上相同的

4.节点的颜色顾名思义不是黑色就是红色

有了上面的认识,我们可以判断下面的图是不是红黑树,乍一看是没有违反规则的,但是实际上是这样吗?

但实际上,在红黑树中真正被定义为叶子结点的,是那些空节点,如下图。

可以看出他们的黑色结点数量并不相等,所以不是一颗红黑树

四  红黑树的操作

红黑树的基本操作和其他树形结构一样,一般都包括查找、插入、删除等操作。前面说到,红黑树是一种自平衡的二叉查找树,既然是二叉查找树的一种,那么查找过程和二叉查找树一样,比较简单,这里不再赘述。相对于查找操作,红黑树的插入和删除操作就要复杂的多。

对于红黑树的操作来说,因为和ALV树是有所区别的,所以旋转的操作是要少于ALV树的,那红黑树肯定就付出了其他的努力去替代这个操作,那就是变色,这也是红黑树的内核所在

4.1  变色操作

什么时候才需要变色呢?

当我们插入一个结点,造成有连续的红节点的时候,变色就是必不可少的了,之所以有连续的红是结点,是因为我们不能插入一个黑色结点,因为插入一个黑色结点会导致黑色结点的数量增多,使得另外的树无法去平衡这这棵树,所以插入黑色结点的代价要远比插入红色结点的代价要大得多

所以我们可以通过变色处理子树红色和黑色结点直接的位置关系,来达到子树本身的平衡



4.2  旋转操作

这里的旋转操作其实和ALV树的旋转是一样的,分为左旋右旋,左右双旋和右左双旋,但是我们这样旋转一般也需要用到变色操作,也就是旋转加变色操作使得红黑树平衡

如果有需要了解旋转的具体实现就看ALV树的旋转

4.3 插入操作

检测新节点插入后,红黑树的性质是否造到破坏?
因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何
性质,则不需要调整;但当新插入节点的双亲节点颜色为红色时,就违反了不能有连在一起的红色节点,此时需要对红黑树分情况来讨论:


约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点

情况一: cur为红,p为红,g为黑,u存在且为红 

这里向上调整导致,g的父亲结点变成下一个cur

这里可能就有疑问了,为什么单纯把p,u改为黑色,g变成红色??

1.g,p,u都为黑色

2.p 单独变黑色

这两种看起来没什么问题,但是对于1来说,如果这颗树为子树,那么就会多出来一个黑色结点,这是犯了大忌的,所以不可取。对于2来说,也是一样的道理。

所以这里采取的是p ,u 变黑色根节点变红,这样就维持了黑色结点的数量

如果 g 是根节点,那么直接变黑就行了

情况二: cur为红,p为红,g为黑,u不存在/u存在且为黑

对于这种情况单纯的变色已经处理不了了,因为我们无论怎么变色处理,右子树都没有能力使得左右子树的黑色结点数量相同

这里我们的处理就是旋转加变色

1.p为g的左孩子,cur为p的左孩子,则进行右单旋转;相反,
2.p为g的右孩子,cur为p的右孩子,则进行左单旋转
3.p、g变色--p变黑,g变红

注意:这里无论u结点是否存在,都进行一样的操作,进行操作过后都会使得左右子树的黑色结点数量相同,因为在上面的情况讨论中,如果u结点是黑色,那么cur一定是更新上来的,所以cur下面肯定是有黑色结点保持平衡的,所以这里的旋转过后也是平衡的,如果u不存在,那么旋转加变色也是没有任何问题的,可以自己画图模拟一遍

情况三: cur为红,p为红,g为黑,u不存在/u存在且为黑

p为g的左孩子,cur为p的右孩子,则针对p做左单旋转;相反,
p为g的右孩子,cur为p的左孩子,则针对p做右单旋转
则转换成了情况2后面就按 情况2 的来就行了

4.4  红黑树插入代码实现

我们要插入得先找到插入的位置在哪

bool insert(const T& data){if (_root == nullptr)//如果头节点为空,直接插到头节点上{_root = new Node(data);_root->_col = BLACK;return make_pair(iterator(_root), true);}Node* parent = nullptr;//如果不是空,那么设置一个当前结点和一个父亲结点去寻找插入的位置Node* cur = _root;while (cur){if (cur->_data < data)//这里和二叉搜索树的情况一样{parent = cur;cur = cur->_right;}else if(cur->_data>data){parent = cur;cur = cur->_left;}else{return false;找到了那么说明存在一个相同的结点,那么直接返回false;}}cur = new Node(data);//走到这里说明找到的位置合适在这里进行插入Node* newnode = cur;cur->_col = RED;//颜色设置为红色//这里还需要判断是在父亲的右还是左,把它和它的父亲结点链接上if (parent->_data < data){parent->right = cur;cur->_parent = parnet;}else{parent->_left = cur;cur->_parent = parent;}

插入结点后,我们就要开始维护红黑树的结构了,这里我们按照上面的情况一 一去模拟就行

1.首先我们需要判断的是父亲结点是否存在,如果存在那它的颜色是什么,从这里开始判断后面的操作,如果不存在或者是黑色那么我们就不用去调整了,因为我们插入的结点是红色

2.如果需要去调整,那我们就应该设置一个祖宗结点,有便于向上调整。

3.判断叔叔结点是否存在且颜色为红还是黑,这关乎到了如果进行调整

4.如果叔叔结点不存在,如果父亲结点在祖宗结点的左边,那么对祖宗结点进行右单旋加变色处理

如果在右边,则相反

5.如果叔叔存在且为黑,和上面一样的判断,在左边,对祖宗结点进行一个右单旋加变色,在右边则对父亲结点进行左单旋,然后再对祖宗结点进行右单旋

6.如果叔叔存在且为红,那么变色就行,把父亲和叔叔结点变为黑色,把祖宗结点变为红色,这样就保持了黑色结点的平衡,如果这里把祖宗结点变为黑色,如果是根节点还可以,如果不是根节点那么就不行,因为多了一个黑色结点

while (parent && parent->_col == RED){Node* grandfather = parent->_parent;if (parent == grandfather->_left){Node* uncle = grandfather->_right;if (uncle && uncle->_col == RED)//如果为红色且存在{parent->_col = uncle->col = BLACK;grandfather->_col = RED;cur = grandfather;parent = cur->_parent;}else//如果不存在和黑色的处理是一样的,上面的情况讨论有说明{if (cur == parent->_left){RotateR(grandfather);grandfather->_col = RED;parent->_col = BLACK;}else{RotateL(parent);RotateR(grandfather);cur->_clo = BLACK;grandfather->_col = RED;}break;}}else if (parent == grandfather->_right)//这里就是反过来{Node* uncle = grandfather->_left;if (uncle && uncle->_col == RED){parent->_col = uncle->col = BLACK;grandfather->_col = RED;cur = grandfather;parent = cur->_parent;}else{if (cur = parent->_right){RoateL(grandfather);parent->_col = BLACK;grandfather->_col = RED;}else{RoateR(parent);RoateL(grandfather);cur->_col = BLACK;grandfather->_col = RED;}break;}}}_root->_col = BLACK;return true;}

  4.5   红黑树的删除

这里红黑树的删除其实和搜索二叉树那里的删除是差不多的思路,只不过加了红黑树的调整,如果对于搜索二叉树的删除过程忘记了可以参考搜索二叉树详解这篇博客

对于ALV树来说,删除和插入对比起来复杂太多,红黑树就更不用说了,删除一个结点,删除完毕以后还要去调整变色,删除叶子结点还行,如果是删除中间结点那么就会变得异常复杂,想到这里我就不想进行下去了😂😂,了解了解就行,看着都恐怖

五 红黑树迭代器实现

对于树形结构的迭代器来说相比于其他迭代器是要复杂一些的,因为不再是链式结构那样无脑遍历了

首先我们遍历二叉树是采取的中序遍历(左子树,根,右子树),所以我们得先想清楚遍历情况

我们在进行迭代器的++的时候,考虑的是该结点的是否存在右子树,因为我们位于一个结点上,说明左子树已经遍历完了,如果说右子树存在,那么就去找右子树的最左结点

如果右子树不存在,再++则需要看这课子树是不是遍历完了,如果当前结点的父亲结点右指针是当前结点,说明这个子树完了,则需要往上调整,继续判断这种情况

这张图很显然在根的左子树上,可以看到现在以1为结点的子树遍历完了,所以应该遍历根,再然后进入右子树找最左结点继续上面的遍历

如果上面不是很理解那么就看代码理解一遍

	Self& operator++(){if (_node->_right){// 下一个就是右子树的最左节点Node* cur = _node->_right;while (cur->_left){cur = cur->_left;}_node = cur;}else{// 左子树 根 右子树// 右为空,找孩子是父亲左的那个祖先Node* cur = _node;Node* parent = cur->_parent;while (parent && cur == parent->_right){cur = parent;parent = parent->_parent;}_node = parent;//这个时候就是第二张图,此时_node应该指向的是父亲结点,因为下一个//遍历的结点是根结点} return *this;}

那我们在进行--操作其实代码和++是一样的, 这里可以想一下,我们++是怎么操作的,比如我们++要找下一个结点,那么就会找右子树的最左结点,其实最后找到的是右子子树的最右结点

比如这里的27号结点,当我们再++就往回退了。

那现在我们看这张图,6结点++以后到1,再++到8,那么我们--就需要到1,那么就和++是一样的,先判断该节点的左子树是否存在,然后找左子树的最右结点,然后退无可退的时候就往回返了

	Self& operator--(){if (_node->_left){//下一个是左子树的最右节点Node* right = _node->_left;while (right->_right){right = right->_right;}_node = right;}else{//孩子不是父亲的左的那个祖先Node* parent = _node->_parent;Node* cur = _node;//如果是父亲的右子树,就一直往上走//如果parent已经为空,那么就停止循环,parent已经到达了我们的end的位置while (parent && cur == parent->_left){cur = cur->_parent;parent = parent->_parent;}_node = parent;}return *this;}

总结:这里可能比较绕,时间久了不记得了,我们只需要知道++的时候是往右边走,也就是找右子树的最左节点,--的时候往左边走,找左子树的最右结点,如果到底了就往回返

这里开始的begin()就是最左结点,end是空,因为我们一直++一定会返回到根,根的父亲为空

总结

红黑树这里其实插入操作也不是很难,迭代器有点绕,多结合图去理解代码!!!

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

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

相关文章

计算机实验室排课查询小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;学生管理&#xff0c;教师管理&#xff0c;实验室信息管理&#xff0c;实验室预约管理&#xff0c;取消预约管理&#xff0c;实验课程管理&#xff0c;实验报告管理&#xff0c;报修信息管理&#xff0…

【Python】如何在 Python 中操作 Excel

Python 操作 Excel 文件 1. 常用的几种库 xlrd和xlwt库 xlrd&#xff1a;用于读取Excel文件。xlwt&#xff1a;用于写入Excel文件。这两个库通常一起使用&#xff0c;xlrd用于读取&#xff0c;xlwt用于写入&#xff0c;但它们不支持Excel 2007及以后的.xlsx格式。 openpyxl库…

Linux的yum源安装MySQL5.7

linux的yum源安装MySQL5.7 一、MySQL 1、简介 MySQL 是一种流行的关系型数据库管理系统&#xff08;RDBMS&#xff09;&#xff0c;由瑞典公司 MySQL AB 开发&#xff0c;后来被 Oracle Corporation 收购。它是一个开源软件&#xff0c;提供了高效、稳定和可靠的数据管理解决…

【Spring Framework】Spring 事务使用的完整示例

为了详细展示 Spring 事务的使用&#xff0c;我将提供一个完整的示例&#xff0c;包括配置、代码和说明。这将涵盖以下几个方面&#xff1a; 数据库配置&#xff1a;包括数据源和事务管理器的配置。实体类&#xff1a;用于数据库操作的数据模型。DAO 层&#xff1a;数据访问对…

Android SurfaceFlinger——GraphicBuffer的提交(三十三)

在 SurfaceFlinger 中,我们 dequeueBuffer 和 queueBuffer 是 Surface 控制接口中非常重要的两个函数,分别用于从 Surface 的 BufferQueue 中取出缓冲区和向 BufferQueue 提交(队列)缓冲区。这两个函数在生产者和消费者模型中扮演着核心角色,确保了图像数据的高效和有序传…

消息队列原理题库

1. 简述什么是消息队列 &#xff1f; 消息&#xff08;Message&#xff09;是指在应用间传送的数据。消息可以非常简单&#xff0c;比如只包含文本字符串&#xff0c;也可以更复杂&#xff0c;可能包含嵌入对象。 消息队列&#xff08;Message Queue&#xff09;是一种应用间…

【算法】子集

难度:中等 题目: 给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的 子集(幂集)。 解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。 示例 1: 输入:nums = [1,2,3] 输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]] 示例 2: 输入…

Spring AI (三) 提示词对象Prompt

3.提示词对象Prompt 3.1.Prompt Prompt类的作用是创建结构化提示词, 实现了ModelRequest<List<Message>>接口 Prompt(String contents)&#xff1a;创建一个包含指定内容的Prompt对象。 Prompt(String contents, ChatOptions modelOptions)&#xff1a;创建一个…

某数据泄露防护(DLP)系统NoticeAjax接口SQL注入漏洞复现 [附POC]

文章目录 某数据泄露防护(DLP)系统NoticeAjax接口SQL注入漏洞复现 [附POC]0x01 前言0x02 漏洞描述0x03 影响版本0x04 漏洞环境0x05 漏洞复现1.访问漏洞环境2.构造POC3.复现某数据泄露防护(DLP)系统NoticeAjax接口SQL注入漏洞复现 [附POC] 0x01 前言 免责声明:请勿利用文章内…

DolphinDB Web 端权限管理:可视化操作指南

在现代数据库管理中&#xff0c;高效和直观的权限管理对于用户的数据安全是至关重要的。过去 DolphinDB 用户需要依赖系统脚本来管理用户和权限&#xff0c;这对于缺乏技术背景的管理员来说既复杂又容易出错。 为了提升用户体验和操作效率&#xff0c;DolphinDB 目前在 Web 上…

数据库设计三范式

文章目录 数据库设计三范式第一范式第二范式第三范式一对一怎么设计主键共享外键唯一 一对多怎么设计多对多怎么设计 数据库设计三范式 数据库表设计的原则。教你怎么设计数据库表有效&#xff0c;并且节省空间。 如果客户有速度要求极致&#xff0c;可以不用。根据客户需求来 …

vue3中动态添加form表单校验

<template><div><div v-for"(formData, index) in forms" :key"index"><u-form :model"formData" :rules"rules" ref"formRefs"><u-form-item label"用户名" prop"username"…

MySQL数据库练习(5)

1.建库建表 # 使用数据库 use mydb16_trigger;# 表格goods create table goods( gid char(8) primary key, name varchar(10), price decimal(8,2), num int);# 表格orders create table orders( oid int primary key auto_increment, gid char(10) not null, name varchar(10…

scrapy 爬取旅游景点相关数据(一)

第一节 Scrapy 练习爬取穷游旅游景点 配套视频可以前往B站&#xff1a;https://www.bilibili.com/video/BV1Vx4y147wQ/?vd_source4c338cd1b04806ba681778966b6fbd65 本项目为scrapy 练手项目&#xff0c;爬取的是穷游旅游景点列表数据 0 系统的环境 现在网上可以找到很多scr…

其它------

这里写目录标题 一级目录二级目录三级目录 一级目录 二级目录 三级目录

Pytorch使用教学6-张量的分割与合并

在使用PyTorch时&#xff0c;对张量的分割与合并是不可避免的操作&#xff0c;本节就带大家深刻理解张量的分割与合并。 在开始之前&#xff0c;我们先对张量的维度进行深入理解&#xff1a; t2 torch.zeros((3, 4)) # tensor([[0., 0., 0., 0.], # [0., 0., 0., 0.…

异或和+博弈论

1.最后状态a1a2a3...an0,显然a1^a2^a3...^an0 2.对ai操作&#xff0c;变成new ai,等价于ai异或一个数字x,使得ai^xnew ai 3.因此得出结论&#xff1a;对某一堆石子(ai)的操作实际上等同于对该堆石子的异或 4.当前我的状况&#xff1a;a1^a2^a3...^an0,假设上一步,你的状况a1^a2…

C语言边界互通传送迷宫

目录 注意事项开头程序程序的流程图程序输入与输出的效果结尾 注意事项 程序里有关字符’\033’的输出都关于Sunshine-Linux的其中一篇博客——《printf函数高级用法设置打印字体颜色和背景色等》 开头 大家好&#xff0c;我叫这是我58。今天&#xff0c;我们来看一下我用C语…

现代Java开发:使用jjwt实现JWT认证

前言 jjwt 库 是一个流行的 Java 库&#xff0c;用于创建和解析 JWT。我在学习spring security 的过程中看到了很多关于jwt的教程&#xff0c;其中最流行的就是使用jjwt实现jwt认证&#xff0c;但是教程之中依然使用的旧版的jjwt库&#xff0c;许多的类与方法已经标记弃用或者…

“分布式” 和 “集群”

“分布式” 和 “集群” 是计算机系统领域中的两个重要概念&#xff0c;它们在设计和实现现代计算架构时经常被提到。虽然它们有重叠之处&#xff0c;但也有显著的区别。 集群&#xff08;Cluster&#xff09; 集群 是由多个计算节点&#xff08;通常是计算机或服务器&#x…