【高阶数据结构】手撕红黑树(超详细版本)

news2024/11/19 11:18:48

🌈欢迎来到数据结构专栏~~手撕红黑树


  • (꒪ꇴ꒪(꒪ꇴ꒪ )🐣,我是Scort
  • 目前状态:大三非科班啃C++中
  • 🌍博客主页:张小姐的猫~江湖背景
  • 快上车🚘,握好方向盘跟我有一起打天下嘞!
  • 送给自己的一句鸡汤🤔:
  • 🔥真正的大师永远怀着一颗学徒的心
  • 作者水平很有限,如果发现错误,可在评论区指正,感谢🙏
  • 🎉🎉欢迎持续关注!
    请添加图片描述

请添加图片描述

文章目录

  • 🌈欢迎来到数据结构专栏~~手撕红黑树
    • 一. 红黑树的概念😎
    • 二. 五大特性
    • 三. 节点的定义
    • 四. 红黑树插入
      • ⚡模型
      • 🥑情况一:u 存在且为红
      • 🥑情况二:
        • 💥具体情况1️⃣:u不存在
        • 💥具体情况2️⃣:u存在且为黑
        • 💥双旋是怎么样产生的?
    • 大总结
    • 五. 验证红黑树
    • 六. 红黑树的性能
    • 七. 红黑树的性能

请添加图片描述

一. 红黑树的概念😎

红黑树也是一种二叉搜索树,但是每个节点都存储了一个颜色,该颜色可以为黑可以为红,因此也叫红黑树

红黑树和AVL树的区别就是:红黑树是近似平衡,但不是完全平衡,没有和AVL树一样 的通过平衡因子来控制高度差,而是通过每条路径上对红黑节点的控制,来达到确保最长路径长度不超过最短路径长度的 2 倍。

在这里插入图片描述

二. 五大特性

  1. 根节点一定为黑色
  2. 每个节点只能是
  3. 节点为红色,则该节点的两个子节点都为黑色(也就是树中没有连续的红色节点
  4. 对于每个结点,从该结点到其所有后代叶子结点的简单路径上,均包含相同数目的黑色结点(每条路径的黑色节点相等
  5. 每个叶子结点都是黑色(此处的叶子节点指的是空节点NIL节点)

如果是空树,刚好是空节点为黑色,也符合第一条规则

那么问题来了,仅仅依靠这五大特性是如何确保最长路径长度 <= 最短路径长度的 2 倍

根据红黑树的性质3可以得出,红黑树当中不会出现连续的红色结点,而根据性质4又可以得出,从某一结点到其后代叶子结点的所有路径上包含的黑色结点的数目是相同的

所以我们不妨假设极端场景,最短的路径无非就是全黑的情况了,假设此时有 n 个节点,长度就为 n

在这里插入图片描述
而最长路径就是:一红一黑排列的,如果有 n 个黑色节点,那么红色节点数目与黑色相同,则长度为 2n,所以不超过两倍!

三. 节点的定义

cv工程师登场!此处我们还是定义成三叉链,只不过把平衡因子替换成了节点颜色,因为节点的颜色就两种,直接枚举就好了

//枚举颜色
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)
	{}
};

此处有个问题:为什么构造结点时,默认将结点的颜色设置为红色?

此处我们知道插入黑色的节点是一定违反了性质4;某条路径的黑色节点一定会增加一个,那为了维护结构,我们岂不是要在其他的路径是不是也要增加一个呢?
但此时如果新增的是一个红色的节点呢;如果根节点为红色,那么又会破坏性质 3 出现了连续的红色节点,但是如果根节点为黑色,就不需要进行调整。 也就是说新增red节点不一定会破坏结构,但新增Black节点就一定会破坏。

在这里插入图片描述

四. 红黑树插入

此处插入的前半部分和AVL树的插入一样:

  1. 按二叉搜索树的插入方法,找到待插入位置
  2. 将待插入结点插入到树中
  3. 若插入结点的父结点是红色的,则需要对红黑树进行调整

红黑树的关键就在这第三步中!大有来头

⚡模型

我们先给出一个基本模型:(可以是子树也可以是一棵完整的树)

在这里插入图片描述

cur :当前节点
p:parent,父节点
g:grandfather,祖父节点
u:uncle,叔叔节点
a,b,c,d,e:子树

顺便科普一下树的路径是从根节点一路走到空节点(NIL 节点)才算一条路径,而不是走到叶子就停下来了

在这里插入图片描述
所以上面这棵树的有效路径有9条,而不是4条

🥑情况一:u 存在且为红

cur 为红,p 为红,g 为黑, u 存在且为红

在这里插入图片描述

首先我们知道红黑树的关键是看叔叔uncle;节点的颜色是固定为黑色的;因为不能有两个相同的红色节点,所以我们开始调整!首先将parent变成黑色;又为了不违反性质4,所以uncle的节点也要变成黑色;同时也要将grandparent节点变红,不要忘了这可能只是一颗子树;为了维持每条路径上黑色节点的数量;祖父必须变红,不然会多出一个黑色节点。

在这里插入图片描述

最后不要忘了将祖父当成 cur 节点继续向上调整,直到g是根,最后才将变成黑色!

具体代码:

		while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent;
			assert(grandfather);
			assert(grandfather->_col == BLACK);

			//关键看叔叔  ~ 判断叔叔的位置
			if (parent == grandfather->_left)
			{
				Node* uncle = grandfather->_right;
				//情况1:uncle存在且为红  + 继续往上处理
				if (uncle && uncle->_col = RED)
				{
					//变色:p和u变黑,g变红
					parent->_col = uncle ->_col = Black;
					grandfather->_col = RED;

					//继续往上调整
					cur = grandfather;
					parent = cur->_parent;
				}
				else  //情况2 
				{}
			}
			else  //parent == grandfather->_right
			{
				Node* uncle = grandfather->_left;
				//情况1:uncle存在且为红 + 继续往上处理
				if (uncle&& uncle->_col = RED)
				{
					//变色:p和u变黑,g变红
					parent->_col = uncle->_col = Black;
					grandfather->_col = RED;

					//继续往上调整
					cur = grandfather;
					parent = cur->_parent;
				}
				else  //情况2 
				{}
			}	
		}
		_root->_col = BLACK;//不管什么,最后根要变黑
		return true;
	}

🥑情况二:

💥具体情况1️⃣:u不存在

cur 为红,p 为红,g 为黑, u 不存在

在这里插入图片描述
如果叔叔不存在,那么a/b/c/d/e都是空树,cur是新增节点!因为叔叔不存在的话,parent下也不可能挂上黑节点!

此处也违背了性质4,所谓单纯的变色也无法处理了,那就旋转试试看吧,
祖孙三代在一条直线上偏左,一波右单旋安排,接着根节点变成 parent 后调整颜色,父亲变黑,祖父变红,一波操作下来黑节点数目没边,根节点也是黑色不用再向上调整了

在这里插入图片描述

💥具体情况2️⃣:u存在且为黑

cur 为红,p 为红,g 为黑, u 存在且为黑

在这里插入图片描述

b和c可以是空树或者是一个红节点,a可以是根也可以是黑节点的子树,下面四种的的任意一种

在这里插入图片描述

以下的情况一定是由情况一往上调整过程中才会出现的!即这种情况下的cur结点一定不是新插入的结点,而是上一次情况一调整过程中的祖父结点,如下图:

在这里插入图片描述

此上的情况必定是左边的的多一个黑色节点,如上图所示, 假设祖父上面有 x 个黑节点,叔叔下有y个节点,那么左子树(含祖父)现在是 x +1 个,右子树(含祖父)是 x + 2 + y 个,很明显 x + 2 + y > x + 1,因此在插入结点前就已经不满足要求了,所以说叔叔结点存在且为黑这种情况,一定是由情况一往上调整过程中才会出现的!

此处单纯的变色已经无法处理,况且还违背了性质4;这时我们需要进行旋转+变色处理

在这里插入图片描述

如果是祖父、父亲、cur 都在同一条直线上,那么只需要单旋即可

💥双旋是怎么样产生的?

若祖孙三代的关系是折线(cur、parent、grandfather这三个结点为一条折现),则我们需要先进行双旋操作,再进行颜色调整,颜色调整后这棵被旋转子树的根是黑色的,因此无需继续往上进行处理

抽象图如下:

在这里插入图片描述

以上情况的完整代码:

//如果插入节点的父节点是红色的,则需要对红黑树进行操作
		while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent;
			assert(grandfather);
			assert(grandfather->_col == BLACK);

			//关键看叔叔  ~ 判断叔叔的位置
			if (parent == grandfather->_left)
			{
				Node* uncle = grandfather->_right;
				//情况1:uncle存在且为红  + 继续往上处理
				if (uncle && uncle->_col == RED)
				{
					//变色:p和u变黑,g变红
					parent->_col = uncle ->_col = BLACK;
					grandfather->_col = RED;

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

						//颜色调整
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else//cur == parent->_right
					{
						//情况三:左右双旋 + 变色
						//    g
						//  p   u
						//    c 
						RotateL(parent);
						RotateR(grandfather);

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

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

						//颜色调整
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else//cur == parent->_left
					{
						//情况三:右左双旋 + 变色
						//    g
						//  u   p
						//    c 
						RotateR(parent);
						RotateL(grandfather);

						//调整颜色
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}	
		}

大总结

无论是情况一还是情况二,cur为红,p为红,g为黑这三个条件是固定的

  • 情况一:叔叔存在且为红

    • 1️⃣单纯的变色:p和u变黑,g变红
    • 2️⃣把g继续当成cur,g不是根继续往上处理(继续往上处理有可能变成情况2);g是根就变成黑色
  • 情况二:叔叔不存在 or 叔叔存在且为黑

    • 1️⃣旋转:具体情况来判断是什么旋转(祖孙三代是折线关系就是双旋,直线就是单旋)
    • 2️⃣变色:p变黑, g变红

在这里插入图片描述

五. 验证红黑树

还是老套路中序遍历来验证:

void Inorder()
{
	_Inorder(_root);
}

void _Inorder(Node* root)
{
	if (root == nullptr)//空树也是红黑树
		return;
	_Inorder(root->_left);
	cout << root->_kv.first << " ";
	_Inorder(root->_right);
}

在这里插入图片描述

那怎么样验证它是红黑树呢?从五大特性下手!

	bool IsBalance()
	{
		if (_root == nullptr)
		{
			return true;
		}

		if (_root->_col == RED)
		{
			cout << "根节点不是黑色" << endl;
			return false;
		}

		// 黑色节点数量基准值
		int benchmark = 0;
		Node* cur = _root;
		while (cur)
		{
		if (cur->_col == BLACK)
		++benchmark;

		cur = cur->_left;//以最左的路径进行
		}

		return PrevCheck(_root, 0, benchmark);
	}
	bool PrevCheck(Node* root, int blackNum, int& benchmark)
	{
		if (root == nullptr)
		{
			//cout << blackNum << endl;
			//return;
			if (benchmark == 0)
			{
				benchmark = blackNum;
				return true;
			}

			if (blackNum != benchmark)
			{
				cout << "某条黑色节点的数量不相等" << endl;
				return false;
			}
			else
			{
				return true;
			}
		}

		if (root->_col == BLACK)
		{
			++blackNum;
		}
        //检测当前节点以及父亲
		if (root->_col == RED && root->_parent->_col == RED)
		{
			cout << "存在连续的红色节点" << endl;
			return false;
		}

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

六. 红黑树的性能

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

参考地址

七. 红黑树的性能

AVL 树和红黑树都是平衡二叉树的两个老大哥,他们增删查改的时间复杂度都O(logN),到底谁更技高一筹?

其实在大数据的场景下,比如百万级量化数据,AVL 需要构建大约 20 多层,同时红黑树需要构建大约 40 多层,毕竟红黑树是近似平衡的二叉搜索树。

但是我们知道 20 和 40 在 CPU 运算速度面前并没有什么差别,虽然 AVL 树在效率上会略胜红黑树一点点,但是生活中红黑树的运用却比 AVL 树更为广泛,因为 AVL 树的效率是有代价的,是充分牺牲结构进行不断旋转得到的,而红黑树大大降低了旋转次数会更安全因此,换来了更优的性能

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

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

相关文章

JMS规范和AMQP协议

参考资料&#xff1a;《JMS与AMQP简述以及比较》《AMQP协议详解》《MQ消息队列的JMS规范和AMQP协议的区别》《消息队列之JMS和AMQP对比》写在开头&#xff1a;本文为学习后的总结&#xff0c;可能有不到位的地方&#xff0c;错误的地方&#xff0c;欢迎各位指正。一般情况下MQ的…

【数据结构与算法——C语言版】6. 排序算法(3)——插入排序

前言 在本系列的上两篇文章分别介绍了两种O(n2)的排序算法——选择排序和冒泡排序&#xff0c;今天是第三种O(n2)的排序算法&#xff1a;插入排序。 插入排序 核心思想 它的基本思想是将一个记录插入到已经排好序的有序表中&#xff0c;从而产生一个新的、记录数增 1 的有序…

软件测试~自动化测试Seleniums---1

一.什么是自动化测试 1.自动化测试介绍 自动化测试指软件测试的自动化&#xff0c;在预设状态下运行应用程序或者系统&#xff0c;预设条件包括正常和异常&#xff0c;最后评估运行结果。将人为驱动的测试行为转化为机器执行的过程。 将测试人员双手解放&#xff0c;将部分测…

机器视觉(十):印刷体字符识别

目录&#xff1a; 机器视觉&#xff08;一&#xff09;&#xff1a;概述 机器视觉&#xff08;二&#xff09;&#xff1a;机器视觉硬件技术 机器视觉&#xff08;三&#xff09;&#xff1a;摄像机标定技术 机器视觉&#xff08;四&#xff09;&#xff1a;空域图像增强 …

Unreal模块创建流程

可以把开发中通用的功能封装成模块,以在不同项目之间复用,这里记录一下创建模块的步骤:在工程的Source文件夹中新建文件夹,命名为模块名称TestCustomModule:如果要区分模块内脚本的公私有权限,则在模块文件夹内创建Public和Private文件夹,这里我没有区分,就不创建了:在模块文件…

Js如何实现一个累加向上漂浮动画

前言 在不久之前,看到一个比较有意思的小程序,就是静神木鱼,可以实现在线敲木鱼,自动敲木鱼,手盘佛珠,静心颂钵的 整个小程序功能比较小巧,大道至简,曾风靡过一阵的,无论在App应用市场上,还是小程序里,一些开发者都赚得盆满钵满,用于缓解当代年轻人的一个焦虑,佛系解压,算是一…

Kubernetes:通过轻量化工具 kubespy 实时观察YAML资源变更

写在前面 分享一个小工具 kubespy 给小伙伴博文内容涉及&#xff1a; 工具的简单介绍下载安装以 kubectl 插件方式使用 Demo 理解不足小伙伴帮忙指正 我所渴求的&#xff0c;無非是將心中脫穎語出的本性付諸生活&#xff0c;為何竟如此艱難呢 ------赫尔曼黑塞《德米安》 简单介…

详解二分查找的两种写法以及二分查找的六种变形

目录 一、二分查找的两种写法 1.1 - 第一种写法&#xff08;左闭右闭&#xff09; 1.2 - 第二种写法&#xff08;左闭右开&#xff09; 二、二分查找的六种变形 2.1 - 查找第一个 target 的元素位置 2.2 - 查找第一个 > target 的元素位置 2.3 - 查找第一个 > ta…

JS类型转换机制

概述 JS中有六种简单数据类型&#xff1a;undefined、null、boolean、string、number、symbol&#xff0c;以及引用类型&#xff1a;object 但是我们在声明的时候只有一种数据类型&#xff0c;只有到运行期间才会确定当前类型let x y ? 1 : a; &#xff0c;x的值在编译阶段…

FPGA基础之内置逻辑门

verilog语言中&#xff0c;针对逻辑门&#xff0c;有许多内置可直接使用的逻辑门&#xff0c;从输入输出数量可分为多输入门和多输出门。 一、多输入门 有单个或多个输入&#xff0c;只有单个输出的逻辑门&#xff0c;包含and(与)&#xff0c;or(或)&#xff0c;xor(异或)&am…

在训练心脏数据集时碰到的问题汇总

在训练心脏数据集时碰到的问题汇总&#xff1a; 1.nii数据处理问题 心脏CT数据集采用的是医学图像常用的压缩文件格式nii&#xff0c;且储存的图像为3D图像&#xff0c;不能直接使用。 首先应导入SimpleITK包&#xff0c;利用如下三个函数进行nii格式文件的提取。 sitk.ReadI…

vlan间的通信

vlan之间要通过三层通信实现互访&#xff0c;三层通信需借助三层设备 如果之前配置了 hybrid模式想删除 命令 undo port link-type hybrid vlan all [Huawei-GigabitEthernet0/0/3]dis this interface GigabitEthernet0/0/3 undo port hybrid vlan 1 这里可以理解为多删了一个…

【python】【数据分析】2022年全国大学生数据分析大赛题解-医药电商销售数据分析

文章目录一、前言二、题目三、题解1&#xff0e;对店铺进行分析&#xff0c;一共包含多少家店铺&#xff0c;各店铺的销售额占比如何&#xff1f;给出销售额占比最高的店铺&#xff0c;并分析该店铺的销售情况。2.对所有药品进行分析&#xff0c;一共包含多少个药品&#xff0c…

Promise和async/await

1、回调地狱 多层回调函数的相互嵌套&#xff0c;就形成了回调地狱。示例代码如下&#xff1a; 回调地狱的缺点&#xff1a; 代码耦合性太强&#xff0c;牵一发而动全身&#xff0c;难以维护大量冗余的代码相互嵌套&#xff0c;代码的可读性变差 1.1、如何解决回调地狱的问题…

手把手实现邮件分类 《Getting Started with NLP》chap2:Your first NLP example

《Getting Started with NLP》chap2&#xff1a;Your first NLP example 感觉这本书很适合我这种菜菜,另外下面的笔记还有学习英语的目的&#xff0c;故大多数用英文摘录或总结 文章目录《Getting Started with NLP》chap2&#xff1a;Your first NLP example2.1 Introducing N…

数据结构与算法【树】

二叉树性质 满二叉树 深度为k&#xff0c;有2k−12^{k}-12k−1个结点的二叉树&#xff0c;为满二叉树。 完全二叉树 完全二叉树的定义如下&#xff1a;在完全二叉树中&#xff0c;除了最底层节点可能没填满外&#xff0c;其余每层节点数都达到最大值&#xff0c;并且最下面…

CSDN第22期周赛(记录一下,不是题解)

希望23年能收获一两本程序员杂志 前言 发现一个问题&#xff0c;codeblocks上编译没问题&#xff0c;在CSDN比赛时&#xff0c;会报错&#xff1a; 1&#xff0c;size()和length()属于unsigned int&#xff0c;所以与之比较大小或者赋值的 i, j 也要用unsigned int&#xf…

巧解 JavaScript 中的嵌套替换

网友 wys 提问&#xff1a;如何仅使用 JavaScript 支持的正则语法&#xff0c;将 <p> <table> <p> <p> </table> <table> <p> <p> </table> <p>中<table>...</table>之间的<p>都替换为<b…

C库函数:stdio.h

stdio.h C 标准库 – <stdio.h> | 菜鸟教程 (runoob.com) 下面是头文件 stdio.h 中定义的变量类型&#xff1a; 序号变量 & 描述1size_t 这是无符号整数类型&#xff0c;它是 sizeof 关键字的结果。2FILE 这是一个适合存储文件流信息的对象类型。3fpos_t 这是一个适…

组件的生命周期

一、组件的生命周期 1、组件的生命周期&#xff1a;至一个组件从 创建——>运行——>销毁的过程 2、声明周期函数&#xff1a;由Vue提供的内置函数&#xff0c;伴随组件生命周期按次序自动运行——>钩子函数 3、生命周期的阶段划分 &#xff08;1&#xff09;创建…