对红黑树的理解与实现(C++实现)

认识红黑树

在看到此篇文章之前最好还是先了解一下左右旋也就是AVL树的插入数据该如何处理。AVL树的插入详解-CSDN博客

红黑树,也属于是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是红色(red)或黑色(black)。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是接近平衡的。相较于AVL树而言红黑树就没有那么繁琐,也就是旋转的频率没有这么高,但是依旧可以保证查找数据和插入删除数据的效率在O(log2n)的时间复杂度。归于红黑树的特征限制:

  1. 每个节点要么是红色,要么是黑色。
  2. 根节点是黑色。
  3. 叶子节点(NIL节点)是黑色的。(此叶子结点指的是空节点)
  4. 如果一个节点是红色的,则它的两个子节点都是黑色的。(不能有连续的两个红节点)
  5. 对于每个节点,从该节点到其后代叶子节点的所有路径上,包含相同数量的黑色节点。

为了保证以上的特征使得我们的红黑树确保没有一条路径会比其他路径长出两倍。由于每条路径(从根节点到空节点才是一条路径)的黑色节点数目都相同,所以我们可以假设每条路径的黑色节点个数为N,而且不能出现连续的黑节点,且根节点为黑,所以从根到叶子最长的情况就是黑红黑红...所以最长节点数也就是2N,因此可以轻松得出:每条路径的节点数目在范围:[N,2N]之间。

红黑树和AVL树

红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O(log_2 N),红黑树不追求绝对平衡,红黑树主要采用变色和旋转而达到满足红黑树的条件,其只需保证最长路径不超过最短路径的2倍。

而AVL树的要求更为严格,要保证任何节点的左右子树高度差为1,所以一旦高度差为2时就要开始进行旋转。

相对而言,红黑树降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,但是可能相对没有AVL树查找数据时那么高效但是此高效相较于不断地旋转而言也可以忽略,所以实际运用中红黑树更多。

红黑树的定义

enum colar//枚举类型
{
	red,
	black
};

template<class K, class V>
struct RBTreeNode
{
	RBTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _co(red)//
	{}
    //三叉链
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;

	pair<K, V> _kv;//实际数据
	colar _co;//颜色
	
};

数据插入 

首先我们要知道插入数据是按照搜索树的方式进行插入的,但是对于二叉树我们关键的是颜色处理,所以我们对于新的节点初始化就要考虑是红还是黑,这就要考虑红黑树的性质了,我们对于要插入数据之前的红黑树肯定是满足性质的,也就是每条路径的黑色节点个数相同,但是对于新插入的节点如果初始为黑色的话,那么肯定是直接打乱了整个红黑树的结构,会导致每条路径的黑色节点个数不同,所以我们采用插入节点都按照初始为红色节点的方式进行插入,所以此时就得判断新插入的节点对应的父节点是红还是黑,如果是黑节点则表明该插入没毛病,但是如是红结点的话就需要进行处理。

    bool insert(const pair<K, V>& kv)
	{
		Node* newroot = new Node(kv);//默认为红
		if (_root == nullptr)
		{
			_root = newroot;
			return true;
		}
		Node* parent = _root, * cur = _root;
		//插入数据
		while (1)
		{
			parent = cur;
			if (cur->_kv.first > kv.first)
			{
				cur = cur->_left;
				if (cur == nullptr)
				{
					parent->_left = newroot;
					newroot->_parent = parent;
					break;
				}
			}
			else if (cur->_kv.first < kv.first)
			{
				cur = cur->_right;
				if (cur == nullptr)
				{
					parent->_right = newroot;
					newroot->_parent = parent;
					break;
				}
			}
			else
			{
				return false;
			}

		}
        //判断父节点
        ……
    }

插入节点的父节点为黑

我们知道当父亲节点是黑色的话,而插入节点默认初始化都是红色,所以此次插入既不会影响路径的黑色节点数目,也不会出现连续的红节点,所以此时直接插入就可以。

插入节点的父节点为红

此时我们就要分析一下,当我们所插入节点的父亲也是红色的话,且我们知道插入前该树肯定是符合红黑树的性质的,那么此时我们插入节点的祖父节点肯定存在并且是黑色的(因为父亲节点是红色,且不会有连续的红色节点,而且根节点为黑)而此时取决定性因素的节点其实就是叔叔节点(也就是祖父节点的另一个子节点)


叔叔节点为红

当我们插入的节点的叔叔节点为红色节点的话,此时的处理就比较轻松。1.先将父亲节点置为黑,同时也将叔叔节点置为黑,2.将组父节点置为红,3.将当前节点指向祖父节点再次循环判断。

此时我们要了解为什么第二步要将组父节点置为红??其实主要就是因为红黑树要保证每条路径的黑色节点数目不变,所以在插入数据前后,我们的黑色节点个数不能发生改变。因此要将组父节点置为红,但是祖父节点的父亲是否为黑色节点或者红色节点,所以此时属于循环判断,也就是继续向上判断调整。


   叔叔节点为黑

这种情况相较于上一种的区别就是叔叔节点为黑的,但是你是否会疑惑为什么会有这种情况,这种情况下会导致每条路径不满足有相同个数的黑节点,但是这种情况虽然在刚插入数据的时候不可能发生,但是在插入数据之后就极有可能发生该种情况:如下

此时如果再像第一种情况一样只采用变色处理的话是远远不够的,因为,无论怎样变都无法保证插入节点前与变色处理后的每条路径黑色节点数目相等。所以此时根据AVL树插入数据的方式就不难发现可以采用旋转的方式进行节点的颜色处理。

此时显然采用单旋就能够轻松解决问题,而此时得结构特征是pparent->_left == parent && parent->_left == cur也就是我们所了解到的直线型,所以此时采用单旋并且将新的根节点设为黑,两个子节点设为红。此时与插入节点前的路径黑节点数保持一致,显然是完全没毛病的。但是貌似也可以将根设为红,两个子节点设为黑,但是此时无疑就复杂了很多,那么就还得继续向上判断调整,而且如果当新的根就是整个数的根的话,最后还得再进行调整回来。


有了单旋的情况自然就少不了双旋(折线型):

双旋其实就是两次单旋,所以说理解了单旋,双旋自然也就直接调用即可,而此时的节点颜色变化发生了一些改变,在第一次进行左单旋时,显然并没有反生节点颜色变化,依旧还是两个连续的红节点,但是在旋转过后,直接就变成了右单旋的情况,此时cur节点变成了新的根节点(变黑),而pparent变成了其中一个子节点(变红)

左右单旋代码

	void RotateL(Node* parent)//左单旋
	{
		Node* cur = parent->_right;
		Node* curl = cur->_left;
		Node* pparent = parent->_parent;//提前记录

		parent->_right = curl;
		if (curl)
		{
			curl->_parent = parent;
		}
		cur->_left = parent;
		parent->_parent = cur;

		//处理pparent与parent的连接
		if (_root == parent)
			_root = cur;
		else
		{
			if (pparent->_left == parent)
				pparent->_left = cur;
			else
				pparent->_right = cur;
			cur->_parent = pparent;
		}

	}
	void RotateR(Node* parent)//右单旋
	{
		{
			Node* cur = parent->_left;
			Node* curr = cur->_right;
			Node* pparent = parent->_parent;//提前记录

			parent->_left = curr;
			if (curr)
			{
				curr->_parent = parent;
			}
			cur->_right = parent;
			parent->_parent = cur;

			//处理pparent与parent的连接
			if (_root == parent)
				_root = cur;
			else
			{
				if (pparent->_left == parent)
					pparent->_left = cur;
				else
					pparent->_right = cur;
				cur->_parent = pparent;
			}

		}
	}

插入数据的全部代码

	bool insert(const pair<K, V>& kv)
	{
		Node* newroot = new Node(kv);//默认为红
        //为空就直接插入
		if (_root == nullptr)
		{
			_root = newroot;
			_root->_co = black;//设为黑
			return true;
		}

		Node* parent = _root, * cur = _root;
		//插入数据(搜索树的插入)
		while (1)
		{
			parent = cur;
			if (cur->_kv.first > kv.first)
			{
				cur = cur->_left;
				if (cur == nullptr)
				{
					parent->_left = newroot;
					newroot->_parent = parent;
					break;
				}
			}
			else if (cur->_kv.first < kv.first)
			{
				cur = cur->_right;
				if (cur == nullptr)
				{
					parent->_right = newroot;
					newroot->_parent = parent;
					break;
				}
			}
			else
			{
				return false;
			}

		}

		//父节点的判断(为黑1直接插入,为红还需调整)
		cur = newroot;//当前节点就是新插入的节点

		while (parent && parent->_co == red)//父亲节点可能不存在(防止空指针的解引用)
		{
			Node* pparent = parent->_parent;//parent为红,不可能是根,一定存在pparent节点
			Node* uncle = nullptr;
			//找叔叔节点(决定着连续的两个红色节点是该变色还是旋转)
			if(pparent->_right==parent)
				uncle = parent->_parent->_left;
			else
				uncle = parent->_parent->_right;

			if (uncle&&uncle->_co == red)//叔叔存在且为红
			{
				//变色
				parent->_co = uncle->_co = black;
				pparent->_co = red;//祖父节点有可能是根节点
				//继续向上更新处理
				cur = pparent;
				parent = cur->_parent;
			}
			else//叔叔节点不存在或为黑色
			{
				//旋转
				if (pparent->_left == parent && parent->_left == cur)
				{
					//右单旋
					RotateR(pparent);
					parent->_co = black;
					pparent->_co = red;
				}
				else if (pparent->_right == parent && parent->_right == cur)
				{
					//左单旋
					RotateL(pparent);
					parent->_co = black;
					pparent->_co = red;
				}
				else if (pparent->_right == parent && parent->_left == cur)
				{
					//右左双旋
					RotateR(parent);
					RotateL(pparent);
					cur->_co = black;
					pparent->_co = red;
				}
				else if (pparent->_left == parent && parent->_right == cur)
				{
					//左右双旋
					RotateL(parent);
					RotateR(pparent);
					cur->_co = black;
					pparent->_co = red;
				}
				break;//旋转之后新的根节点都是黑色,所以直接不用再向上判断调整了
			}
	
		}

		_root->_co = black;//循环体内(叔叔为黑节点时)很有可能将根节点改为红
		return true;
	}

 

平衡判断

我们判断我们插入的树是否为红黑树,主要就是取决于是否满足红黑树的所有性质:

  1. 每个节点要么是红色,要么是黑色。
  2. 根节点是黑色。
  3. 叶子节点(NIL节点)是黑色的。(此叶子结点指的是空节点)
  4. 如果一个节点是红色的,则它的两个子节点都是黑色的。(不能有连续的两个红节点)
  5. 对于每个节点,从该节点到其后代叶子节点的所有路径上,包含相同数量的黑色节点。

对于以上的性质,主要就是判断4和5两个性质,所以我们要实现一个函数判断该树是否有连续的两个红节点,而且每条路径的黑节点个数是否相等。

	// 根节点->当前节点这条路径的黑色节点的数量
	bool Check(Node* cur,int blacknum,int ref_val)
	{
		if (cur == nullptr)
		{
			if (blacknum == ref_val)
				return true;
			cout << "每条路径的黑色节点个数不同" << endl;
			return false;
		}
		Node* parent = cur->_parent;
		if (cur->_co == red && parent->_co == red)//向上判断,向下判断的节点可能为空或其它的。
			return false;
		if (cur->_co == black)
			blacknum++;

		return Check(cur->_left,blacknum,ref_val) && Check(cur->_right,blacknum,ref_val);
	}
	bool Is_balance()
	{
		if (_root->_co == red)
			return false;
		if (_root == nullptr)
			return true;

		//不能出现连续红节点
		//每条路径黑色节点要保证相同
		int blacknum = 0;//必须传值,相当于是每个节点都有一个变量表示从根到当前的黑节点个数
		int ref_val = 0;//参考值,求出任意一条路径中黑色节点数目
		Node* cur = _root;
		while (cur)
		{
			if (cur->_co == black)
				ref_val++;
			cur = cur->_left;
		}
		return Check(_root,blacknum,ref_val);
	}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于ssm+vue协同过滤算法的电影推荐系统

基于ssmvue协同过滤算法的电影推荐系统 摘要 电影推荐系统在信息技术发展的背景下日益成为研究的焦点&#xff0c;本研究基于SSM&#xff08;Spring SpringMVC MyBatis&#xff09;框架与Vue.js技术&#xff0c;以协同过滤算法为核心&#xff0c;旨在构建一种高效、准确的电影…

第28章_mysql缓存策略

文章目录 MySQL缓存方案目的分析缓存层作用举例 缓存方案选择场景分析 提升MySQL访问性能的方式MySQL主从复制读写分离连接池异步连接 缓存方案缓存和MySQL一致性状态分析制定读写策略 同步方案canalgo-mysql-transfer 缓存方案的故障问题及解决缓存穿透缓存击穿缓存雪崩缓存方…

nodejs express vue uniapp电影购票系统源码

开发技术&#xff1a; node.js&#xff0c;vscode&#xff0c;HBuilder X express vue elementui uniapp 功能介绍&#xff1a; 用户端&#xff1a; 登录注册 首页显示搜索电影&#xff0c;轮播图&#xff0c;电影分类&#xff0c;最近上架电影 点击电影进入电影详情&am…

MySQL(15):存储过程与函数

存储过程概述 含义&#xff1a; 存储过程的英文是 Stored Procedure 。它的思想很简单&#xff0c;就是一组经过 预先编译 的 SQL 语句的封装。 执行过程&#xff1a; 存储过程预先存储在 MySQL 服务器上&#xff0c;需要执行的时候&#xff0c;客户端只需要向服务器端发出调用…

Obsidian同步技巧

Obsidian介绍 Obsidian支持Markdown语法&#xff0c;所见即所得。 软件支持多仓库功能&#xff0c;支持笔记文件夹和分层文件夹&#xff0c;等功能。 值得一提的是&#xff0c;软件的笔记同步功能需要付费。 同步技巧 官方同步方法 若资金充足&#xff0c;则可在Obsidian官网…

Django(四、路由层)

文章目录 一、路由层1.路由匹配url方法第一个是参数 的正则表达式 二、正则无名分组与有名分组无名分组有名分组 三、反向解析1.概念无名分组动态路由解析有名分组动态路由解析 四、路由分发为什么要用路由分发&#xff1f; 1.总路由分发配置名称空间 五、伪静态的概念六、虚拟…

使用Jmeter进行http接口性能测试

在进行网页或应用程序后台接口开发时&#xff0c;一般要及时测试开发的接口能否正确接收和返回数据&#xff0c;对于单次测试&#xff0c;Postman插件是个不错的Http请求模拟工具。 但是Postman只能模拟单客户端的单次请求&#xff0c;而对于模拟多用户并发等性能测试&#xf…

【Java】集合(二)Set

1.Set接口基本介绍 无序:存取顺序不一致不重复:可以去除重复无索引:没有带索引的方法&#xff0c;所以不能使用普通for循环遍历&#xff0c;也不能通过索引来获取元素 2.Set集合的实现类 HashSet:无序、不重复、无索引LinkedHashSet: 有序、不重复、无索引TreeSet: 可排序、不…

二维码智慧门牌管理系统升级解决方案:数据可视化助力运营精准决策

文章目录 前言一、升级版二维码智慧门牌管理系统的特点二、数据可视化助力运营精准决策 前言 随着科技的不断进步&#xff0c;传统的门牌管理系统已经无法满足现代社会的需求。为了提高管理效率&#xff0c;减少人力成本&#xff0c;我们引入了升级版的二维码智慧门牌管理系统…

同城服务如何引流和推广 同城小程序制作

客观原因线下实体店经营变得很艰难&#xff0c;而抖音推出的同城号功能&#xff0c;为许多商家带来了新的生机。抖音同城号的操作很简单&#xff0c;只需在短视频发布时打开同城号&#xff0c;短视频将被投入到同城流量池中&#xff0c;可以让位置附近的用户看到&#xff0c;线…

AttributeError: module ‘matplotlib‘ has no attribute ‘get_data_path‘

【报错】使用 AutoDL 下 Notebook 调用 matplotlib 时遇到 AttributeError: module matplotlib has no attribute get_data_path 报错&#xff1a; --------------------------------------------------------------------------- AttributeError …

C语言ZZULIOJ1148:组合三位数之一

题目描述 把1、2、3、4、5、6、7、8、9组合成3个3位数&#xff0c;要求每个数字仅使用一次&#xff0c;使每个3位数均为完全平方数。按从小到大的顺序输出这三个三位数。 输入:无 输出:按从小到大的顺序输出这三个三位数&#xff0c;由空格隔开。输出占一行。 提示 若一个数能表…

No192.精选前端面试题,享受每天的挑战和学习

🤍 前端开发工程师(主业)、技术博主(副业)、已过CET6 🍨 阿珊和她的猫_CSDN个人主页 🕠 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 🍚 蓝桥云课签约作者、已在蓝桥云课上架的前后端实战课程《Vue.js 和 Egg.js 开发企业级健康管理项目》、《带你从入…

2017年计网408

第33题 假设 OSI 参考模型的应用层欲发送 400B 的数据 (无拆分), 除物理层和应用层之外, 其他各层在封装 PDU 时均引入 20 B 的额外开销, 则应用层数据传输效率约为( )A. 80%B. 83%C. 87%D. 91% 本题考察有关数据包逐层封装的相关概念。我们来一起分析一下。 这是要求大家必须…

Egg.js 中 Service 的使用

Service 服务 Service是用来编写和数据库直接交互的业务逻辑代码。Service就是在复杂业务场景下用于做业务逻辑封装的一个抽象层。 简单来说&#xff0c;就是把业务逻辑代码进一步细化和分类&#xff0c;所以和数据库交互的代码都放到Service中。这样作有三个明显的好处。 保…

【leetcode】8.字符串转换整数

题目 请你来实现一个 myAtoi(string s) 函数&#xff0c;使其能将字符串转换成一个 32 位有符号整数&#xff08;类似 C/C 中的 atoi 函数&#xff09;。 函数 myAtoi(string s) 的算法如下&#xff1a; 读入字符串并丢弃无用的前导空格 检查下一个字符&#xff08;假设还未…

同城小程序怎么运作 本地化生活小程序开发

同城小程序可以采取公域加私域的运营方式&#xff0c;进行运作。 在社交媒体平台上分享有趣的本地生活内容、社区动态&#xff0c;可以通过举办本地活动、合作推广等方式进行线下宣传&#xff0c;可以通过抖音本地化生活服务进行线下门店推广。 本地化生活小程序开发需要结合自…

基于RK3568新零售智能售货柜解决方案

I 方案简介 新零售智能售货柜解决方案&#xff1a; 无人零售除了无人货架外&#xff0c;自动售货机仍是亮点。但仍有很多人认为自动售货机已经过时&#xff0c;不会成为新零售领域的新星。 随着手机支付、人脸支付不断普及&#xff0c;智能售卖不断的推陈出新&#xff0c;无人…

Netty入门指南之Reactor模型

作者简介&#xff1a;☕️大家好&#xff0c;我是Aomsir&#xff0c;一个爱折腾的开发者&#xff01; 个人主页&#xff1a;Aomsir_Spring5应用专栏,Netty应用专栏,RPC应用专栏-CSDN博客 当前专栏&#xff1a;Netty应用专栏_Aomsir的博客-CSDN博客 文章目录 参考文献前言单线程…

基因检测技术的发展与创新:安全文件数据传输的重要作用

基因是生命的密码&#xff0c;它决定了我们的身体特征、健康状况、疾病风险等。随着基因检测技术的高速发展&#xff0c;我们可以通过对基因进行测序、分析和解读&#xff0c;更深入地认识自己&#xff0c;预防和治疗各种遗传性疾病&#xff0c;甚至实现个性化医疗和精准健康管…