【手撕红黑树】

news2024/11/27 22:42:11

前言

相信很多人初学者听到了红黑树后心中不免有些心慌,那你看到了这篇文章后相信会有所收获,我其实刚开始也是对红黑树抱着一种害怕甚至是恐惧,但是在老师的帮助下也终于慢慢的不在恐惧了,你想知道为什么的话就继续往下看吧。(注意本篇博客只讲解了红黑树的插入,没有讲解红黑树的删除,删除比插入还要难一些,为了更好的阅读体验,就不再讲解了)


1 红黑树的概念

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

如果说AVL树是大佬设计的话,那么红黑树就是大佬中的大佬设计出来的,为什么这么说呢,我们接下来慢慢看。


2 红黑树的性质

  • 每个结点不是红色就是黑色
  • 根节点是黑色的
  • 如果一个节点是红色的,则它的两个孩子结点是黑色的
  • 对于每个结点,从该结点到其所有后代叶结点的简单路径上均包含相同数目的黑色结点
  • 每个叶子结点都是黑色的 (此处的叶子结点指的是空结点)

我们先不要看最后一条性质,其他性质中比较重要的就是性质三和性质四,我们可以用自己的话来解读性质三和性质四:
性质三的意思是没有连续红色结点
性质四的意思是每条路径下的黑色结点数量是相等的

大家思考一下,为什么满足了上面性质就能够保证红黑树中最长路径中节点个数不会超过最短路径节点个数的两倍

我们可以从极限的条件下来判断:
最短路径是全黑,最长路径是红黑相间,由于要满足性质三和性质四所以最长路径除以最短路径最大也不会超过二倍。

在这里插入图片描述
我们再来看看最后一个性质,有些教科书上可能会有NIL结点的定义,并且把颜色定义为黑色,注意这里的NIL结点并不是一个真正有效的节点,而是一个空结点。通过每条空结点来标识每一条路径,如在上图中就存在着11条路径。

通过红黑树的性质我们也不难发现,其实红黑树的平衡并没有AVL树那么严格,因为红黑树只需要保证最长路径的结点个数不会超过最短路径节点个数两倍即可,但是AVL树要求着所有子树高度差绝对值不超过1。这就导致了红黑树的旋转条件是比AVL树更加苛刻的,也就是在同等条件下红黑树旋转次数是有较大机率低于AVL树的,那么红黑树的性能肯定是比AVL要好上一些的(旋转是有代价的),如果还没有了解过旋转,建议先看看博主的上一篇博客:
[AVL树的旋转]


3 红黑树的模拟实现

3.1 节点的定义

这个很简单,我们已经讲解过很多次了:

#pragma once


enum Corlor
{
	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;
	Corlor _corlor;

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

template<class K, class V>
class RBTree
{
	typedef RBTreeNode<K, V> Node;
private:
	Node* _root = nullptr;
};

这里问题就来了,新节点给的颜色是给红色还是给黑色呢?
给红色的话就可能违背性质三,给黑色结点就违背性质四。如果是你你想违背哪个性质?
这里给新节点的默认颜色为红色比较好,为什么呢?
给出红色结点的话,我们可能就只需要调整本路径下结点颜色,但是给黑色结点的话其他路径黑色结点就不相等了,调整的代价肯定更大。所以新节点的默认颜色给红色是比较合理的。

在这里插入图片描述我们观察上图,如果我们在11 或者15 下插入新节点,那么这就太好了,不需要进一步调整,插入后还是一颗红黑树,但是我们想要在6 22 27下面插入新节点的话,就要调整了,具体怎样调整我们下面会详细讲解。

3.2 分类

约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点
红黑树调整分类的关键就是看叔叔

3.2.1 情况一

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

在这里插入图片描述
老规矩,我们这里画的仍然是抽象图,表示无数种情况,但是他们都可以用同一种方法来解决。
此时我们只需要将p和u置黑,将g置红,然后接着往上调整
为什么要继续往上调整呢?通过观察我们可以很容易分析出问题所在,这样一次调整了后各个路径上黑色结点数目并没有发生改变,但是有可能g结点的父亲结点是红色的,而导致又出现了连续红色结点(注意,调整了后有可能再次调整使用的方法不在是第一种情况)
在这里插入图片描述继续向上调整的话,直到不满足连续红色结点或者已经调整到了根节点。

3.2.2 情况二

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

在这里插入图片描述
我们先分析第一种情况:u不存在。
如果u不存在的话,那么cur一定是新增。此时光变色已经无法解决问题了,因为此时已经不满足最长路径中节点个数不会超过最短路径节点个数的两倍,就需要旋转处理:
在这里插入图片描述这里处理方式是:右单旋+p变黑,g变红。
也有可能p和cur都在右边且在一条直线上,所以处理方式可能是:左单旋+p变黑,g变红。
总结本次调整方法为:单旋+p变黑,g变红

同理,当u存在且为黑的时候仍然是同种处理方式:
在这里插入图片描述旋转变色完以后就不用再向上更新了

3.2.3 情况三

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

等等,这种方式不就是第二种方式吗?
别着急,我们先来看看图:
在这里插入图片描述
从图片中我们不难发现,g p cur 三者并没有在一条直线,而是一种折线关系,这种情况我们只是单旋就处理不了了,在这张图中我们先以p点进行左单旋,在以g点进行右单旋上。
(注意,我们在b和c下面插入结点导致引起上面这种关系的都可以用这种方式处理)
在这里插入图片描述
在这里插入图片描述当然,不只是这一种情况,还可能是反着来,那么处理方式就可能是:
以p右单旋+以g左单旋+cur变黑,g变红。
所以这次情况处理方式是:双旋+cur变黑,g变红
旋转变色完以后就不用再向上更新了

3.3 代码实现

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

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

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

			if (grand->_left == parent)
			{
				Node* uncle = grand->_right;
				//情况一:uncle存在且为红
				if (uncle && uncle->_corlor == RED)
				{
					uncle->_corlor = parent->_corlor = BLACK;
					grand->_corlor = RED;
					//继续往上处理
					cur = grand;
					parent = cur->_parent;
				}
				else
				{
					//情况二和三:
					//1 :parent->_left==cur(右单旋+变色)
					//      g
					//   p     u
					//c
					if (parent->_left == cur)
					{
						RotateR(grand);
						parent->_corlor = BLACK;
						grand->_corlor = RED;
					}
					else
					{
						//2 :parent->_right==cur(左单旋+右单旋+变色)
						//      g
						//   p     u
						//      c
						RotateL(parent);
						RotateR(grand);
						cur->_corlor = BLACK;
						grand->_corlor = RED;

					}

					//此时已经旋转变色完成,可以break出去
					break;
				}
			}
			else//grand->_right=parent
			{
				Node* uncle = grand->_left;
				//情况一:uncle存在且为红
				if (uncle && uncle->_corlor == RED)
				{
					uncle->_corlor = parent->_corlor = BLACK;
					grand->_corlor = RED;
					//继续往上处理
					cur = grand;
					parent = cur->_parent;
				}
				else
				{
					//情况二和三:
					//1 :parent->_right==cur(左单旋+变色)
					//      g
					//   u     p
					//            c
					if (parent->_right == cur)
					{
						RotateL(grand);
						parent->_corlor = BLACK;
						grand->_corlor = RED;
					}
					else
					{
						//2 :parent->_left==cur(右单旋+左单旋+变色)
						//      g
						//   u     p
						//      c
						RotateR(parent);
						RotateL(grand);
						cur->_corlor = BLACK;
						grand->_corlor = RED;
					}

					//此时已经旋转变色完成,可以break出去
					break;
				}
			}

		}
		_root->_corlor = BLACK;
		return true;
	}

3.4 红黑树的验证

验证的话我们要从红黑树的性质开始着手,只要满足了红黑树的几个性质自然就没啥问题.

最主要的验证是性质三和性质四:

//bool prevCheck(Node* root, int blackCnt, int& benchmark)
	bool prevCheck(Node* root, int blackCnt, int benchmark)
	{
		if (root == nullptr)
		{
			/*if (benchmark == 0)
			{
				benchmark=blackCnt;
				return true;
			}*/

			if (benchmark != blackCnt)
			{
				cout << "每条路径上的黑色结点不相等" << endl;
				return false;
			}
			else
			{
				return true;
			}
		}

		if (root->_corlor == BLACK)
		{
			++blackCnt;
		}

		if (root->_corlor == RED && root->_parent->_corlor == RED)
		{
			cout << "有连续的红色结点" << endl;
			return false;
		}

		return prevCheck(root->_left, blackCnt, benchmark)
			&& prevCheck(root->_right, blackCnt, benchmark);
	}

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

		int benchMark = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_corlor == BLACK)
				++benchMark;
			cur = cur->_left;
		}
		int blackCnt = 0;
		return prevCheck(_root, blackCnt, benchMark);
	}

处理方式有很多种,像每条路径下的黑色节点我们可以一次性先算出来然后传参数即可,也可以不算出来,传参数引用来修改。具体方式大家可以自行选择。
大家测试时最好多用几组随机数测测。


5 总结

大家看到了这里相信也对红黑树有了一个谱,其实说难吧,感觉还没有刚学AVL的旋转难,关键是如何把图画好,跟着图一步一步的来,大概率是不会出错的。如果该文对你有帮助的话能不能一键三联支持博主呢

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

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

相关文章

【C,C++】内存管理new和delete

内存管理 前言正式开始几道热身题C语言动态内存管理方式C内存管理new/delete操作内置类型new和delete对于内置类型new开辟失败 operator new与operator delete函数new和delete的实现原理内置类型自定义类型 定位new表达式面试常考&#xff1a;malloc/free和new/delete的区别 前…

板子短路了?

有段时间没更新了&#xff0c;主要是最近有点忙&#xff0c;当然也因为有点“懒”。 做这行业的都知道&#xff0c;下半年都是比较忙的&#xff0c;相信大家也是&#xff01; 相信做硬件的小伙伴们&#xff0c;遇到过短路的板子已经不计其数了。 短路带来的危害&#xff1a;…

关于单目视觉 SLAM 的空间感知定位技术的讨论

尝试关于单目视觉 SLAM 的空间感知定位技术的学习&#xff0c;做以调查。SLAM算法最早在机器人领域中提出&#xff0c;视觉SLAM又可以分为单目、双目和深度相机三种传感器模式&#xff0c;在AR应用中通常使用轻便、价格低廉的单目相机设备。仅使用一个摄像头作为传感器完成同步…

Web基础 ( 四 ) JavaScript 介绍

4.JavaScript 4.1.概念 4.1.1.什么是JavaScript 通过浏览器中内置的解析器&#xff0c;逐行解析执行的一种脚本语言 主要是处理系统使用者的行为逻辑的 4.1.2.与Java语言的比较 代码格式不同 ​ Java与HTML无关的格式 ​ JavaScript代码是一种文本字符格式&#xff0c;可…

chatgpt赋能Python-numpy归一化函数

介绍&#xff1a;numpy归一化函数 在数据处理和分析中&#xff0c;常常需要将数据归一化到一定范围内&#xff0c;以便于不同数据之间进行比较和处理。在Python的数据科学方面&#xff0c;numpy库是非常常用的工具之一&#xff0c;其中的归一化函数非常便捷和有效。 在这篇文…

如何快速入门 Java?

在一线互联网公司做开发 13 年了&#xff0c;“精通”Java&#xff0c;“吊打”一众面试官&#xff0c;如何快速入门 Java&#xff0c;对我来说简直就是小儿科&#xff0c;相信看完后你一定能收获满满、醍醐灌顶&#xff0c;今年秋招拿下阿里、美团等互联网大厂的 offer。 逼装…

django ORM框架 第二章 表与表的关系关联表

目录 一、表的几种关联关系 1.1 一对一 1、介绍&#xff1a; 2、举例 3、建表原则&#xff1a; 4、django ORM 框架实现 一对一 的表的创建 1.2 一对多 1、介绍&#xff1a; 2、举例 3、建表原则&#xff1a; 4、django ORM 框架实现 一对多 的表的创建 1.3 多对多 1…

汇编八、汇编控制静态数码管显示数字

1、实现目标 通过汇编语言&#xff0c;实现单个静态数码管依次循环显示0~9。 2、数码管 2.1、数码管外观 2.2、数码管工作原理 (1)数码管的亮灭是由内部LED的亮灭实现的。 (2)一位数码管内部有八颗LED灯&#xff0c;利用内部的LED灯的亮和灭让数码管显示不同的数字。 3、…

chatgpt赋能Python-mac怎么用python

Mac如何使用Python&#xff1a;从入门到实践 简介 Mac操作系统上的Python开发环境非常受欢迎&#xff0c;因为它是一种优雅的编程语言&#xff0c;具有良好的可读性&#xff0c;可以轻松处理不同类型的任务&#xff0c;包括网站开发、机器学习和数据分析等领域。本文将介绍如…

干外包3年,彻底寄了...

先说一下自己的情况&#xff0c;大专生&#xff0c;18年通过校招进入湖南某软件公司&#xff0c;干了接近6年的功能测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了6年的功能测试&…

瑞吉外卖 - 删除分类功能(13)

某马瑞吉外卖单体架构项目完整开发文档&#xff0c;基于 Spring Boot 2.7.11 JDK 11。预计 5 月 20 日前更新完成&#xff0c;有需要的胖友记得一键三连&#xff0c;关注主页 “瑞吉外卖” 专栏获取最新文章。 相关资料&#xff1a;https://pan.baidu.com/s/1rO1Vytcp67mcw-PD…

chatgpt赋能Python-minhash_python

MinHash Python算法&#xff1a;优化大数据处理和搜索引擎 在如今互联网化和其他技术转型的时代&#xff0c;SEO已经成为许多企业和个人的必要条件。SEO方法(搜索引擎优化)一直在不断的发展&#xff0c;MinHash算法是其中之一。本篇文章将会介绍MinHash算法和它在Python中的实…

万金油表示真干不过,部门新来的00后测试员已把我卷崩溃,想离职了...

在程序员职场上&#xff0c;什么样的人最让人反感呢? 是技术不好的人吗?并不是。技术不好的同事&#xff0c;我们可以帮他。 是技术太强的人吗?也不是。技术很强的同事&#xff0c;可遇不可求&#xff0c;向他学习还来不及呢。 真正让人反感的&#xff0c;是技术平平&#x…

chatgpt赋能Python-numpy_分割

Numpy 分割&#xff1a;简介与应用 什么是 Numpy 分割&#xff1f; Numpy 是一种基于 Python 的科学计算库&#xff0c;它提供了对多维数组的支持。其中&#xff0c;分割是 Numpy 中一个非常重要的操作&#xff0c;它允许我们将一个数组沿着指定的轴切分成多个子数组&#xf…

synchronized 底层原理

synchronized 关键字的底层原理 jdk5 之前 synchronized 是重量级锁&#xff0c;但是jdk6 之后会有一个锁升级的过程 Monitor实现的锁属于重量级锁&#xff0c;你了解过锁升级吗? Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式&#xff0c;分别对应了锁只被一个…

Apache Zeppelin系列教程第六篇——Zengine调用Interpreter原理分析

Apache Zeppelin系列教程第五篇——Interpreter原理分析_诸葛子房_的博客-CSDN博客 Apache Zeppelin系列教程第四篇——JDBCInterpreter原理分析_诸葛子房_的博客-CSDN博客 前文介绍jdbc interpreter和interpreter模块交互代码&#xff0c;本篇文章主要分析Zengine调用Interp…

智能的本质人工智能与机器人领域的64个大问题阅读笔记(三)

目录 机器智能提高到人类的水平或者人类智能下降到机器的水平&#xff0c;都可以到达图灵点。 或许图灵测试是一个自我实现的预言&#xff1a;我们&#xff08;声称&#xff09;在打造“聪明”机器的同时&#xff0c;我们也在把人变笨。 不长脑的机器和不思考的人没什么两样&…

工作利器:三种简单方法将PPT转换成PDF

PDF是一种常用的文件格式&#xff0c;适合数据传输和阅读。在工作中&#xff0c;有时我们需要将PPT文件转换为PDF格式以方便使用。下面是几种将PPT转换为PDF的方法&#xff0c;其中方法二将修改为使用记灵在线工具进行转换。 方法一&#xff1a;直接将文件导出为PPT 一般来说…

OpenHarmony3.1安全子系统-签名系统分析

介绍 应用签名系统主要负责鸿蒙hap应用包的签名完整性校验&#xff0c;以及应用来源识别等功能。 子系统间接口&#xff1a; 应用完整性校验模块给其他模块提供的接口&#xff1b;完整性校验&#xff1a; 通过验签&#xff0c;保障应用包完整性&#xff0c;防篡改&#xff1b;…

postman接口自动化测试

Postman除了前面介绍的一些功能&#xff0c;还有其他一些小功能在日常接口测试或许用得上。今天&#xff0c;我们就来盘点一下&#xff0c;如下所示&#xff1a; 1.数据驱动 想要批量执行接口用例&#xff0c;我们一般会将对应的接口用例放在同一个Collection中&#xff0c;然…