【数据结构取经之路】图解红黑树

news2024/11/15 13:43:39

目录

前言

红黑树的概念

红黑树的性质

红黑树结点的定义

左右旋动图

红黑树的插入分析

红黑树的插入代码

红黑树与AVL-tree比较

红黑树的应用场景


前言

AVL-tree之外,另一个颇具历史且被广泛使用的平衡二叉搜索树是红黑树(RB-tree),这名字听起来除了很抽象外,还颇具大哥味哈,会不会很难?客观地说,难度是有的,下面我们细细道来。

红黑树的概念

红黑树(Red Black Tree)本质上就是一棵平衡二叉搜索树。这种数据结构通过增加节点颜色的维度(红色或黑色),并在插入、删除等操作中执行特定的旋转和重新着色操作,以保持树的平衡,从而确保高效的查找、插入和删除性能。红黑树通过对任何一条从根到叶子结点的路径上各个节点着色方式的限制,确保没有一条路径会比其他路径长出两倍,因而接近平衡。

看到这你是否有这样的疑问:通过着色也可以控制树的平衡?!太不可思议了吧!

当然,仅仅通过着色它还真无法做到平衡,它还在其他方面做了一些限制,下面我们来看看红黑树的性质,就可以揭开这个谜团。

红黑树的性质

只顾着说红黑树,不见识见识总感觉不太好,下面我们正式把红黑树给请上来。

红黑树有以下的几个性质,可以对照着上图验证。

        ● 每个节点不是红色就是黑色。(嘿嘿,我没在开玩笑)

        ● 根结点是黑色的。

        ● 任意一条路径中,没有连续的红色节点。

        ● 每条路径上的黑色结点数量是相同的。

        ● 每个叶子结点都是黑色的(这里特指的是空节点NIL)。

以上就是红黑树的性质了,那么我的问题来了:为什么满足以上的性质,红黑树就能保证其最长路径中结点个数不会超过最短路径结点个数的两倍呢?这里我就不卖关子了,直接分析。

全由黑色节点组成的路径就是最短路径。最长路径就是在各个黑色结点间插入红色结点。根据上面的性质,不能有连续的红色结点,所以每两个黑色结点间只能插入一个红色结点,故最长路径不会超过最短路径的两倍。有图有真相,请看:

根据上面的性质,根节点是黑色的,所以不管是最长路径还是最短路径的开端都是黑色结点。同时,不管是最长路径还是最短路径,每条路径上的黑色结点数量相等。关于上面的问题,就解释到这。

红黑树结点的定义

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

//红黑树结点的定义
template <class T>
struct RBTreeNode
{
	RBTreeNode<T>* _left;  //结点的左孩子
	RBTreeNode<T>* _right; //结点的右孩子
	RBTreeNode<T>* _parent;//结点的父节点
	Color _color;		   //结点的颜色
	T _data;			   //结点的数据域

	RBTreeNode(const T& data = T(), const Color color = RED)
		:_left(nullptr), _right(nullptr), _parent(nullptr)
		,_color(color), _data(data)
	{}
};

以上就是红黑树节点定义,但有两个问题:为什么需要指向父节点的指针?为什么要将结点的默认颜色设置成黑色?

先来回答第一个问题,在旋转时需要访问父节点,所以引入了指向父节点的指针。

第二个问题,之所以结点的默认颜色设置成黑色,是因为要确保黑色结点平衡(上面的性质4)。我们试想一下,如果插入的新节点默认颜色是黑色的,那么会导致黑色结点失衡,影响到所有路径。

左右旋动图

为了帮助大家理解左旋和右旋,这里给出两张动图,希望对你有帮助。

左旋:

右旋:

 

红黑树的插入分析

相对于AVL-tree来说,红黑树比较抽象。因为AVL-tree是通过直接控制高度来维持平衡的,这一点我们都很好理解。而红黑树呢,它是通过颜色控制来间接维持平衡的,要是不了解,我们就很有可能有这样的疑问:颜色和高度有什么关系呢?!疯了?!

虽然红黑树抽象,但不至于抽象到理解不了的地步,下面我在讲解的时候也会插入一些图片来供大家参考学习。所以,我们开始吧!

红黑树,本质上也属于二叉搜索树,所以它在插入时也要先找到自己的落脚点才能插入。所以我们可以高度的概括为:

        ● 按照二叉搜索树的方式插入新节点。

        ● 检测新节点插入后是否破坏了红黑树的规则,若满足则大功告成,否则就对红黑树进行调整。

在开始下面内容之前,我们先在命名上做个约定:cur——当前结点,p——父节点,g——祖父结点,u——叔叔节点,gg——祖父结点的父节点(g的父节点)。

这张图是为了让大家理解上面声明的内容,理解它们之间的关系,请忽略颜色。

下面我们进入本文最头疼的部分,插入的情形分类,这里既要细心又要耐心。

①cur为红,p为红,g为黑,u存在且为红(如下图)。 

首先说明一下gg这一节点,它可能存在,也可能不存在。如果gg存在,颜色可能是黑色也可能是红色。因为我们这里所看到的树,可能是一棵完整的树,即g为整棵树的根节点,也可能是一棵子树,即g为gg结点的左子树或者右子树。 这里要好好理解。对于这种情况,很显然已经违背了红黑树的性质——任何路径中都不能有连续的红节点。那么我们如何调整呢?

解决方案:把p和u的颜色设为黑,g的颜色置为红(如下图)。②②

这就好啦?!未必!注意啦,这里将要分出3中情况。

a. 如果gg的颜色为黑(如下图),那么万事大吉。

b. 如果gg的颜色为红(如下图),那么就出现了连续的红节点,需要继续向上调整。 

此处的变化是把g当成cur,然后继续向上调整。继续向上调整的情形就是我们后面所要讲到的,在此处我们只需要知道在这种情况下还需要向上调整即可,切记不可恋战,否则容易走火入魔。这种情况会转化为②的a分支所对应的两种情况(u存在且为黑或u不存在,u不可能是存在且为红,因为这会导致原来的树不满足红黑树的规则)。

c. 如果gg结点不存在,也就是说g就是整棵树的根节点(如下图),我们只需要将根节点设为黑色即可——根节点是黑色的。

② cur为红,p为红,g为黑,u不存在或者存在且为黑(如下图)。

这是我们的第二大类,老铁们可不能被上面列举的一堆情形给整懵了呀。

当u不存在时,cur一定是新增节点。如果cur不是新增节点,那么原有的红黑树就存在连续的红节点,违反了规则。然而我们要确保在插入新节点前为红黑树,所以cur只能是新增节点。

当u存在且为黑时(u存在且为红的情况我们前面已经讨论过了),cur原来一定是黑色的。也就是说cur是被它的子树在向上继续调整时改为红色的。请看到①的b分支,就是①的b分支这种情况转化而来的

在u不存在或者存在且为黑的情况下,就需要旋转了。旋转又涉及到单旋和双旋,所以注意啦,我又要从这里开始进行分岔了。

a. p为g的左孩子,cur为p的左孩子(如下图),则进行右旋,旋转后,p变为黑色,g变为红色,over。

可以看到,u结点存在且为黑和u结点不存在的处理方案是一样的,所以把它们归为了一类。

b. p为g的左孩子,cur为p的右孩子(如下图)。

不论u不存在还是u存在且为黑,在这种情况下都需要双旋。

        ● 对以p为根的子树进行左旋。

        ● 重新调整父子关系。

        ● 对以g为根结点的树进行右旋。

        ● 重新调整父子关系。

做完了这些操作,还没结束哟。你看第二张图,是不是还有连续的红节点。但先别慌,其实这种情况我们已经有处理方案了或者说已经处理过了,请看②的a分支,第二张图旋转后的结果转变成了②的a分支中的情况。到这,我们也算处理完成了,因为在循环处理的过程中,这种情况会匹配到②的a分支的处理方案。如果对这句话感到云里雾里的,没关系,后面看代码就明白了。

有人也许会有这样的疑问:为什么所有情况都假设p为红,cur为红?cur为新插入节点或者是被调整的结点,所以颜色是红色。如果p为黑,就不需要我们在绞尽脑汁的去调整了。只有cur为红,p也为红才要调整,所以所有情况下都假设cur为红,p为红。

所有情况(只需要改变颜色、不需要旋转,单旋,双旋)都已经讲完了,我知道,到这我们该总结了,各种情况之间的相互转换,确实让我们摸不着头脑,下面我画张思维导图帮助大家理解我的分类情况。

红黑树的插入代码

#pragma once
#include <iostream>
using namespace std;

enum Color{RED, BLACK};

template <class T>
struct RBTreeNode
{
	RBTreeNode<T>* _left;
	RBTreeNode<T>* _right;
	RBTreeNode<T>* _parent;
	Color _color;
	T _data;

	RBTreeNode(const T& data = T(), const Color color = RED)
		:_left(nullptr), _right(nullptr), _parent(nullptr)
		,_color(color), _data(data)
	{}
};

template <class T>
class RBTree
{
	typedef RBTreeNode<T> Node;
public:
	bool insert(const T& data)
	{
		//第一次插入
		if (_root == nullptr)
		{
			_root = new Node(data);
			_root->_color = BLACK;
			return true;
		}

		Node* cur = _root;
		Node* parent = nullptr;

		//找到插入的落脚点
		while (cur)
		{
			if (cur->_data < data)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_data > data)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}

		//插入
		cur = new Node(data);
		cur->_parent = parent;

	
		if (parent->_data > data)
			parent->_left = cur;
		else
			parent->_right = cur;

		//检查是否破坏红黑树的规则
		while (parent && parent->_color == RED)
		{
			//parent的父节点一定存在,因为根节点是黑色的
			Node* grandfather = parent->_parent;

			//先讨论左边
			if (grandfather->_left == parent)
			{
				Node* unclue = grandfather->_right;

				if (unclue && unclue->_color == RED)
				{	
					//u存在且为红
					parent->_color = BLACK;
					unclue->_color = BLACK;
					grandfather->_color = RED;
					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					//u存在且为黑或u不存在——旋转
					if (cur == parent->_left)
					{
						//右单旋
						RotateRight(grandfather);
					}
					else
					{
						//左旋
						RotateLeft(parent);
						//右旋
						RotateRight(grandfather);

						//注意上面左旋和右旋是处理以谁为根的子树
					}

					//调整颜色
					parent->_color = BLACK;
					grandfather->_color = RED;
					//旋转结束后绝对符合红黑树的规则了,就该结束了
					//除非旋转错误
					break;
				}
			}
			else
			{
				Node* unclue = grandfather->_left;

				if (unclue && unclue->_color == RED)
				{
					//u存在且为红
					parent->_color = BLACK;
					unclue->_color = BLACK;
					grandfather->_color = RED;
					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					//u存在且为黑或u不存在——旋转
					if (parent->_right == cur)
					{
						//左旋
						RotateLeft(grandfather);
					}
					else
					{
						//右旋
						RotateRight(parent);
						//左旋
						RotateLeft(grandfather);

						//注意上面左旋和右旋是处理以谁为根的子树
					}

					parent->_color = BLACK;
					grandfather->_color = RED;
					break;
				}
			}
		}
		//确保根节点是黑色的
		_root->_color = BLACK;
		return true;
	}

	void inorder() { _inorder(_root); }
private:
	void RotateLeft(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;

		subR->_left = parent;
		Node* parentParent = parent->_parent;
		parent->_parent = subR;

		subR->_parent = parentParent;

		if (parentParent)
		{
			if (parentParent->_left == parent)
				parentParent->_left = subR;
			else
				parentParent->_right = subR;
		}
		else
		{
			_root = subR;
			subR->_color = BLACK;
			subR->_parent = nullptr;
		}
	}

	void RotateRight(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;

		subL->_right = parent;
		Node* parentParent = parent->_parent;
		parent->_parent = subL;

		subL->_parent = parentParent;

		if (parentParent)
		{
			if (parentParent->_left == parent)
				parentParent->_left = subL;
			else
				parentParent->_right = subL;
		}
		else
		{
			_root = subL;
			subL->_color = BLACK;
			subL->_parent = nullptr;
		}
	}

	void _inorder(Node* root)
	{
		if (root == nullptr)
			return;
		_inorder(root->_left);
		cout << root->_data << " ";
		_inorder(root->_right);
	}
private:
	Node* _root = nullptr;
};


#include "RBTree.h"

int main()
{
	int arr[] = { 1, 6, 12, 17, 3, 2, 18, 9, 10 };
	RBTree<int> rb;
	for (auto val : arr)
	{
		rb.insert(val);
	}
	rb.inorder();
	return 0;
}

红黑树与AVL-tree比较

由于AVL树追求严格的平衡,所以在插入和删除数据时需要经过多次旋转来恢复平衡。而红黑树的平衡条件比较宽松,只需保证最长路径不超过最短路径的两倍即可,所以相对而言,红黑树在插入和删除操作中旋转次数少一些。因而在经常需要进行增删的场景下,红黑树的性能更优一些。

红黑树平衡条件较宽松,AVL树追求严格的平衡,相同数据量情况下,AVL树的高度相对比较的低,搜索的效率理论上要比红黑树的高,但实践表明,在大多数情况下这种优势并不明显。

如果搜索的需求远高于插入和删除,那么选择AVL树可能会更好一些。

红黑树的应用场景

C++STL库中就用到了红黑树,除此之外,数据库索引、文件系统的目录结构以及操作系统的任务调度等领域中都可以看到红黑树的身影。


本文到这就结束啦~感谢支持! 

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

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

相关文章

码头童话,“丈量”行业数智化转型

作者 | 曾响铃 文 | 响铃说 一箱车厘子从地球正对的另一边远渡重洋来到中国&#xff0c;而一旦到达&#xff0c;5个小时内它就能变成北京、天津、河北、河南等区域老百姓果盘里的美味。 这一幕&#xff0c;来自央视联合华为制作发布的《新智中国说-谈智一会间》第一期“码头…

PHP邮件发送教程:如何用PHP发送电子邮件?

php邮件怎么实现发送电子邮件&#xff1f;php怎么给邮箱发邮件&#xff1f; PHP作为一种广泛使用的服务器端脚本语言&#xff0c;提供了多种方法来实现电子邮件的发送。AokSend将详细介绍如何使用PHP邮件功能来发送电子邮件&#xff0c;帮助开发者轻松实现这一重要功能。 PHP…

30道常见的软件测试面试题(含答案+文档)

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 1、什么项目适合做自动化测试&#xff1f; 关键字&#xff1a;不变的、重复的、规范的 1&#xff09;任务测试明确&#xff0c;需求不会频繁变动 2&#xff09;项…

vulnhub靶场 DC-3

地址: https://download.vulnhub.com/dc/DC-3-2.zip 开启NAT模式 namp只扫到了一个端口 打开网页有一个登录的页面 目录扫描一下,可以找到一个 后台登录界面 看一下指纹信息 joomla cms 网上搜一下可以发现存在一个JoomScan工具 在kali上面安装一下 apt install joomscan …

4. 密码协议

4. 密码协议 (1) 协议的基本概念 协议是一种在两个或多个参与者之间进行通信的规范,它定义了参与者之间的交互方式、消息格式和通信过程。协议的目的是确保通信的可靠性和安全性,防止信息被篡改、伪造或泄露。 (2) 密码协议分类及基本密码协议 密码协议是用于加密和解密数…

【Python报错已解决】ModuleNotFoundError: No module named ‘tensorflow‘

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 专栏介绍 在软件开发和日常使用中&#xff0c;BUG是不可避免的。本专栏致力于为广大开发者和技术爱好者提供一个关于BUG解决的经…

浅谈“流量回放”

一、流量回放定义 “流量回放”通常指的是将之前记录的网络请求和响应数据重新发送到系统中&#xff0c;以模拟真实用户的操作。这种技术主要用于测试和调试目的&#xff0c;帮助开发人员和测试人员更好地理解系统在特定条件下的行为。 二、流量回放的技术原理&#xff1a; 数…

前端动画库大比拼:为何选择Velocity.js

前端动画库大比拼&#xff1a;为何选择Velocity.js 前言 在现代网页设计中&#xff0c;动画效果是提升用户体验的重要手段。 Velocity.js: 一个与 jQuery 动画 API 兼容的动画引擎&#xff0c;以其卓越的性能和丰富的功能&#xff0c;成为了开发者的好工具。 本文将详细介绍…

大数据Flink(一百二十二):阿里云Flink MySQL连接器介绍

文章目录 阿里云Flink MySQL连接器介绍 一、特色功能 二、​​​​​​​语法结构 三、​​​​​​​​​​​​​​WITH参数 阿里云Flink MySQL连接器介绍 阿里云提供了MySQL连接器&#xff0c;其作为源表时&#xff0c;扮演的就是flink cdc的角色。 一、特色功能 MySQ…

【Qt笔记】QToolBox控件详解

目录 引言 一、QToolBox的基本功能 1.1 分页布局 1.2 可点击标签 1.3 图标支持 1.4 信号与槽 二、 QToolBox的属性设置 2.1 设置指定索引位置可用性 2.2 设置指定索引位置图标 2.3 设置标题 2.4 设置提示信息 2.5 获取信息 三、QToolBox的常用API 3.1 构造函数…

前端vue-单选按钮的实现

要把name“sex”和value"男" 和 要把name“sex”和value"女"写上&#xff0c;然后在各自的标签内部写上v-model绑定属性。data中定义v-model的绑定值&#xff0c;后面的值是默认选中的男或者女性。

Google Play金融类应用上了又被下,怎么搞定设备短信权限问题?

不久前谷歌对金融类产品应用更新了政策要求&#xff0c;即8月31日起新注册的开发者账号必须得注册为企业账号才可以上架金融类产品应用&#xff0c;这对原本就是用企业号的开发团队或公司没什么影响&#xff0c;但如果用的是个人号得做点准备了。 可以看出&#xff0c;谷歌对金…

剑灵服务端源码(c#版本+数据库+配套客户端+服务端)

剑灵服务端源码&#xff0c;喜欢的下载研究研究。谁技术牛B的话&#xff0c;能把最新版本的客户端接上&#xff0c;就好了。 剑灵服务端源码&#xff08;c#版本数据库配套客户端服务端&#xff09; 下载地址&#xff1a; 通过网盘分享的文件&#xff1a;【源码】剑灵服务端源码…

利士策分享,自我和解:通往赚钱与内心富足的和谐之道

利士策分享&#xff0c;自我和解&#xff1a;通往赚钱与内心富足的和谐之道 在这个快节奏、高压力的时代&#xff0c;我们往往在追求物质财富的同时&#xff0c;忽略了内心世界的和谐与平衡。 赚钱&#xff0c;作为现代生活中不可或缺的一部分&#xff0c;它不仅仅是生存的手段…

YOLOv8改进 - 注意力篇 - 引入ECA注意力机制

一、本文介绍 作为入门性第一篇&#xff0c;这里介绍了ECA注意力在YOLOv8中的使用。包含ECA原理分析&#xff0c;ECA的代码、ECA的使用方法、以及添加以后的yaml文件及运行记录。 二、ECA原理分析 ECA官方论文地址&#xff1a;ECA文章 ECA的pytorch版代码&#xff1a;ECA的…

C++第十一节课 new和delete

一、new和delete操作自定义类型 new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间还会调用构造函数和析构函数&#xff08;new会自动调用构造函数&#xff1b;delete会调用析构函数&#xff09; class A { public:A(int a 0): _a(a){cout <&l…

systemd学习

传统init进程启动流程 kernel内核代码init/main.c&#xff0c;内核启动init进程过程&#xff1a; init进程是由内核启动的第一个&#xff08;也是唯一的一个&#xff09;用户进程&#xff08;进程id为1&#xff09;&#xff0c;它根据配置文件决定启动哪些程序&#xff0c;ini…

mybatisplus的多记录操作 批量删除和批量查询

1.批量查询 通过in查询 Testpublic void testBatchSelectByIds(){List<Integer> ids Arrays.asList(5,7);List<User> users userMapper.selectBatchIds(ids);log.info(users);} 2.批量删除 Testpublic void testBatchDelete(){List<Integer> ids Arrays…

LLM - 理解 多模态大语言模型(MLLM) 的 指令微调(Instruction-Tuning) 与相关技术 (四)

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/142237871 免责声明&#xff1a;本文来源于个人知识与公开资料&#xff0c;仅用于学术交流&#xff0c;欢迎讨论&#xff0c;不支持转载。 完备(F…

Java知识点小结3:内存回收

文章目录 对象引用强引用软引用&#xff08;SoftReference&#xff09;弱引用&#xff08;WeakReference&#xff09;考一考 虚引用&#xff08;PhantomReference&#xff09;总结 垃圾回收新生代老年代永生代 内存管理小技巧尽量使用直接量使用StringBuilder和StringBuffer进行…