C++笔记:从零开始一步步手撕红黑树

文章目录

  • 红黑树概念
  • 红黑树的性质
  • 红黑树 VS AVL树
  • 红黑树的结点与树的描述——定义类
  • 红黑树的插入操作
    • 步骤一:按照二叉搜索树的规则插入新结点
    • 步骤二:检测新节点插入后,红黑树的性质是否造到破坏
      • 情况一:uncle存在且为红
      • 情况二:uncle不存在或者uncle存在且为黑
    • 验证一棵红黑树是否符合规则

红黑树概念

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

在这里插入图片描述

红黑树的性质

红黑树主要靠以下几条性质或者说规则达到高度近似平衡:

  1. 结点的颜色不是 黑色 就是 红色
  2. 根结点的颜色是 黑色
  3. 任意一条路径中不存在连续的红色结点
  4. 每条路径上的黑色结点数量相同
  5. 规定空结点才是叶子结点,叶子结点都是黑色的(主要作用:数路径)。

为什么满足以上几条规则,红黑树就能保证:最长路径的有效结点个数不超过最短路径结点个数的 2 倍,接下来举个例子证明一下:
在这里插入图片描述

红黑树 VS AVL树

  1. 平衡条件的严格性:

AVL 树要求达到的是一种左右子树高度差的绝对值不超过 1 的绝对平衡
红黑树要求达到的是一种最长路径上的结点数不超过最短路径上的结点数的 2 倍的近似平衡

  1. 查找的效率分析:

假设红黑树和 AVL 树都具有 N N N 个结点:

对于AVL树:高度最多达到 l o g 2 ( N + 1 ) log_2(N+1) log2(N+1),最坏的情况下的查找次数为 2 log ⁡ 2 ( N + 1 ) 2\log_2(N+1) 2log2(N+1)

对于红黑树:高度最多达到 2 log ⁡ 2 ( N + 1 ) 2\log_2(N+1) 2log2(N+1),最坏的情况下的查找次数为 2 log ⁡ 2 ( N + 1 ) 2\log_2(N+1) 2log2(N+1)

虽然从分析来看红黑树的查找效率稍差于 AVL 树,但是由于 l o g 2 ( N + 1 ) log_2(N+1) log2(N+1) 这个数值足够小,它们之间的差异其实可以忽略不计。

举个例子来说,当 N = 10 亿 N = 10亿 N=10亿 时,AVL 树最多查找 30 次,红黑树最多也才查找 60 次,对于现代每秒钟运算上亿次的 CPU 来说,差异几乎不存在。

  1. 旋转操作的频率:

由于AVL树对平衡要求更严格,因此在插入或删除结点时可能需要更频繁地执行旋转操作来保持平衡;相比之下,红黑树对于树的平衡性有更宽松的要求,因此在实际操作中可能需要更少的旋转操作。

红黑树的结点与树的描述——定义类

// 结点的颜色
enum COLOR 
{
	RED,
	BLACK
};

// 结点类
template<class K, class V>
struct RBTreeNode 
{
	RBTreeNode(const pair<K, V>& kv)
		: _parent(nullptr)
		, _left(nullptr)
		, _right(nullptr)
		, _color(RED)
		, _kv(kv)
	{}

	RBTreeNode *_parent;
	RBTreeNode *_left;
	RBTreeNode *_right;
	COLOR _color;
	pair<K, V> _kv;
};

template<class K, class V>
class RBTree 
{
	typedef RBTreeNode<K, V> Node;
public:
	RBTree() : _root(nullptr) {}
protected:
	Node *_root;
};

【说明】

  1. 枚举类型COLOR: 用于表示红黑树结点的颜色。这样的设计是为了提高可读性,如结点->_color == RED 表示结点是红色,结点->_color == BLACK 表示结点是黑色的。
  2. RBTreeNode 结构体(类): 用于描述红黑树结点,包含了红黑树节点的基本属性和数据。
    • _parent:指向父节点的指针。
    • _left_right:分别指向左子节点和右子节点的指针。
    • _color:节点的颜色位,是一个枚举类型(红色黑色)。
    • _kv:表明设计的红黑树结点中存储的数据是键值对结构。
  3. RBTree 类: 描述红黑树的结构,只包含一个指向根节点的指针成员_root,红黑树的所有操作都是在这个基础上进行的。
    • RBTree() 是红黑树的无参默认构造函数,构造一棵空的红黑树。

关于结点类,这里有一个问题:为什么新构造的结点是红色的,不能是黑色的吗?

答案是不能,因为红黑树中有这样的两条规则:“ 任意一条路径中不存在连续的红色结点 ”、“ 每条路径上的黑色结点数量相同

由于在插入和删除操作过程中,难免会违反规则其中的一或者两条规则,但是在这两条规则中违反后者比违反前者的代价来得更大,新构造的结点是红色的在插入过程中很容易出现连续的红色结点,但是可以通过对结点的颜色重新调整或者旋转来处理,但是如果新增加黑色却很难处理,因为黑色结点的数量关乎到所有路径。

红黑树的插入操作

步骤一:按照二叉搜索树的规则插入新结点

  1. 树为空,则构造新结点,让_root 指针指向该结点,由于根结点必定是黑色的规定,将根结点的颜色置为黑,返回true。
  2. 树不空,按key的大小寻找插入位置,如果已存在,按插入失败处理,返回false。
  3. 走到空表示找到合适位置,然后插入构造的新结点,插入时要判断左边插入或者右边插入。

【步骤一部分的代码如下:】

/**
 * 函数介绍:插入一个键值对。
 *
 * 函数参数:
 *		kv: 要插入的键值对,其中第一个元素为键,第二个元素为值。
 *
 * 返回值:插入成功返回true,否则返回false。
 */
bool insert(const pair<K, V>& kv) 
{
	if (_root == nullptr)
	{
		_root = new Node(kv);
		_root->_color = BLACK;

		return true;
	}
	else
	{
		Node *parent = nullptr;
		Node *cur = _root;

		while (cur != nullptr)
		{
			if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				// 待插入kv已存在,插入失败
				return false;
			}
		}// 循环结束,构造新结点并链接

		cur = new Node(kv);

		if (parent->_kv.first < cur->_kv.first)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}

		cur->_parent = parent;
		
		// 检测新节点插入后,红黑树的性质是否造到破坏
		// 如果被性质被破坏要进行特殊处理
		// 步骤二的代码写在这里
		// ...
	}
}

步骤二:检测新节点插入后,红黑树的性质是否造到破坏

新插入节点的默认颜色是红色,插入之后又可能存在两种情况:

  1. 新节点的双亲节点的颜色是黑色,没有违反红黑树任何性质,则不需要调整插入结束
  2. 新插入节点的双亲节点颜色为红色,违反了不能有连续红色节点的规则,需要重新设置结点颜色;

而结点颜色具体怎么设置和 以下 4 个结点的情况有关:
在这里插入图片描述

仔细想一下,我们不难发现,curparentgrandparent这三个结点的颜色基本固定为、黑,cur可能是新插入的结点,也有可能是从黑色变成红色的结点(为什么等下会说),所以唯一的变量就是uncle结点。

情况一:uncle存在且为红

满足情况一时不关注结点位置,只进行颜色转换处理:

  • parentuncle变成黑色;
  • grandparent变成红色;
  • cur成为grandparent后重新判断;
    在这里插入图片描述

情况二:uncle不存在或者uncle存在且为黑

当遇到情况二时,单纯的颜色调整就不管用了,得旋转 + 颜色调整一起上,这里的旋转和AVL树的旋转几乎是一样的,关于旋转的部分的细节我就不多分析了,旋转的细节可以参考我之前写的《C++笔记:从零开始一步步手撕高阶数据结构AVL树》

当uncle不存在时:
在这里插入图片描述

当uncle存在且为黑时
在这里插入图片描述
综上所述:

  • parent为grandparent的左孩子时
    • cur为parent的左孩子时,右单旋,parent置黑,grandparent置红。
    • cur为parent的右孩子时,左右双旋,cur置黑,grandparent置红。
  • parent为grandparent的右孩子时
    • cur为parent的右孩子时,左单旋,parent置黑,grandparent置红。
    • cur为parent的左孩子时,右左双旋,cur置黑,grandparent置红。

【完整的红黑树插入代码如下,仅供参考】

bool insert(const pair<K, V>& kv) 
{
	if (_root == nullptr)
	{
		_root = new Node(kv);
		_root->_color = BLACK;

		return true;
	}
	else
	{
		Node *parent = nullptr;
		Node *cur = _root;

		while (cur != nullptr)
		{
			if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				// 待插入kv已存在,插入失败
				return false;
			}
		}// 循环结束,构造新结点并链接

		cur = new Node(kv);

		if (parent->_kv.first < cur->_kv.first)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}

		cur->_parent = parent;

		// 满足该条件下执行特殊处理
		while (parent && parent->_color == RED)
		{
			Node *grandparent = parent->_parent;

			if (parent == grandparent->_left)
			{
				Node *uncle = grandparent->_right;

				// 情况1:uncle存在且为RED ---> recolor(只变色处理)
				if (uncle != nullptr && uncle->_color == RED)
				{
					// recolor
					parent->_color = uncle->_color = BLACK;
					grandparent->_color = RED;

					// 继续往上处理
					cur = grandparent;
					parent = cur->_parent;
				}
				else // 情况2:uncle不存在 或者 uncle存在且为BLACK
					 // 调整颜色 + 旋转
				{
					if (cur == parent->_left)
					{
						// recolor + 右旋
						//       g
						//   p       u
						// c
						R_Rotate(grandparent);

						parent->_color = BLACK;
						grandparent->_color = RED;
					}
					else
					{
						// recolor + 左右双旋
						//       g
						//   p       u
						//     c
						L_Rotate(parent);
						R_Rotate(grandparent);

						cur->_color = BLACK;
						grandparent->_color = RED;
					}

					break;
				}
			}
			else // parent == grandparent->_right
			{
				Node *uncle = grandparent->_left;
				
				// 情况1:uncle存在且为RED ---> recolor(只变色处理)
				if (uncle != nullptr && uncle->_color == RED)
				{
					// recolor
					parent->_color = uncle->_color = BLACK;
					grandparent->_color = RED;

					// 继续往上处理
					cur = grandparent;
					parent = cur->_parent;
				}
				else // 情况2:uncle不存在 或者 uncle存在且为BLACK
				     // 调整颜色 + 旋转
				{
					if (cur == parent->_right)
					{
						// recolor + 左旋
						//       g
						//   u       p
						//             c
						L_Rotate(grandparent);

						parent->_color = BLACK;
						grandparent->_color = RED;
					}
					else
					{
						// recolor + 右左双旋
						//       g
						//   u       p
						//         c
						R_Rotate(parent);
						L_Rotate(grandparent);

						cur->_color = BLACK;
						grandparent->_color = RED;
					}

					break;
				}
			}
		}

		// parent == nullptr || parent->_color == BLACK
		// 暴力处理,根节点一定为黑色
		_root->_color = BLACK;		
		return true;
	}
}

验证一棵红黑树是否符合规则

使用上面的插入接口构建的红黑树,我们不能保证一定就没有问题,也许过程中存在某一处违反规则的情况出现,所以这里要针对红黑树的定义和规则写一个函数来验证它是否符合规则。

红黑树的检测分为两步:

  1. 检测其是否满足二叉搜索树,即中序遍历是否为有序序列;
void _inorder(Node *root)
{
	if (root == nullptr)
		return;

	_inorder(root->_left);
	cout << root->_kv.first << endl;
	_inorder(root->_right);
}

void inorder()
{
	_inorder(_root);
}
  1. 检测其是否满足红黑树的性质,主要有以下三条:
    • 根结点的颜色是 黑色
    • 任意一条路径中不存在连续的红色结点
    • 每条路径上的黑色结点数量相同
bool isRBTree()
{
	// 规则:根结点是黑色的
	if (_root && _root->_color == RED)
	{
		return false;
	}

	// 规则:每条路径的黑色结点数量一致 -> EqualBLACK
	int numOfBLACK = 0;
	Node *cur = _root;
	while (cur)
	{
		if (cur->_color == BLACK)
		{
			++numOfBLACK;
		}

		cur = cur->_left;
	}
	
	// 规则:不存在连续红结点 -> DiscontinuousRED
	return DiscontinuousRED(_root) && EqualBLACK(_root, 0, numOfBLACK);
}

bool DiscontinuousRED(Node *root)
{
	if (root == nullptr)
	{
		return true;
	}

	if (root->_color == RED && root->_parent->_color == RED)
	{
		return false;
	}

	return DiscontinuousRED(root->_left) && DiscontinuousRED(root->_right);
}

bool EqualBLACK(Node *root, int curPathBLACK, int &standardNumOfBALCK)
{
	if (root == nullptr)
	{
		// 走到空说明一条路径已经走完,该路径上的黑色结点数也同统计好了
		return curPathBLACK == standardNumOfBALCK;
	}

	if (root->_color == BLACK)
	{
		++curPathBLACK;
	}

	return EqualBLACK(root->_left, curPathBLACK, standardNumOfBALCK)
		&& EqualBLACK(root->_right, curPathBLACK, standardNumOfBALCK);
}

【顺带一提,这是优化后的代码】

判断是否存在连续红结点的函数和判断每条路径黑色结点数量是否相同的思路相似的,完全可以二合一。

bool isRBTree()
{
	// 规则:根结点是黑色的
	if (_root && _root->_color == RED)
	{
		return false;
	}

	// 规则:每条路径的黑色结点数量一致
	int numOfBLACK = 0;
	Node *cur = _root;
	while (cur)
	{
		if (cur->_color == BLACK)
		{
			++numOfBLACK;
		}

		cur = cur->_left;
	}
	
	return Check(_root, 0, numOfBLACK);
}

bool Check(Node* root, int curPathBLACK, int& standardNumOfBALCK)
{
	if (root == nullptr)
	{
		if (curPathBLACK != standardNumOfBALCK)
		{
			// 走到空说明一条路径已经走完,该路径上的黑色结点数也同统计好了
			return false;
		}

		return true;
	}

	if (root->_color == BLACK)
	{
		++curPathBLACK;
	}

	if (root->_color == RED && root->_parent->_color == RED)
	{
		return false;
	}

	return Check(root->_left, curPathBLACK, standardNumOfBALCK)
		&& Check(root->_right, curPathBLACK, standardNumOfBALCK);
}

至于红黑树的删除操作,这里留一个坑,之后有时间再填,完整的代码已经上传gitee仓库,这里是链接:https://gitee.com/ljinhao03/study-achievement/tree/master/RBTree

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

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

相关文章

外包干了9天,技术退步明显。。。。。

先说一下自己的情况&#xff0c;本科生&#xff0c;2018年我通过校招踏入了南京一家软件公司&#xff0c;开始了我的职业生涯。那时的我&#xff0c;满怀热血和憧憬&#xff0c;期待着在这个行业中闯出一片天地。然而&#xff0c;随着时间的推移&#xff0c;我发现自己逐渐陷入…

【微服务】分布式调度框架PowerJob使用详解

目录 一、前言 二、定时任务调度框架概述 2.1 为什么需要定时任务调度框架 2.2 定时任务调度使用场景 三、PowerJob 介绍 3.1 PowerJob 概述 3.2 PowerJob 功能特性 3.3 PowerJob 应用场景 3.4 PowerJob 与其他同类产品对比 四、PowerJob 部署 4.1 PowerJob 架构 4.…

【linux】进程(一)

先看预备知识&#xff0c;对本篇文章更有帮助。 目录 进程概念&#xff1a;了解动态运行的概念&#xff1a;进程的本身内部属性&#xff1a;启动进程&#xff1a;关闭进程&#xff1a; 如何创建进程&#xff1a;进程状态&#xff1a;直接看进程状态&#xff1a;僵尸进程与孤儿…

工智能的迷惑是技术发展的产物

简述&#xff1a; 随着ChatGPT在全球科技舞台上掀起一股热潮&#xff0c;人工智能再次成为了人们关注的焦点。各大公司纷纷紧跟潮流&#xff0c;推出了自己的AI大模型&#xff0c;如&#xff1a;文心一言、通义千问、讯飞星火、混元助手等等&#xff0c;意图在人工智能领域占据…

HarmonyOS NEXT应用开发—状态栏显隐变化

介绍 本示例介绍使用Scroll组件的滚动事件 onScroll 实现状态栏显隐变化。该场景多用于各种软件的首页、我的等页面中。 效果预览图 使用说明 加载完成后显示状态栏显隐变化页面&#xff0c;上下拖动屏幕&#xff0c;顶端状态栏出现显隐变化。 实现思路 在置顶位置使用sta…

三维坐标系之间的转换

一、概括 这个完全是抄别人的&#xff0c;给我自己看的&#xff0c;后期会吧代码更新上去。 彻底搞懂“旋转矩阵/欧拉角/四元数”&#xff0c;让你体会三维旋转之美_欧拉角判断动作-CSDN博客 在不同的坐标系中物体的位姿是相对的&#xff0c;任何的坐标系都是相对来说的&…

sql查询语句中提取【】里面的值

在实际工作中&#xff0c;有时候会遇到提取sql查询出来的字段中括号里面的码值&#xff0c;比如&#xff1a; 我现在要提取student表的sname中括号里面的字段 解决方法如下&#xff1a; select sname,replace(substr(sname, instr(sname, [,1)1),],) from student;成功提取&am…

BIG-Bench Hard 数据集分享

来源: AINLPer公众号&#xff08;每日干货分享&#xff01;&#xff01;&#xff09; 编辑: ShuYini 校稿: ShuYini 时间: 2024-3-17 该数据集由Google、斯坦福等研究人员开发&#xff0c;BBH的全称是BIG-Bench Hard&#xff0c;它是BIG-Bench数据集的一个子集&#xff0c;它专…

说JS作用域,就不得不说说自执行函数

一个兜兜转转&#xff0c;从“北深”回到三线城市的小码农&#xff0c;热爱生活&#xff0c;热爱技术&#xff0c;在这里和大家分享一个技术人员的点点滴滴。欢迎大家关注我的微信公众号&#xff1a;果冻想 前言 不得不吐槽&#xff0c;学个JS&#xff0c;这个概念也太多了&am…

Java Web项目—餐饮管理系统Day06-套餐管理(一)

文章目录 1. 需求分析与实体类准备2. 依据菜品分类或者名字进行查询的请求(需求B)3. 新增套餐 1. 需求分析与实体类准备 如上图为新增套餐的界面, 它包含了套餐的一些基本信息, 例如名称、价格等, 同时还有套餐分类(因此这里需要一个查询所有套餐分类的请求处理方法, 需求A). 以…

Twincat实现电机控制

不仅是控制系统的核心部分&#xff0c;而且能够将任何基于PC的系统转换为一个带有PLC、NC、CNC和机器人实时操作系统的实时控制系统。TwinCAT软件在工业自动化领域具有广泛的应用&#xff0c;特别是在机器人关节电机控制方面!!! 在机器人关节电机控制方面&#xff0c;TwinCAT通…

Go——运算符,变量和常量,基本类型

一.运算符 Go语言内置的运算符有&#xff1a; 算术运算符 关系运算符 逻辑运算符 位运算符 赋值运算符 1.1 算术运算符 注意&#xff1a;(自增)和--(自减)在go语言中是单独的语句&#xff0c;并不是运算符。 1.2 关系运算符 1.3 逻辑运算符 1.4 位运算符 位运算符对整数在内存…

UnityShader:IBL

效果&#xff1a; 实现&#xff1a; Shader "MyShader/IBL" {Properties{_CubeMap ("环境贴图", Cube) "white" {}_Exposure("曝光",float)1.0_Color("颜色",color)(1,1,1,1)_NormalMap("法线贴图",2d)"bu…

鸿蒙开发(五)-应用签名相关

鸿蒙开发(五)-应用签名相关 本篇文章主要介绍下鸿蒙应用下的应用签名的创建与配置。 根据之前的介绍&#xff0c;我们知道&#xff0c;在DevEco Studio默认创建的应用程序&#xff0c;是没有sign配置的。 默认输出的应用文件如下&#xff1a; build->default->output…

【小沐学AI】数据分析的Python库:Pandas AI

文章目录 1、简介2、安装2.1 Python2.2 PandasAI 3、部署4、功能4.1 大型语言模型 &#xff08;LLM&#xff09;4.1.1 BambooLLM4.1.2 OpenAI 模型4.1.3 谷歌 PaLM4.1.4 谷歌 Vertexai4.1.5 Azure OpenAI4.1.6 HuggingFace 模型4.1.7 LangChain 模型4.1.8 Amazon Bedrock 模型4…

手机翻页效果的电子画册如何实现?

​在信息 爆炸的时代&#xff0c;纸质画册已经难以满足人们快速获取和分享信息的需求。而电子画册&#xff0c;以其独特的翻页效果和便捷的传播方式&#xff0c;正逐渐受到大众的青睐。那么&#xff0c;这种让人眼前一亮的手机翻页电子画册是如何制作的呢&#xff1f; 接下来&a…

一体成型PFA尖头镊子高纯特氟龙材质镊子适用半导体新材料

PFA镊子用于夹取小型片状、薄状、块状样品&#xff0c;广泛应用在半导体、新材料、新能源、原子能、石油化工、无线电、电力机械等行业。 具有耐高低温性&#xff08;可使用温度-200℃&#xff5e;&#xff0b;260℃&#xff09;、耐腐蚀、表面不粘性等特点&#xff0c;用于苛…

C#调用Halcon出现尝试读取或写入受保护的内存,这通常指示其他内存已损坏。System.AccessViolationException

一、现象 在C#中调用Halcon&#xff0c;出现异常提示&#xff1a;尝试读取或写入受保护的内存,这通常指示其他内存已损坏。System.AccessViolationException 二、原因 多个线程同时访问Halcon中的某个公共变量&#xff0c;导致程序报错 三、测试 3.1 Halcon代码 其中tsp_width…

【Linux】进程间通信2(共享内存||消息队列)

共享内存 介绍 1.共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间&#xff0c;这些进程间数据传递不再涉及到内核&#xff0c;换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。 2.当共享内存创建出来后&#xff0c;通过系统调用挂接到…

StarRocks实战——云览科技存算分离实践

目录 背景 一、平台现状&痛点 1.1 使用组件多&#xff0c;维护成本高 1.2 链路冗长&#xff0c;数据时效性难以保证 1.3 服务稳定性不足 二、StarRocks 存算分离调研 2.1 性能对比 2.2 易用性 2.3 存储成本 三、StarRocks 存算分离实践 3.1 查询优化 3.1.1 物化…