【数据结构与算法】—— 手撕红黑树

目录

(一)红黑树的定义

1、红黑树的引入

2、红黑树的概念

3、红黑树的性质

(二)红黑树的操作

1、红黑树节点的定义

2、红黑树的插入操作

1️⃣ 思路

2️⃣ 代码实现

3、红黑树的删除操作(了解)

4、红黑树与AVL树的比较

5、红黑树的验证

总结


(一)红黑树的定义

1、红黑树的引入

为了保持 AVL 树的平衡性,插入和删除操作后,非常频繁地调整全树整体拓扑结构,代价较大。为此在 AVL 树的平衡标准上进一步放宽条件,引入了红黑树的结构。

2、红黑树的概念

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

3、红黑树的性质

一棵红黑树是满足如下红黑性质的二叉排序树:

  • ①每个结点或是红色,或是黑色的
  • ②根结点是黑色的
  • ③叶结点(虚构的外部结点、 NULL 结点)都是黑色的
  • ④不存在两个相邻的红结点(即红结点的父结点和孩子结点均是黑色的)
  • ⑤对每个结点,从该结点到任一叶结点的简单路径上,所含黑结点的数量相同

与折半查找树和 B 树类似,为了便于对红黑树的实现和理解,引入了 n +1个外部叶结点,以保证红黑树中每个结点(内部结点)的左、右孩子均非空。下图所示是一棵红黑树:

从某结点出发(不含该结点)到达一个叶结点的任一简单路径上的黑结点总数称为该结点的黑高(记为 bh ),黑高的概念是由性质⑤确定的。根结点的黑高称为红黑树的黑高。

结论1:从根到叶结点的最长路径不大于最短路径的2倍。

  1. 由性质⑤,当从根到任一叶结点的简单路径最短时,这条路径必然全由黑结点构成;
  2. 由性质④,当某条路径最长时,这条路径必然是由黑结点和红结点相间构成的,此时红结点和黑结点的数量相同;
  3. 上图中的 13-8-1 和13 - 17 - 25 - 27就是这样的两条路径。

结论2:有n个内部结点的红黑树高度 h <=2log(N+1)

由此可见,红黑树的"适度平衡",由 AVL 树的"高度平衡",降低到"任一结点左右子树的高度,相差不超过2倍",也降低了动态操作时调整的频率。对于一棵动态查找树,如果插入和删除操作比较少,查找操作比较多,采用 AVL 树比较合适,否则采用红黑树更合适。但由于维护这种高度平衡所付出的代价比获得的效益大得多,红黑树的实际应用更广泛, C ++中的 map 和 set ( Java 中的 TreeMap 和 TreeSet )就是用红黑树实现的。
 


(二)红黑树的操作

1、红黑树节点的定义

enum Colour
{
	RED,
	BLACK,
};

// 红黑树节点的定义

template<class K, class V>
struct RBTreeNode
{ 
	RBTreeNode<K, V>* _left;   // 节点的左孩子
	RBTreeNode<K, V>* _right;  // 节点的右孩子
	RBTreeNode<K, V>* _parent; // 节点的双亲(红黑树需要旋转,为了实现简单给出该字段)
	pair<K, V> _kv;            // 节点的值域
	Colour _col;               // 节点的颜色

	RBTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _col(RED)
	{}
};

思考:在节点的定义中,为什么要将节点的默认颜色给成红色的?

红黑树是一种自平衡的二叉搜索树,它通过在插入和删除节点时进行一系列的旋转和重新着色操作来维持平衡性质。根据红黑树的性质,每个节点要么是红色,要么是黑色。

在红黑树的插入和删除操作中,为了保持树的平衡,需要对节点进行旋转和着色。将新插入的节点默认着色为红色有以下几个原因:

  1. 红色节点具有更多的调整空间:将节点默认着色为红色,可以让新节点具有更多的调整空间,有利于保持树的平衡性。红色节点的插入和删除操作对树的平衡影响较小,因为红色节点可以在需要时进行旋转和着色调整。

  2. 简化插入操作的修正过程:新节点默认为红色,可以简化插入操作的修正过程。根据红黑树的性质,插入红色节点后只需要考虑和父节点的关系,而不需要像黑色节点那样考虑更多的调整情况。这样可以减少修正操作的复杂性和开销。

  3. 减少整体平衡操作的次数:将新节点默认设置为红色,可以减少整体平衡操作的次数。由于红色节点的插入和删除操作对树的平衡影响较小,相比于将新节点默认设置为黑色,将其默认设置为红色可以减少平衡操作的频率和开销。

总的来说,将节点的默认颜色设置为红色是为了在红黑树等自平衡数据结构中简化插入和删除操作的修正过程,同时减少整体平衡操作的次数,提高了算法的效率和性能。

2、红黑树的插入操作

1️⃣ 思路

红黑树是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树的插入可分为两步:

  • 1.按照二叉搜索的树规则插入新节点(这个我们之前说过,这里就不过多解释了)
  • 2.检测新节点插入后,红黑树的性质是否造到破坏(重点讲解

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

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

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

 解决方式:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整

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

1.p为g的左孩子,cur为p的左孩子,则进行右单旋转;相反,

2.p为g的右孩子,cur为p的右孩子,则进行左单旋转

3.p、g变色--p变黑,g变红

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

 针对每种情况进行相应的处理即可


2️⃣ 代码实现

bool Insert(const pair<K, V>& kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			_root->_col = BLACK;
			return true;
		}

		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}

		//先插入结点进行链接操作
		cur = new Node(kv);
		if (parent->_kv.first > kv.first)
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}
		cur->_parent = parent;

		//进行调整操作
		while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent;
			if (grandfather->_left == parent)
			{
				Node* uncle = grandfather->_right;
				// 情况1:u存在且为红,变色处理,并继续往上处理
				if (uncle && uncle->_col == RED)
				{
					parent->_col = BLACK;
					uncle->_col = BLACK;
					grandfather->_col = RED;

					// 继续往上调整
					cur = grandfather;
					parent = cur->_parent;
				}
				else // 情况2+3:u不存在/u存在且为黑,旋转+变色
				{
					//     g
					//   p   u
					// c 
					if (cur == parent->_left)
					{
						RotateR(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						//     g
						//   p   u
						//     c
						RotateL(parent);
						RotateR(grandfather);
						cur->_col = BLACK;
						//parent->_col = RED;
						grandfather->_col = RED;
					}

					break;
				}
			}
			else // (grandfather->_right == parent)
			{
				//    g
				//  u   p
				//        c
				Node* uncle = grandfather->_left;
				// 情况1:u存在且为红,变色处理,并继续往上处理
				if (uncle && uncle->_col == RED)
				{
					parent->_col = BLACK;
					uncle->_col = BLACK;
					grandfather->_col = RED;

					// 继续往上调整
					cur = grandfather;
					parent = cur->_parent;
				}
				else // 情况2+3:u不存在/u存在且为黑,旋转+变色
				{
					//    g
					//  u   p
					//        c
					if (cur == parent->_right)
					{
						RotateL(grandfather);
						grandfather->_col = RED;
						parent->_col = BLACK;
					}
					else
					{
						//    g
						//  u   p
						//    c
						RotateR(parent);
						RotateL(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}

					break;
				}
			}
		}

		_root->_col = BLACK;

		return true;
	}

3、红黑树的删除操作(了解)

红黑树的删除本节不做讲解,有兴趣的同学可参考:《算法导论》或者《STL源码剖析》
红黑树的删除


4、红黑树与AVL树的比较

红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O(log_2 N),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。

5、红黑树的验证

红黑树的检测分为两步:

  • 1. 检测其是否满足二叉搜索树(中序遍历是否为有序序列)
  • 2. 检测其是否满足红黑树的性质
     
	void Inorder()
	{
		_Inorder(_root);
	}

	bool IsBalance()
	{
		if (_root && _root->_col == RED)
		{
			cout << "根节点颜色是红色" << endl;
			return false;
		}

		int benchmark = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK)
				++benchmark;
			cur = cur->_left;
		}
		// 连续红色节点
		return _Judge(_root, 0, benchmark);
	}

bool _Judge(Node* root, int blackNum, int benchmark)
	{
		if (root == nullptr)
		{
			if (blackNum != benchmark)
			{
				cout << "某条路径黑色节点的数量不相等" << endl;
				return false;
			}
			return true;
		}

		if (root->_col == BLACK)
		{
			++blackNum;
		}

		if (root->_col == RED
			&& root->_parent
			&& root->_parent->_col == RED)
		{
			cout << "存在连续的红色节点" << endl;
			return false;
		}

		return _Judge(root->_left, blackNum, benchmark)
			&& _Judge(root->_right, blackNum, benchmark);
	}

总结

以上便是关于红黑树的介绍。感谢大家的观看与支持!!!

 

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

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

相关文章

ssm汽车养护管理系统源码和论文

ssm汽车养护管理系统038 开发工具&#xff1a;idea 数据库mysql5.7 数据库链接工具&#xff1a;navcat,小海豚等 技术&#xff1a;ssm 开题报告内容&#xff1a;&#xff08;研究现状、目的意义&#xff1b;基本内容、研究方法、参考文献等。&#xff09; 研究现状 国外…

【中危】Apache Ivy<2.5.2 存在XXE漏洞 (CVE-2022-46751)

漏洞描述 Apache Ivy 是一个管理基于 ANT 项目依赖关系的开源工具&#xff0c;文档类型定义(DTD)是一种文档类型定义语言,它用于定义XML文档中所包含的元素以及元素之间的关系。 Apache Ivy 2.5.2之前版本中&#xff0c;当解析自身配置、Ivy 文件或 Apache Maven 的 POM 文件…

IntelliJ IDEA 官方网站 idea官网 http://www.jetbrains.com/idea/

IntelliJ IDEA 官方网站 idea官网 http://www.jetbrains.com/idea/ Idea下载官网一键直达&#xff1a; 官网一键直达

【STM32】FreeRTOS软件定时器学习

软件定时器 FreeRTOS提供了现成的软件定时器功能&#xff0c;可以一定程度上替代硬件定时器&#xff0c;但精度不高。 实验&#xff1a;创建一个任务&#xff0c;两个定时器&#xff0c;按键开启定时器&#xff0c;一个500ms打印一次&#xff0c;一个1000ms打印一次。 实现&…

关于模板的大致认识【C++】

文章目录 函数模板函数模板的原理函数模板的实例化模板参数的匹配原则 类模板类模板的定义格式类模板的实例化 非类型模板参数typename 与class模板的特化函数模板特化类模板特化全特化偏特化 模板的分离编译 函数模板 函数模板的原理 template <typename T> //模板参数…

mongodb集群

端口192.168.115.3 192.168.115.4 1192.168.115.5 下载MongoDB软件包版本为4.2.14并安装 rpm -ih --force --nodeps *.rpm 2创建文件夹mkdir -p /opt/local/mongo-cluster/conf 3.在目录里创建配置文件cd /opt/local/mongo-cluster/conf …

3D数据转换工具HOOPS Exchange概览

HOOPS Exchange SDK是一组C软件库&#xff0c;使开发团队能够快速为其应用程序添加可靠的2D和3D CAD导入和导出功能。这允许访问广泛的数据&#xff0c;包括边界表示&#xff08;BREP&#xff09;、产品制造信息&#xff08;PMI&#xff09;、模型树、视图、持久ID、样式、构造…

低代码开发ERP:精打细算,聚焦核心投入

企业数字化转型已经成为现代商业环境中的一项关键任务。如今&#xff0c;企业面临着日益激烈的竞争和不断变化的市场需求。在这样的背景下&#xff0c;数字化转型不仅是企业生存的必然选择&#xff0c;也是取得竞争优势和实现可持续发展的关键因素。 在数字化转型的过程中&…

【Hello Network】数据链路层协议

本篇博客简介&#xff1a;介绍数据链路层的各协议 数据链路层 以太网协议认识以太网协议以太网帧格式局域网通信原理再理解 MTU认识MTUMTU对IP协议的影响MTU对UDP协议的影响MTU对于TCP协议的影响如何查看ip地址 mac地址 以及mtu ARP协议ARP协议的作用ARP协议在哪里ARP的工作过程…

解决charles无法抓取localhost数据包

我们有时候在本地调试的时候&#xff0c;使用charles抓取向本地服务发送的请求的&#xff0c;发现无法抓取。 charles官方也作了相应说明&#xff1a; 大概意思就是 某些系统使用的是硬编码不能使用localhost进行传输&#xff0c;所以当我们连接到 localhost的时候&#xff0c…

leetcode 188. 买卖股票的最佳时机 IV

2023.8.21 这道题是 买卖股票的最佳时机III 的升级版&#xff0c;即买卖次数限制为k次&#xff0c;做法和上一篇如法炮制&#xff0c;直接看代码&#xff1a; class Solution { public:int maxProfit(int k, vector<int>& prices) {vector<vector<int>>…

常用性能测试工具及其功能

在软件开发周期的不同阶段&#xff0c;性能测试工具被广泛用于评估系统的性能和发现潜在的性能瓶颈。本文介绍了几种常用的性能测试工具&#xff0c;包括负载测试工具、压力测试工具和基准测试工具&#xff0c;并详细描述了它们的功能和用法。 性能测试在软件开发的各个阶段都至…

Docker的数据管理及端口映射与容器互联(使用centos镜像)

目录 Docker数据管理 1&#xff0e;数据卷 2&#xff0e;数据卷容器 Docker端口映射 Docker容器互联 Docker数据管理 管理 Docker 容器中数据主要有两种方式&#xff1a;数据卷&#xff08;Data Volumes&#xff09;和数据卷容器&#xff08;DataVolumes Containers&…

Java接口详解

接口 接口的概念 在现实生活中&#xff0c;接口的例子比比皆是&#xff0c;比如&#xff1a;笔记本上的USB口&#xff0c;电源插座等。 电脑的USB口上&#xff0c;可以插&#xff1a;U盘&#xff0c;鼠标&#xff0c;键盘等所有符合USB协议的设备 电源插座插孔上&#xff0c;…

linux 上安装es

首先 到官网 https://www.elastic.co/cn/downloads/elasticsearch 下载对应的安装包&#xff0c;我这里下载的是 https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.9.1-linux-x86_64.tar.gz 然后讲该压缩包上传到 linux 的/usr/local 目录下执行 tar -z…

redis 存储结构原理 2

咱们接着上一部分来进行分享&#xff0c;我们可以在如下地址下载 redis 的源码&#xff1a; https://redis.io/download 此处我下载的是 redis-6.2.5 版本的&#xff0c;xdm 可以直接下载上图中的 **redis-6.2.6 **版本&#xff0c; redis 中 hash 表的数据结构 redis hash …

(6)(6.2) 任务命令

文章目录 前言 6.2.1 概述 6.2.2 导航命令 6.2.3 条件命令 6.2.4 DO命令 前言 本文介绍了 Copter、Plane 和 Rover 切换到自动模式时支持的任务指令。 &#xff01;Warning 这是一项正在进行中的工作&#xff0c;尚未经过全面审核。有关 Copter 的更佳列表&#xff0c;请…

git拉取失败/git fatal终极解决方法

前言 被折磨不下20次总结出来的终极方案 步骤 0 首先关闭代理试试&#xff0c;不行就下一步 1 重置代理或者取消代理的方式 git config --global --unset http.proxy git config --global --unset https.proxy添加全局代理 git config --global http.proxy git config …

【从零学习python 】56. 异常处理在程序设计中的重要性与应用

文章目录 异常的概念读取文件异常try...except语句try...else语句try...finally语句 进阶案例 异常的概念 在程序运行过程中&#xff0c;由于编码不规范或其他客观原因&#xff0c;可能会导致程序无法继续运行&#xff0c;此时就会出现异常。如果不对异常进行处理&#xff0c;…

RabbitMQ---work消息模型

1、work消息模型 工作队列或者竞争消费者模式 在第一篇教程中&#xff0c;我们编写了一个程序&#xff0c;从一个命名队列中发送并接受消息。在这里&#xff0c;我们将创建一个工作队列&#xff0c;在多个工作者之间分配耗时任务。 工作队列&#xff0c;又称任务队列。主要思…