C++进阶 | [4.3] 红黑树

摘要:什么是红黑树,模拟实现红黑树

红黑树 ,是一种 二叉搜索树 ,但 在每个结点上增加一个存储位表示结点的颜色,可以是  Red Black 。 通过对 任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路 径会比其他路径长出俩倍 ,因而是 接近平衡 的。

1. 红黑树的性质

注意:红黑树是一种搜索二叉树。性质如下:

① 红黑树的根节点为 Black ;

Red 节点不连续,即 Red 节点一定有 Black 的孩子节点;

③ 每条路径含有相同数量的 Black 节点;(关于这点下文会详细解释)

④ 每个节点要么是 Black 的要么是 Red 的;

nullptr 节点为 Black(在红黑树中这样的节点被称为NIL节点,这样是为了方便找准路径,非硬性要求)

  • 路径 - path从根节点到空,称为一条路径。
    最短路径:如果一条路径的所有节点均为 Black ,则该路径为最短路径;
    最长路径: Black Red Black Red Black Red Black ……Red NIL 这样总是黑红节点相间的路径为最长路径。(注意:路径的结尾为空节点,而空节点一定是 Black 的,又因为红黑树的每条路径含有数量相同的 Black 节点,因此可得最长路径=最短路径×2-1

如图所示,path1和path2为该红黑树的最短路径;path8为该红黑树的最长路径。红黑树中的叶节点为特殊的叶节点,即 nullptr 节点,在此图中写作“NIL”。
与AVL树比较,AVL树是更接近满二叉树的严格平衡,而红黑树是近似平衡。


2. 模拟实现红黑树

1)框架

同样的,先定义红黑树的节点。根据红黑树对节点的“颜色”要求,这里采用枚举(enum)类型来表达节点的“颜色”。示例代码如下。

ps.默认新创建的节点为红色(下文会解释这样做的原因)。

enum Colour
{
	RED, BLACK
};

template<class T>
struct RBTreeNode
{
	RBTreeNode(T data = T())
		:_data(data)
		, _pLeft(nullptr)
		, _pRight(nullptr)
		, _pParent(nullptr)
		, _col(RED)//默认新创建的节点都是红色的
	{}
	T _data;
	RBTreeNode<T>* _pLeft;
	RBTreeNode<T>* _pRight;
	RBTreeNode<T>* _pParent;
	Colour _col;
};

// 模拟实现红黑树的插入--注意:为了后序封装map和set,在实现时给红黑树多增加了一个头结点
template<class T>
class RBTree
{
	typedef RBTreeNode<T> Node;
public:
	RBTree()
	{
		_pHead = new Node;
		_pHead->_pLeft = _pHead;
		_pHead->_pRight = _pHead;
	}

private:
	Node* _pHead;
};

2)insert

红黑树的插入函数实现的思路同AVL树是类似的。

-🟡Part1.插入新节点-   红黑树首先是一个BST,先按BST的规则插入新节点。代码如下。注意:当我们插入新的节点时,总是默认新插入的节点为红色。理由是:如果在红黑树中插入黑色节点将对整棵红黑树造成影响,因为红黑树要求每条路径上含有数量相同的黑色节点,一旦插入黑色节点必然要应对红黑树的调整,而插入红色节点,若该新节点的parent节点不为红色节点则不用调整这棵红黑树。

// 在红黑树中插入值为data的节点,插入成功返回true,否则返回false.注意:为了简单起见,本次实现红黑树不存储重复性元素
bool Insert(const T& data)
{
	if (_pHead->_pLeft == _pHead && _pHead->_pRight == _pHead)
	{
		Node* _pNewNode = new Node(data);
		_pHead->_pLeft = _pHead->_pRight = _pNewNode;
		_pNewNode->_pParent = _pHead;
		_pNewNode->_col = BLACK;//跟节点为黑色
		return true;
	}

	Node* pCur = _pHead->_pLeft;
	Node* pParent = pCur->_pParent;
	while (pCur)
	{
		pParent = pCur;
		if (pCur->_data > data)
		{
			pCur = pCur->_pLeft;
		}
		else if (pCur->_data < data)
		{
			pCur = pCur->_pRight;
		}
		else
		{
			return false;
		}
	}
	Node* _pNewNode = new Node(data);
	if (pParent->_data > data)
		pParent->_pLeft = _pNewNode;
	else
		pParent->_pRight = _pNewNode;
	_pNewNode->_pParent = pParent;

	//……
	
	return true;
}

-🟡Part2.调整红黑树-  由于插入的新节点默认为红色,因此当插入导致出现连续的红色节点时,需要调整红黑树。 

如上图。下一步,继续对圈出的subtree进行展开:👇

  • 情况一:当圈出的subtree的根节点 psibling 为红色,这种情况不需要旋转,直接调整节点颜色即可。从下图的换色中不难看出,这样换色之后,每个路径的黑色节点数量不受影响。注意:当 pGrandparent 变为红色后,可能会与 其parent 形成连续的红色节点,因此需要继续向上操作,即令 pCur = pGrandparent(赋值操作),继续判断,并在必要的情况下调整。(这个“向上的”思路和AVL树的调整是类似的)
    ps.pParent 不一定是 pGrandparent 的节点,pCur 不一定就是 pParent 的节点。这里之所以分析下图中的这一种情况是因为,这类情况的调整不需要旋转,即直接调整节点颜色——pParent和psibling都变红,因此这两个节点相对于pGrandparent的左右位置不重要,pCur不做改变,因此pCur相对于pParent的左右位置也不重要。但需要旋转调整的情况就不一样了,pParent 、pGrandparent 、pCur 这三个节点之间的关系决定了旋转的方向,下面“情况二”会详细讲解。

  • 情况二:当圈出的subtree根节点为黑色,这种情况需要旋转+调色,下面对这种情况继续细分。👇
    ps.这棵被圈出的 subtree 黑色节点数目为n,在情况二这种情况下,psibing已经是一个黑色节点了,因此其子树黑色节点为n-1,如图中所写。(n≥1) 另外,注意 psibing 可能为NIL,即黑色的空节点。

    如下图,第三列表示调色后的结果,调色思路可以归纳为——旋转后的树的新根变为黑色,旋转前的树的旧根变为红色。

示例代码:

{//RBTree
	// 在红黑树中插入值为data的节点,插入成功返回true,否则返回false.注意:为了简单起见,本次实现红黑树不存储重复性元素
	bool Insert(const T& data)
	{
		//前面插入新节点的部分省略

		///新插入的节点默认是红色的,因此若在这条含有新插入节点的路径中出现·连·续·红·色·节点则需要调整///
		pCur = _pNewNode;//以新插入的结点为起点向上调整
		while (pCur->_col == RED && pParent->_col == RED)
		{
			Node* pGrandparent = pParent->_pParent;
			Node* pP_sibling = pGrandparent->_pLeft == pParent ? pGrandparent->_pRight : pGrandparent->_pLeft;
			if (pP_sibling && pP_sibling->_col == RED)//调色
			{
				pParent->_col = pP_sibling->_col = BLACK;
				if (pGrandparent != _pHead->_pLeft)
				{
					pGrandparent->_col = RED;
					pCur = pGrandparent;
					pParent = pCur->_pParent;
				}
			}
			else//旋转+调色
			{
				if (pCur == pParent->_pLeft)
				{
					if (pParent == pGrandparent->_pLeft)//右旋
					{
						RotateR(pGrandparent);
						pParent->_col = BLACK;
					}
					else//右左双旋
					{
						RotateR(pParent);
						RotateL(pGrandparent);
						//双旋后pCur成为新的subroot
						pCur->_col = BLACK;
					}
					pGrandparent->_col = RED;
				}
				else if (pCur == pParent->_pRight)
				{
					if (pParent == pGrandparent->_pRight)//左旋
					{
						RotateL(pGrandparent);
						//左旋后pParent成为新的subroot
						pParent->_col = BLACK;
					}
					else//左右双旋
					{
						RotateL(pParent);
						RotateR(pGrandparent);
						pCur->_col = BLACK;
					}
					pGrandparent->_col = RED;
				}
			}
		}
		return true;
	}

private:
	// 左单旋
	void RotateL(Node* subroot)
	{
		Node* pGrandparent = subroot->_pParent;
		Node* subR = subroot->_pRight;
		Node* subR_L = subR->_pLeft;

		if (pGrandparent == _pHead)
		{
			pGrandparent->_pLeft = pGrandparent->_pRight = subR;
		}
		else
		{
			if (subroot == pGrandparent->_pLeft)
			{
				pGrandparent->_pLeft = subR;
			}
			else
				pGrandparent->_pRight = subR;
		}
		subR->_pParent = pGrandparent;

		subR->_pLeft = subroot;
		subroot->_pParent = subR;

		subroot->_pRight = subR_L;
		if (subR_L)
			subR_L->_pParent = subroot;
	}
	// 右单旋
	void RotateR(Node* subroot)
	{
		Node* pGrandparent = subroot->_pParent;
		Node* subL = subroot->_pLeft;
		Node* subL_R = subL->_pRight;

		if (pGrandparent == _pHead)
		{
			pGrandparent->_pLeft = pGrandparent->_pRight = subL;
		}
		else
		{
			if (subroot == pGrandparent->_pLeft)
			{
				pGrandparent->_pLeft = subL;
			}
			else
				pGrandparent->_pRight = subL;
		}
		subL->_pParent = pGrandparent;

		subroot->_pLeft = subL_R;
		if (subL_R)
			subL_R->_pParent = subroot;

		subL->_pRight = subroot;
		subroot->_pParent = subL;
	}
};

ps.psibling 这个节点是有可能为空结点的,写旋转函数的时候要注意!

3)判断红黑树的合法性

要判断红黑树是否合法就需要比对着红黑树的性质一条一条来确认:

① 红黑树的根节点为 Black ;(⭕需要去判断树的根节点的颜色

Red 节点不连续,即 Red 节点一定有 Black 的孩子节点;(⭕需要验证

③ 每条路径含有相同数量的 Black 节点;(⭕需要验证

④ 每个节点要么是 Black 的要么是 Red 的;(✅通过enum类型来确保节点黑或红且无其他颜色可能

nullptr 节点为 Black。(✅这条显然不必验证

思路:选择一条路径并的到这条路径的黑色节点数量,并这条路径的黑色节点数量为标准,判断其他任以路径的黑色节点数量是否与其相等。简单起见,选择最左路径的黑色节点数量为标准数量去比对别的路径的黑色节点数量。

由上,关键问题在于如何计算出所有路径(除选择的标准路径)的外的黑色节点。以下图为例进行分析。

代码示例: 

// 检测红黑树是否为有效的红黑树
bool IsValidRBTRee()
{
	Node* pCur = _pHead->_pLeft;
	if (pCur == nullptr)
		return true;
	if (pCur->_col == RED)
		return false;
	size_t blackNUM = 0;
	while (pCur)
	{
		if (pCur->_col == BLACK)
			++blackNUM;
		pCur = pCur->_pLeft;
	}
	++blackNUM;//算上最后的空节点
	size_t pathblack = 0;
	return _IsValidRBTRee(_pHead->_pLeft, blackNUM, pathblack);
}

bool _IsValidRBTRee(Node* pRoot, size_t blackCount, size_t pathBlack)
{
	if (pRoot == nullptr)
	{
		++pathBlack;//算上最后的空节点
		if (blackCount == pathBlack)
			return true;
		else
			return false;
	}

	if (pRoot->_col == BLACK)
	{
		++pathBlack;
		return _IsValidRBTRee(pRoot->_pLeft, blackCount, pathBlack)
			&& _IsValidRBTRee(pRoot->_pRight, blackCount, pathBlack);
	}
	else
	{
		if (
			(pRoot->_pLeft && pRoot->_pLeft->_col == RED)
			|| (pRoot->_pRight && pRoot->_pRight->_col == RED)
			)
		{
			cout << "错误:存在连续的红色节点" << "->" << pRoot->_data << "->该节点附近存在连续红色节点" << endl;
			return false;
		}
		else
		{
			return _IsValidRBTRee(pRoot->_pLeft, blackCount, pathBlack)
				&& _IsValidRBTRee(pRoot->_pRight, blackCount, pathBlack);
		}
	}
}

END

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

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

相关文章

Web端登录页和注册页源码

前言&#xff1a;登录页面是前端开发中最常见的页面&#xff0c;下面是登录页面效果图和源代码&#xff0c;CV大法直接拿走。 1、登录页面 源代码&#xff1a; <!DOCTYPE html> <html><head><meta charset"utf-8"><title>登录</ti…

超详细的 C++中的封装继承和多态的知识总结<2.多态>

引言 小伙伴们我们都知道了&#xff0c;什么是封装和继承&#xff0c;在有了这个的基础上我们接着来看什么是多态。多态从字面上意思我们就可以知道&#xff0c;大概就是一个函数的不同形态&#xff0c;而且&#xff0c;前边我们在学习函数重载的时候我们已经简单的了解了如何用…

企业源代码加密软件丨透明加密技术是什么

在一个繁忙的软件开发公司中&#xff0c;两位员工小李和小张正在讨论源代码安全的问题。 “小张&#xff0c;你有没有想过我们的源代码如果被泄露了怎么办&#xff1f;”小李担忧地问。 “是啊&#xff0c;这是个大问题。源代码是我们的核心竞争力&#xff0c;一旦泄露&#…

最短路算法三

图论三 20240624 算法实用主义&#xff0c;用到再学 1. 大纲&#xff1a; a. 最小生成树都是无向图 难在建图&#xff0c;不考原理&#xff0c;重点记思路&#xff08;是骨头&#xff09;&#xff0c;自己复述一遍&#xff0c;不能死记代码 血肉 类似最短路 prim&#xff08;…

web基础以及http协议

web基础介绍 web&#xff1a;就是我们所说的网页&#xff0c;打开网站展示的页面。&#xff08;全球广域网&#xff0c;万维网&#xff09; world wide web &#xff08;www&#xff09; 分布式图形信息系统 分布式&#xff1a;计算机系统或者是应用程序分布在多台独立的计算…

探索智慧校园人事系统,了解人事合同功能的核心优势

智慧校园人事系统中的人事合同管理功能&#xff0c;是一个高度集成且自动化的模块&#xff0c;专注于优化合同的全生命周期管理&#xff0c;从合同创建、审批、签署到存档及续签提醒&#xff0c;旨在提升人事管理工作的规范性与效率&#xff0c;同时保障学校的法律合规性。 在智…

微信小程序-插槽slot

一.插槽slot 在页面使用自定义组件的时候&#xff0c;如果在自定义组件里面写子组件&#xff0c;子组件的内容无法显示。 <custom01> <text slotslot-top>你好&#xff0c;上方组件</text> 你好&#xff0c;组件 <text slotslot-bottom>你好&#xf…

数据结构 - C/C++ - 栈

目录 结构特性 结构实现 结构容器 结构设计 顺序栈 链式栈 结构特性 栈(stack)是线性表的一种形式&#xff0c;限定仅在表的一端进行插入或者删除的操作。 栈顶 - 表中允许插入、删除的一端称为栈顶(top)&#xff0c;栈顶位置是可以发生变化的。 插入 - 进栈、入栈、压栈…

蒂升电梯职业性格和Verify认知能力SHL测评答题攻略及薪资待遇解密!

​一、蒂升电梯职业性格和认知能力测评考什么 您好&#xff01;蒂升电梯公司邀请您参加的OPQ职业性格测评和Verify认知能力测评是两种常见的评估工具&#xff0c;用于帮助了解个人的职场性格特点和认知能力。 OPQ职业性格测评 这是一种性格测试&#xff0c;通常用于评估个人在…

APP逆向 day8 JAVA基础3

一.前言 昨天我们讲了点java基础2.0&#xff0c;发现是又臭又长&#xff0c;今天就是java基础的最后一章&#xff0c;也就是最难的&#xff0c;面向对象。上一末尾也是提到了面向对象&#xff0c;但是面向对象是最重要的&#xff0c;怎么可能只有这么短呢&#xff1f;所以今天…

人工智能——常用数学基础之线代中的矩阵

1. 矩阵的本质&#xff1a; 矩阵本质上是一种数学结构&#xff0c;它由按照特定规则排列的数字组成&#xff0c;通常被表示为一个二维数组。矩阵可以用于描述一组数据&#xff0c;或者表示某种关系&#xff0c;比如线性变换。 在人工智能中&#xff0c;矩阵常被用来表示数据集…

技术革新:如何用数据中台实现数字化转型

作为程序员&#xff0c;我们总是对技术如何改变企业运作充满好奇。今天&#xff0c;我们将深入探讨森马集团如何利用数据中台技术&#xff0c;实现从传统数据分析到数字化转型的华丽转身。 1. 技术背景&#xff1a;森马集团的数字化挑战 森马集团&#xff0c;一个在服饰行业占…

SpringCloud_Ribbon负载均衡

概述 SpringCloud底层其实是利用了一个名为Ribbon的组件&#xff0c;来实现负载均衡功能的。 源码 LoadBalancerInterceptor 其中含有intercept方法&#xff0c;拦截用户的HttpRequest请求&#xff1a; request.getURI() 获取请求uri&#xff0c;即http://userservice/use…

解析QAnything启动命令过程

一.启动命令过程日志 启动命令bash ./run.sh -c local -i 0 -b hf -m Qwen-1_8B-Chat -t qwen-7b-chat。输入日志如下所示&#xff1a; rootMM-202203161213:/mnt/l/20230918_RAG方向/QAnything# bash ./run.sh -c local -i 0 -b hf -m Qwen-1_8B-Chat -t qwen-7b-chat From …

Spring Boot如何集成Spring Security?

&#x1f345; 作者简介&#xff1a;哪吒&#xff0c;CSDN2021博客之星亚军&#x1f3c6;、新星计划导师✌、博客专家&#x1f4aa; &#x1f345; 哪吒多年工作总结&#xff1a;Java学习路线总结&#xff0c;搬砖工逆袭Java架构师 &#x1f345; 技术交流&#xff1a;定期更新…

1-3.文本数据建模流程范例

文章最前&#xff1a; 我是Octopus&#xff0c;这个名字来源于我的中文名–章鱼&#xff1b;我热爱编程、热爱算法、热爱开源。所有源码在我的个人github &#xff1b;这博客是记录我学习的点点滴滴&#xff0c;如果您对 Python、Java、AI、算法有兴趣&#xff0c;可以关注我的…

算法笔记:模拟过程(螺旋遍历矩阵)

1 模拟过程 “模拟过程题”通常指的是那些要求编程者通过编写代码来“模拟”或重现某个过程、系统或规则的题目。这类题目往往不涉及复杂的数据结构或高级算法&#xff0c;而是侧重于对给定规则的精确执行和逻辑的清晰表达。 其中螺旋遍历矩阵的题目就是一类典型的模拟过程题…

代码随想录-Day44

322. 零钱兑换 给你一个整数数组 coins &#xff0c;表示不同面额的硬币&#xff1b;以及一个整数 amount &#xff0c;表示总金额。 计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额&#xff0c;返回 -1 。 你可以认为每种硬币的数…

ARCGIS python 裁剪栅格函数 arcpy.management.Clip

ARCGIS python 裁剪栅格函数 arcpy.management.Clip 1 功能 裁剪掉栅格数据集、镶嵌数据集或图像服务图层的一部分。 2 使用情况 基于模板范围提取部分栅格数据集&#xff0c;输出与模板范围相交的所有像素使用以 x 和 y 坐标的最小值和最大值确定的包络矩形或使用输出范围文…

商汤上海AI实验室联合发布:自动驾驶全栈式高精度标定工具箱(含车、IMU、相机、激光雷达等的标定)

前言 在自动驾驶技术飞速发展的今天&#xff0c;传感器的精确标定对于确保系统性能至关重要。SensorsCalibration&#xff0c;一个专为自动驾驶车辆设计的标定工具箱&#xff0c;提供了一套全面的解决方案&#xff0c;用于校准包括IMU、激光雷达、摄像头和雷达在内的多种传感器…