C++数据结构重要知识点(3)(红黑树及其插入操作)

news2024/11/19 20:20:13

1.红黑树和AVL树的区别

红黑树和AVL树都是平衡树,都是为了解决二叉搜索树的劣势。其中,AVL树的左右子树的高度差不超过1,而红黑树的最长路径不超过最短路径的二倍,也就是说,红黑树是一种近似平衡,而AVL树是严格平衡。看起来红黑树要略逊一筹,但实际上两者效率差不多。当我们学习时间复杂度时,就知道n和2n在本质上没有区别,n、2n、3n......它们都是O(N),对于红黑树也是如此,就算最短和最长路径最多可以相差一倍,但对于计算机来说,最短和最长都差不多,它们是在同一个量级的。而红黑树的近似平衡还意味着它旋转的次数会比AVL树少,旋转开销上会少一些。

可以看出,红黑树牺牲了一些平衡,换来了一些旋转优势;AVL树保证平衡,但旋转次数变多了。

map和set底层使用的都是红黑树

2.红黑树的重要性质

红黑树和AVL树本质上都是控制树的高度,但两者的思想差别还是很大的。

红黑树的每个节点都对应有一种颜色,要么是红,要么是黑

(1)根节点是黑色

(2)如果一个结点是红色,那它的两个结点是黑色(不能存在连续的红节点,但黑色节点可以连续存在)

(3)对于每个节点而言,从该节点到其后代叶结点的简单路径上,均包含相同数量的黑色节点(最重要)

(4)每个NIL结点(nullptr结点)都是黑色,叶子数量 != NIL结点标识路径数量,每个叶子有两个NIL结点(了解)

(5)最长路径最多是最短路径的二倍,理论上最短路径全是黑结点,最长路径一黑一红,有最长路径和有最短路径反而意味着树不均衡

(6)最短路径横切可以得到满二叉树,最短logN,最长2 * logN

我们从上面的性质中就能了解到,红黑树控制高度依赖(2)和(3),只要保证没有连续红色结点(最长一黑一红交替,最短全黑,始终满足红黑树要求)和同根下不同路径黑色节点相同,那就一定满足高度条件,这和AVL树的平衡因子思想有较大出入,需要体会,下面我顺着红黑树的实现思路进一步分析

3.insert

定义节点,利用枚举常量来标识不同节点对应的颜色

处理空树

nodeC向下探路,nodeP找到插入节点的父节点

上面这些都和AVL树处理一致,很好理解。

插入节点

插入节点默认都是红节点,要保证从同一个根出发每条路径上的黑色节点数相同,所以要插入红节点。但是这样就又会破坏另一个规则——不能出现连续红节点。所以,接下来的所有操作都是基于黑色节点数相同的条件来解决这个问题的。换句话说,我们只需要关注连续红节点的问题。

4.两种情况的引入

我们可以从一个节点开始,慢慢地加节点,思考可能出现的情况,这是解决复杂难题的一个方式

(1)变色

根据上面插入节点的代码可知,每次插入后nodeC都指向新插入的结点,nodeP是它的父节点,nodeC和nodeP都一定不为空,nodeC一定为红

前两次插入都因为nodeP是黑色,所以跳出了。而第三个结点插入时就出现了连续红节点的情况。如何解决呢,这就是第一种情况——直接变色。

只要nodeG和nodeU存在,nodeU是红色,就可以采取这种解决办法

这样解决有一个问题,就是nodeG从黑色变成了红色,这虽然保证了黑色结点数量不变,但是却可能增加新的麻烦。

可是,难点在于循环条件该怎样写,nodeC、nodeP、nodeG同时跨了三代,要是nodeC、nodeP为空或nodeG为空呢?

首先,第一次循环的时候nodeC、nodeP必定不为空,nodeG可能为空,如果nodeG为空,那么nodeP一定是根节点,一定是黑色,所以第一次循环要么直接通过nodeP为黑跳出循环,要么就存在nodeG。

第二次及以后的循环就有多种情况了。nodeC被赋值为nodeG,一定不为空,如果nodeP为空,说明nodeC是根,在上一次循环结束后变为黑色,变色已经结束,不应该进入循环。如果nodeG为空,和上面一样,说明nodeP一定为黑,也不需要处理了。

综上,只要nodeP和nodeG有一个为空,就不应该进入循环,只需要判断循环后更新的nodeP、nodeG为不为空就可以了。只要进入循环体,就说明nodeC,nodeP、nodeG均不为空

但有一个需要注意的是nodeU可能为空,不过这是要在nodeG不为空的情况下才能讨论的,是在循环体内部讨论的,所以上面nodeU初始化为nullptr

在循环体内先将父节点不为红的处理掉,第一次循环时这个条件是一定不会触发的,你可能回想nodeP为根,但注意nodeP为根时nodeG为空,根本不会进入这个循环,这是将那些变完色后没有触发新问题的情况处理掉。

再确定nodeU的情况

之后进行变色处理,注意根的变色要特殊处理,nodeP和nodeG的更新正常进行即可,只要两者有其一为空就会跳出循环,标志着变色结束。逻辑是严密的。

当nodeU为空时又是另一种情况了。

(2)旋转

变色是尽可能地将连续红色结点的问题解决而不旋转,但在有的情况下就必须旋转了。

下面画一下简图

旋转逻辑和AVL树的一模一样,无论双旋单旋,并且变色逻辑也很简单

nodeU为空和nodeU为黑这两种情况对应的旋转操作一样(nullptr处理有细微区别),但它们出现的情况完全不一样。

当nodeU为空的时候nodeC一定是新增加的结点,因为右子树为空,所有路径都只能有根这一个黑色节点,根据不能存在连续红色结点这条规则,有且仅有nodeC为新增节点才满足。

当nodeU为黑色结点时nodeC一定不为新增结点,而是nodeC = nodeG赋值上来的。仔细看上面的图,会发现我展示的根本不算是红黑树(不同路径黑色节点不同)。如果nodeC是新插入的,那么插入结点所在路径一定不符合红黑树规则

旋转完之后就不用继续向上更新了。原因在于旋转完后的旋转点都是黑色的,无一例外。之前为什么要选择向上更新,是因为变色会导致nodeG变成红色,可能产生新的问题,而旋转就不会,只要旋转完,就代表更新完毕。

注意单双旋影响的结点不同,原因在于双旋的第一次单旋会使得nodeC和nodeP换位,使得单旋是nodeP和nodeG变色,而双旋是nodeC和nodeG变色,其余节点都不受影响。

单旋的实现和AVL树的一样,虽然模型不一样,AVL树的旋转模型对子树的高度有讲究,红黑树没有,但它们本质上要处理的结点一样,要注意的nullptr也一样。如果根要改变,就要特殊处理。

5.总体循环逻辑

红黑树在思想上是通过控制红色节点的连续性来保障高度差的问题的,这和AVL树有很大不同,需要时间接收。

在插入节点的时候,默认增加红节点,这样所有路径的黑色节点数量都不会改变。当插入新节点时,nodeC和nodeP一定不为空,如果出现了nodeC和nodeP都为红色,那就一定存在nodeG,如果这时还有nodeU且为红,就一组一组向上更新,只要nodeP、nodeG为空,就会跳出循环,不用担心越界问题。如果不符合变色规则就旋转,旋转逻辑和AVL树一样,旋转完后直接跳出循环,不再向上调整。

6.所有代码

#pragma once

#include <iostream>
#include <utility>
#include <string>
using namespace std;



namespace my_RBTree
{
	enum Color
	{
		RED,
		BLACK
	};


	template<class K, class V>
	struct RBTreeNode
	{
		RBTreeNode(const pair<K, V>& val, Color col = RED)//默认创建红节点
			:_col(col)
			,_val(val)
			,_left(nullptr)
			,_right(nullptr)
			,_parent(nullptr)
		{}
			
		Color _col;
		pair<K, V> _val;
		RBTreeNode<K, V>* _left;
		RBTreeNode<K, V>* _right;
		RBTreeNode<K, V>* _parent;
	};


	template<class K, class V>
	class RBTree
	{
		typedef RBTreeNode<K, V> Node;
	public:

		void RotateL(Node* const node)
		{
			Node* nodeP = node->_parent, * nodeR = node->_right, * nodeRL = nodeR->_left;
			node->_right = nodeRL, node->_parent = nodeR;
			nodeR->_left = node, nodeR->_parent = nodeP;

			if (nodeRL)
				nodeRL->_parent = node;
			
			if (nodeP)
			{
				if (nodeP->_left == node)
					nodeP->_left = nodeR;
				else
					nodeP->_right = nodeR;
			}

			if (node == _root)
				_root = nodeR;

		}

		void RotateR(Node* const node)
		{
			Node* nodeP = node->_parent, * nodeL = node->_left, * nodeLR = nodeL->_right;
			node->_left = nodeLR, node->_parent = nodeL;
			nodeL->_right = node, nodeL->_parent = nodeP;

			if (nodeLR)
				nodeLR->_parent = node;

			if (nodeP)
			{
				if (nodeP->_left == node)
					nodeP->_left = nodeL;
				else
					nodeP->_right = nodeL;
			}

			if (node == _root)
				_root = nodeL;

		}

		bool insert(const pair<K, V>& val)
		{
			if (_root == nullptr)
			{
				_root = new Node(val, BLACK);
				return true;
			}

			Node* nodeP = nullptr, * nodeC = _root;
			while (nodeC)
			{
				if (nodeC->_val.first == val.first)
				{
					cout << "插入失败,树中已有该key!" << endl;
					return false;//去重
				}
				if (nodeC->_val.first < val.first)
					nodeP = nodeC, nodeC = nodeC->_right;
				else
					nodeP = nodeC, nodeC = nodeC->_left;
			}

			if (nodeP->_val.first < val.first)
				nodeP->_right = new Node(val), nodeC = nodeP->_right, nodeC->_parent = nodeP;
			else
				nodeP->_left = new Node(val), nodeC = nodeP->_left, nodeC->_parent = nodeP;

			Node* nodeG = nodeP->_parent, * nodeU = nullptr;
			while (nodeP && nodeG)//只有nodeP和nodeG都存在才向上走
			{
				if (nodeP->_col != RED)//处理父亲不为红节点的情况
					break;

				if (nodeG->_left == nodeP)
					nodeU = nodeG->_right;//nodeU可能为空
				else
					nodeU = nodeG->_left;//nodeU可能为空

				//nodeU不为空
				if (nodeU && nodeP->_col == RED && nodeU->_col == RED)//这个时候nodeG一定为黑
				{
					nodeU->_col = nodeP->_col = BLACK;

					if (nodeG == _root)//变色,根节点不变色
						nodeG->_col = BLACK;
					else
						nodeG->_col = RED;

					nodeC = nodeG, nodeP = nodeC->_parent;
					if (nodeP)
						nodeG = nodeP->_parent;//nodeG为空会退出循环
					else
						nodeG = nullptr;//没有nodeP会退出循环
				}

				//nodeU为空
				else
				{
					if (nodeG->_left == nodeP)
					{
						if (nodeC == nodeP->_left)
						{
							RotateR(nodeG);//右单旋
							nodeG->_col = RED, nodeP->_col = BLACK;
						}
						else//左右双旋
						{
							RotateL(nodeP);
							RotateR(nodeG);
							nodeC->_col = BLACK, nodeG->_col = RED;
						}
					}
					else
					{
						if (nodeC == nodeP->_right)
						{
							RotateL(nodeG);//左单旋
							nodeP->_col = BLACK, nodeG->_col = RED;
						}
						else//右左双旋
						{
							RotateR(nodeP);
							RotateL(nodeG);
							nodeC->_col = BLACK, nodeG->_col = RED;
						}
					}

					break;
				}
			}

			cout << "该key-value插入成功" << endl;
			return true;
		}



		bool Check()
		{
			if (_root->_col == RED)//根节点为红
				return false;

			return _Check1(_root) && _Check2(_root, num_of_black(_root), 0);
		}


		void InOrder()
		{
			_InOrder(_root);
		}

	private:

		bool _Check1(Node* cur)//是否有连续红节点
		{
			if (cur == nullptr)
				return true;

			if (cur->_col == RED)
			{
				if (cur->_parent->_col == RED)
					return false;
			}

			return _Check1(cur->_left) && _Check1(cur->_right);
		}

		int num_of_black(Node* root)//计算一条路径黑色节点数量
		{
			if (root == nullptr)
				return 0;
			return (root->_col == BLACK ? 1 : 0) + num_of_black(root->_left);
		}

		bool _Check2(Node* cur, int num, int count)
		{
			if (cur == nullptr)
			{
				if (count == num)
					return true;
				return false;
			}

			if (cur->_col == BLACK)
				count++;
			
			return _Check2(cur->_left, num, count) && _Check2(cur->_right, num, count);
		}

		void _InOrder(Node* cur)
		{
			if (cur == nullptr)
				return;
			_InOrder(cur->_left);
			cout << cur->_val.first << " : " << cur->_val.second << endl;
			_InOrder(cur->_right);
		}

		
	private:
		Node* _root = nullptr;
	};


}

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

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

相关文章

【设计模式】(万字总结)深入理解Java中的创建型设计模式

1. 前言 在软件开发的世界里&#xff0c;设计模式是一种被广泛接受并应用的解决方案。它们不仅仅是代码的设计&#xff0c;更是对问题的思考和解决的方法论。在Java开发中&#xff0c;特别是在面向对象的编程中&#xff0c;设计模式尤为重要。创建型设计模式&#xff0c;作为设…

JavaScript安全编程宝典【万字详解】

文章目录 简介基本说明特点两种使用方式在script中写使用script标签引入JS文件 数据类型介绍特殊值 运算符算数运算符赋值运算符逻辑运算符&#xff1a;条件运算符 数组的定义基本使用数组的遍历 函数含义函数定义方式基本语法代码示例 细节和注意事项 自定义对象Object形式{} …

解决mysql数据库表读取中文乱码问题

本文目录 0、省流1、问题出现2、问题排查3、结论 0、省流 在服务器上创建数据库服务时&#xff0c;使用的sql脚本加载的数据库表&#xff0c;其中脚本中有一些预设的测试数据包含中文汉字&#xff0c;由于linxu服务器控制台默认编码是lantin1&#xff0c;导致中文通过该编码方…

鸿蒙HarmonyOS开发:多种内置弹窗及自定义弹窗的详细使用指南

文章目录 一、消息提示框&#xff08;showToast&#xff09;1、导入模块2、语法3、参数4、示例5、效果 二、对话框&#xff08;showDialog&#xff09;1、导入模块2、语法3、参数4、示例5、效果 三、警告弹窗&#xff08;AlertDialog&#xff09;1、语法2、参数3、AlertDialogP…

JDBC(Java访问数据库)

Java Database Connectivity&#xff1a;Java访问数据库的解决方案 JDBC定义了一套标准接口&#xff0c;即访问数据库的通用API&#xff0c; 不同的数据库厂商根据各自数据库的特点去实现这些接口。 JDBC希望用相同的方式访问不同的数据库&#xff0c;让具体的数据库操作与数…

科普文:科普文:springcloud之-Hystrix服务容错

Hystrix概念 Hystrix 服务容错保护 的概念和说明 这就是大名鼎鼎的&#xff1a;豪猪 豪猪的英文就是&#xff1a;Hystrix&#xff0c;国外一些大牛的程序员在给自己的架构起名字的时候&#xff0c;往往就这么特别。哪天咱们中国人自己也能写出些架构&#xff0c;咱们就按照中…

⚒linux通过shell脚本上传文件至minio中

&#x1f534;大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;雄雄的小课堂 前言 之前数据库是备份到了七牛云上了&#xff0c;但是眼看着数据库文件越来越大&#xff0c;七牛云里面的余额越来越少&#xff0c;所以&#xff0c;转移阵地。 家里的nas&…

昇思25天学习打卡营第20天|munger85

GAN图像生成 生成对抗网络中是为了让我们生成的东西向期望的那样&#xff0c;就是为了让生成的东西很像&#xff0c;真的&#xff0c;例如用它来画画。就是描述整个网络的逻辑和目的&#xff0c;它有两部分组成&#xff0c;一个是生成器&#xff0c;一个是辨别器。他希望的是辨…

C++程序编程中的 SetWindowLong 函数的几个常见用途(附源码)

目录 1、API函数SetWindowLong说明 2、修改窗口风格 3、给窗口指定新的窗口消息处理函数,以拦截窗口消息 4、可以给窗口设置关联的UserData数据,方便在窗口内部使用这些数据 5、64位程序中需要使用SetWindowLongPtr 6、最后 C++软件异常排查从入门到精通系列教程(专栏…

Sping项目只能勾选17和21 (已解决) 导致的后续Invalid bound statement (not found):

问题发现 今天创建项目的时候发现 idea初始化spring的时候选择不了Java8 解决方案:替换URL为 https://start.aliyun.com/ 将IDEA页面创建Spring项目&#xff0c;其实是访问spring initializr去创建项目。故我们可以通过阿里云国服去间接创建Spring项目。 将https://start.spr…

TortoiseSVN安装使用教程(超详细)

目录 前言1. 下载2. 安装2.1 安装TortoiseSVN&#xff08;看图操作&#xff09;2.2 安装语言包&#xff08;看图操作&#xff09; 3. 使用3.1 版本库浏览器3.2 其他教程 前言 TortoiseSVN&#xff1a;&#xff08;俗称小乌龟&#xff09;Subversion版本控制系统的一个免费开源客…

Pytorch基础:Tensor的view方法(非连续张量也可以使用view)

相关阅读 Pytorch基础https://blog.csdn.net/weixin_45791458/category_12457644.html?spm1001.2014.3001.5482 在Pytorch中&#xff0c;view是Tensor的一个重要方法&#xff0c;用于返回一个改变了形状&#xff0c;但数据和数据的顺序与原来一致的新张量&#xff0c;但是新张…

夸克Android一面凉经(2024)

夸克Android一面凉经(2024) 笔者作为一名双非二本毕业7年老Android, 最近面试了不少公司, 目前已告一段落, 整理一下各家的面试问题, 打算陆续发布出来, 供有缘人参考。今天给大家带来的是《夸克Android一面凉经(2024)》。 面试职位: 智能信息-客户端开发工程师-夸克小说 技术一…

20240728 每日AI必读资讯

Google Gemini 聊天机器人更新 可以免费使用Gemini 1.5 Flash 1. 引入Gemini 1.5 Flash模型&#xff1a; • 提供更快和更高质量的响应。 • 提升推理和图像理解能力。 • 上下文窗口扩大到 32Ktokens&#xff0c;允许进行更长的对话和处理更复杂的问题。 • 即将支持通过 Goo…

【你也能从零基础学会网站开发】 SQL结构化查询语言应用基础-- SQL Server数据库开发创建表之FOREIGN KEY外键约束完全详解最详细!

&#x1f680; 个人主页 极客小俊 ✍&#x1f3fb; 作者简介&#xff1a;程序猿、设计师、技术分享 &#x1f40b; 希望大家多多支持, 我们一起学习和进步&#xff01; &#x1f3c5; 欢迎评论 ❤️点赞&#x1f4ac;评论 &#x1f4c2;收藏 &#x1f4c2;加关注 浅谈FOREIGN K…

故障诊断 | CNN-LSSVM卷积神经网络结合最小二乘支持向量机故障诊断(Matlab)

效果一览 文章概述 故障诊断 | CNN-LSSVM卷积神经网络结合最小二乘支持向量机故障诊断(Matlab) 模型描述 使用CNN进行特征提取: 使用CNN模型对数据进行训练,通常是图像数据。 通过CNN的卷积层和池化层提取图像的特征。 将提取到的特征作为输入,可以是全连接层的输出或者卷…

系统架构师考点--系统架构设计(上)

大家好。今天我来总结一下系统架构设计相关的考点。这块考点是重中之重&#xff0c;每年上午场客观题占20-25分左右&#xff0c;下午案例题也会考到&#xff0c;下午论文也会考到。大家要好好学学这部分内容。 一、软件架构概述 软件架构是指从需求分析到软件设计之间的过渡过…

机器学习 | 分类算法原理——逻辑回归

Hi&#xff0c;大家好&#xff0c;我是半亩花海。接着上次的线性可分继续更新《白话机器学习的数学》这本书的学习笔记&#xff0c;在此分享逻辑回归这一分类算法原理。本章的分类算法原理基于《基于图像大小进行分类》项目&#xff0c;欢迎大家交流学习&#xff01; 目录 一、…

【51CTO-注册安全分析报告】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 暴力破解密码&#xff0c;造成用户信息泄露短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造成亏损无底洞…

vim的使用及退出码(return 0)

linux基础之vim快速入门 linux基础之vim快速入门_基本linux vim-CSDN博客https://blog.csdn.net/ypxcan/article/details/119878137?ops_request_misc&request_id&biz_id102&utm_termvim%E7%BC%96%E8%BE%91%E5%99%A8%E5%A4%8D%E5%88%B6%E7%B2%98%E8%B4%B4%E4%BA%…