【C++进阶】心心念念的红黑树,它来了!

news2024/11/18 13:39:58

在这里插入图片描述

👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨


目录

  • 一、红黑树的概念
  • 二、红黑树的规则总结
  • 三、红黑树的定义
  • 四、新增结点颜色的选择
  • 五、插入分析及代码实现
      • 5.1 前言
      • 5.2 uncle存在且为红
      • 5.3 当uncle不存在
      • 5.4 uncle存在且为黑
  • 六、对插入操作总结一波
      • 6.1 uncle存在且为红
      • 6.2 uncle不存在或uncle存在且为黑
      • 6.3 代码实现
  • 七、验证红黑树
  • 八、红黑树与AVL树的比较
  • 九、代码

一、红黑树的概念

  • 红黑树是除AVL-tree之外,另一个被广泛运用的平衡二叉搜索树
  • 红黑树比AVL-tree还牛逼。这是因为AVL-tree需要严格遵守平衡因子不超过1的规则;而红黑树是 通过颜色(红/黑)的限制,来达到最长路径不超过最短路径的2,因此并不是严格的平衡,而是近似平衡

二、红黑树的规则总结

  1. 每个结点不是红色就是黑色。
  2. 根节点必须是黑色的。
  3. 如果一个节点是红色的,那么它的孩子结点必须是黑色的(说明任何路径不可能存在连续的红色结点
  4. 对于每个结点,从根到空结点NIL,黑色结点的数量相等。
  5. 每个空结点NIL都是黑色的。

需要注意的是,在红黑树中,路径是由根结点到空结点。

根据以上规则,一颗红黑树就诞生了

在这里插入图片描述

上图中,红黑树的路径有11条!

三、红黑树的定义

红黑树和AVL-tree都是一个三叉链结构,只是控制平衡的方式不同,红黑树是通过颜色来控制的

#pragma once

#include <utility>
#include <iostream>
using namespace std;

// 颜色
enum Colour
{
	RED,
	BLACK
};

template <class K, class V>
struct RBTreeNode 
{
	pair<K, V> _key;
	struct RBTreeNode<K, V>* _left;
	struct RBTreeNode<K, V>* _right;
	struct RBTreeNode<K, V>* _parent;
	Colour _col;

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

template <class K, class V>
class RBTree
{
	typedef struct RBTreeNode<K, V> Node;

public:
	// 默认构造
	RBTree()
		:_root(nullptr)
	{}
	
private:
	Node* _root;
};

为什么要定义parent指针,详细讲解请参考AVL章节:点击跳转

四、新增结点颜色的选择

在红黑树中,新增的默认结点颜色可以选择红色,也可以选择黑色。但是,建议选择红色。

接下来分析为什么选择红色。

  • 如果为新增结点默认为红色,可能违反原则3:【如果一个节点是红色的,它的孩子结点必须是黑色的】,那么需要进行适当调整。当然也可能不需要调整。

在这里插入图片描述

  • 如果为新增结点默认为黑色,必然违反原则4:【对于每个结点,从根到空结点NIL,黑色结点的数量相等】,并且因为这一条路,影响了其他所有路径,可能需要对现有的红黑树进行更多的旋转和重新着色操作,从而导致更大的改动,增加了调整平衡的复杂度。

在这里插入图片描述

因此,为了尽可能少地改变树的结构,让新结点默认为红色,插入后,不一定调整,但即使调整,也不至于影响全局。

五、插入分析及代码实现

5.1 前言

RB-tree的平衡条件虽然不同于AVL-tree,但同样运用了单旋转和双旋转来调节平衡。

为了方便讨论,可以为某些特殊结点“取别名”。

  • 插入的新结点为cur

  • 新结点的父结点为parent

  • 新结点的祖父结点为(父结点的父亲)grandparent

  • 叔叔结点(父结点的兄弟结点)为uncle

通常情况下,我们会 特别关注叔叔结点。具体来说会有以下三种情况:

5.2 uncle存在且为红

  • cur插在parent的左边时
    在这里插入图片描述

解决方法:变色 + 继续向上更新看是否需要调整

  • 【变色】结点parent(父亲结点一定要为黑色)和uncle变黑,grandparent变红。变色操作是保证每条路径的黑节点个数相同,并且在grandparent这个子树中,暂时解决了出现连续的红结点的情况。
    在这里插入图片描述

  • 【向上调整】:解决整个树可能出现连续红结点情况(三种):

① 如果grandparent没有父亲:将祖父grandparent变黑即可。

在这里插入图片描述

② 如果grandparent有父亲,且父亲是黑色的,那么不用调整。

③ 如果grandparent有父亲,且父亲是红色的,就要向上进行调整,因为不能出现连续的红色结点。具体的情况也就只有3

比如说以下这种:
在这里插入图片描述

此时uncle为红色,并且cur插在parent的右边。虽然插入位置不同,但解决方法还是一样的。

  • cur插在parent的右边时

解决方法:变色 + 继续向上更新看是否需要调整。详细细节可以看看上面的解释说明

在这里插入图片描述

5.3 当uncle不存在

  • uncle不存在于grandparent的右边时

在这里插入图片描述

解决方法:旋转 + 变色

  • 【旋转】:什么旋转是根据cur插入的位置来定的。如果插入在parent的左边,那么就要以grandparent结点进行右单旋;如果插入在parent的右边,就要进行双旋,先左单旋,最后再右单旋。

在这里插入图片描述

  • 【变色】parent变黑,grandparent变红。

在这里插入图片描述

  • uncle不存在于grandparent的左边时

解决方法还是一样:旋转 + 变色。这里就不再重点讲解了,大家看看下图来领会吧 ~

在这里插入图片描述

接下来再基于uncle不存在时,看看 【双旋】 是怎么个事:

  • uncle不存在于grandparent的左边时

在这里插入图片描述

解决方法同样是变色

  • 【双旋】:我们在上面说过,对于uncle不存在于grandparent的左边这种情况,并且cur插入在parent的左侧,那么就要进行双旋。首先先对parent进行右单旋;再对parent进行左单旋。

在这里插入图片描述

  • 【变色】cur变黑,grandparent变红

在这里插入图片描述

当然了,对于对于uncle不存在于grandparent的右边这种情况,并且cur插入在parent的右侧。这种调整的解决方法同样是双旋 + 变色。双旋是先对于parent左旋转,再对grandparent右旋,最后再将cur变黑以及grandparent变红。由于演示的样例过多,这里就不再演示了hh

5.4 uncle存在且为黑

来看看一下这种情况
在这里插入图片描述

首先我们需要处理uncle存在且为红的情况,解决方法很简单:变色 + 继续向上更新

在这里插入图片描述

继续向上更新时,就出现了uncle存在且为黑的情况

在这里插入图片描述

解决方法:旋转 + 变色(parent变黑、grandparents变红)

在这里插入图片描述

我们发现:uncle存在且为黑的情况好像和uncle不存在的解决方法是一模一样的,因此我们可以将其归为一类。

六、对插入操作总结一波

6.1 uncle存在且为红

在这里插入图片描述

解决方式:变色 + 向上调整

【变色】:将parentunlce改为黑,grandparent改为红。

【向上调整】:把grandparent当成cur,继续向上调整。在调整的过程中,如果grandparent是根结点,则直接将其变黑。

6.2 uncle不存在或uncle存在且为黑

在这里插入图片描述

解决方法:旋转 + 变色

注意:什么旋转是根据cur插入的位置来定的。

  • 【单旋转】 如果cur插入在parent的左边,那么就要以grandparent结点进行右单旋
    【变色】 parent变成黑色,grandparent变为红色。
  • 【双旋转】 如果cur插入在parent的右边,就要进行双旋,先左单旋,最后再右单旋。
    【变色】 cur变成黑色(旋转后cur变为根了,根一定为黑),grandparent变为红色。

当然了,以上的情况均是以parent作为grandparent的左孩子分析的,还需要考虑parent作为grandparent的右孩子,其本质是不变。我大致为大家总结了一下:

  1. 不需要旋转的代码都一样。
  2. 旋转部分的代码要注意结点的方向。

6.3 代码实现

bool Insert(const pair<K, V>& key)
{
	// 如果一开始根结点为空,直接插入即可
	if (_root == NULL)
	{
		_root = new Node(key); // new会自动调用自定义类型的构造函数
		_root->_col = BLACK; // 规则1:根结点_root必须是黑色的
		return true;
	}

	// 如果一开始根结点不为空,就要找到合适的位置插入
	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)
	{
		if (cur->_key.first < key.first)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_key.first > key.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		else // 出现数据冗余,插入失败
		{
			return false;
		}
	}
	// 当cur走到空,说明已经找到了合适的位置
	cur = new Node(key);
	cur->_col = RED; // 插入的新结点必须是红色的
	
	if (parent->_key.first < key.first)
	{
		parent->_right = cur;
	}
	else
	{
		parent->_left = cur;
	}
	cur->_parent = parent;

	// 控制平衡:通过颜色(红/黑)的限制,来达到最长路径不超过最短路径的2倍
	while (parent && parent->_col == RED) // 向上调整的条件
	{
		Node* grandparent = parent->_parent; // 找祖父
		// 如果父亲在祖父的左边
		if (parent == grandparent->_left)
		{
			// 那么uncle一定在祖父的右边
			Node* uncle = grandparent->_right;

			// 情况1:如果uncle存在且为红
			if (uncle && uncle->_col == RED)
			{
				// 解决方法:父亲和叔叔的颜色变黑,祖父变红 + 向上处理

				parent->_col = uncle->_col = BLACK;
				grandparent->_col = RED;
				// 向上处理
				cur = grandparent;
				parent = cur->_parent;
			}

			// 情况2:叔叔不存在或叔叔存在且为黑
			// 解决方法:旋转 + 变色
			else
			{
				// 1. 插入在parent的左边:单旋 + 变色
				if (cur == parent->_left)
				{
					//     g
					//   p
					// c
					
					// 右单旋转
					RotateRight(grandparent);
					// 变色
					parent->_col = BLACK;
					grandparent->_col = RED;
				}
				// 2. 插入在parent的右边:双旋 + 变色
				else
				{
					//     g
					//   p
					//     c

					RotateLeft(parent);
					RotateRight(grandparent);

					cur->_col = BLACK;
					grandparent->_col = RED;
				}
				// 旋转完之后红黑树一定平衡,不需要向上调整
				// 因为旋转后,树/子树的根一定是黑色
				break;
			}
		}

		else // parent == grandparent->_right
		{
			// parent在grandparent的右,那么uncle一定在grandparent的左
			Node* uncle = grandparent->_left;

			// 情况1:如果uncle存在且为红
			// 解决方法:父亲和叔叔的颜色变黑,祖父变红 + 向上处理
			if (uncle && uncle->_col == RED)
			{
				// 变色
				parent->_col = uncle->_col = BLACK;
				grandparent->_col = RED;
				// 向上处理
				cur = grandparent;
				parent = cur->_parent;
			}
			// 情况2:uncle不存在且uncle为黑
			else
			{
				if (cur == parent->_right)
				{
					//  g
					//     p
					//        c
					RotateLeft(grandparent);
					grandparent->_col = RED;
					parent->_col = BLACK;
				}
				else
				{
					//  g
					//     p
					//  c
					RotateRight(parent);
					RotateLeft(grandparent);
					cur->_col = BLACK;
					grandparent->_col = RED;
				}
				break;
			}
		}
	}
	// 当循环退出来到此处,有两种情况
	// 第一种是break出来的,那么红黑树是百分之百已经调整好的
	// 还有一种是向上调整的过程中父亲为空,那么此时根结点可能为空
	// 因此我们可以直接进行暴力处理将根结点的颜色变为黑。因为根为黑是必定的!
	_root->_col = BLACK; 

	return true;
}
  • 至于旋转代码大家可以参考AVL树的博客:点击跳转。
  • 或者参考我的代码仓库:点击跳转

七、验证红黑树

注意:不能使用最长路径(高度)不能超过最短路径的2倍来验证,因为你写的程序有可能会破坏红黑树的规则,比如说你写的红黑树可能会出现连续的红色结点,可能会出现最长路径不会超过最短路径的2倍。我们这里使用红黑树的规则来进行检查。

// backnumber - 用于统计黑色结点的数量
// benchmark - 基准值。此变量是为了求出一条路径的黑色结点个数作为基准值
bool CheckColour(Node* root, int blacknums, int benchmark)
{
	if (root == nullptr)
	{
		// 前序遍历走到空就拿backnumber与基准值benchmark比较即可
		if (blacknums != benchmark)
		{
			return false;
		}
		return true;
	}

	// 2. 每条路径的黑色结点数量相等
	if (root->_col == BLACK) // 遇到黑结点backnumber自增1
	{
		++blacknums;
	}

	// 2. 不可能出现连续的红结点
	// 检查当前结点的颜色和其父亲结点的颜色即可
	if (root->_col == RED && root->_parent && root->_parent->_col == RED)
	{
		cout << root->_key.first << "连续红色结点" << endl;
		return false;
	}

	// 递归检查左子树和右子树
	return CheckColour(root->_left, blacknums, benchmark)
		&& CheckColour(root->_right, blacknums, benchmark);
}

bool _IsBalance(Node* root)
{
	// 根结点为空也算红黑树
	if (root == nullptr)
	{
		return true;
	}
	// 1. 每个结点不是红色就是黑色。(这个不需要验证)
	// 2. 根节点必须是黑色的。
	if (root->_col != BLACK)
	{
		return false;
	}

	int benchmark = 0;	
	Node* cur = _root;
	while (cur)
	{
		if (cur->_col == BLACK)
		{
			++benchmark;
		}
		cur = cur->_left;
	}

	// 3. 颜色的检查
	return CheckColour(root, 0, benchmark);
}

八、红黑树与AVL树的比较

红黑树和AVL树都是自平衡的二叉搜索树,它们在维护树的平衡性方面有些不同,因此在不同的应用场景下会有不同的性能表现。

  1. 平衡性:

    • AVL树:AVL树通过保持任意节点的左右子树高度之差不超过1来维护平衡。(严格平衡)
    • 红黑树:红黑树通过保持以五个性质来维护平衡。(近似平衡)
  2. 插入和删除操作:

    • AVL树:AVL树在进行插入和删除操作时,也会通过旋转来调整树的结构并保持平衡。但相比红黑树,AVL树对平衡的要求更加严格,可能需要进行更多的旋转操作。这使得插入和删除操作的时间复杂度略高于红黑树,为O(log n)

    • 红黑树:红黑树在进行插入和删除操作时,只需通过旋转和颜色变换来调整树的结构并保持平衡。这些操作的时间复杂度为O(log n),其中n是树的节点数量。

  3. 查询操作:

    • 红黑树和AVL树在查询操作上具有相同的时间复杂度,都为O(log n)。这是因为它们都是二叉搜索树,具有相似的查找性能。
  4. 存储空间:

    • 红黑树:红黑树通过颜色标记来维护平衡,需要额外存储每个节点的颜色信息,因此在空间上稍微占用更多的内存。
    • AVL树:AVL树不需要额外的信息来维护平衡,因此在空间上相对较小。

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

九、代码

本篇博客我放到gitte仓库了,感兴趣的小伙伴可以自取:点击跳转

对了,关于红黑树的删除操作大家不用担心,因为在面试中一般只会考察插入操作 ~

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

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

相关文章

codesys【看门狗】

看门狗&#xff1a; 时间&#xff1a; 看门狗饿死时间。灵敏度&#xff1a;是看门狗时间的倍数。看门狗1秒&#xff0c;灵敏度5&#xff0c;结果就是5秒。Task类型&#xff1a; 循环&#xff1a;用于现场总线。惯性滑行&#xff1a;CPU空闲就运行&#xff0c;主程序用这个。 P…

【模板规范】会议纪要模板

文章目录 1、简介2、纪要模板2.1、表格类会议纪要2.2、文档类会议纪要2.3、简易版项目纪要 3、会议纪要3.1、作用3.2、特点3.2.1、工作会议纪要3.2.2、代表会议纪要3.2.3、座谈会议纪要3.2.4、联席会议纪要3.2.5、办公会议纪要3.2.6、汇报会议纪要3.2.7、技术鉴定会议纪要 3.3、…

HCS私有云简介

1、HCS简介和发展史 华为云产品&#xff1a;私有云和公有云&#xff0c;现在的私有云已经和公有云越来越像了FusionSphere是华为的一个品牌2016年&#xff0c;在5.0版本的时候&#xff0c;华为Openstack叫FusionSphere Openstack 5.0&#xff0c;底层用的是suse操作系统&#…

一天一个设计模式---组合模式

基本概念 组合模式是一种结构型设计模式&#xff0c;它允许客户端统一对待单个对象和对象的组合。组合模式通过将对象组织成树形结构&#xff0c;使得客户端可以一致地使用单个对象和组合对象。 主要角色&#xff1a; Component&#xff08;组件&#xff09;&#xff1a; 定…

Express安装与基础使用

一、express 介绍 express 是一个基于 Node.js 平台的极简、灵活的 WEB 应用开发框架&#xff0c; 官方网站&#xff1a; Express - 基于 Node.js 平台的 web 应用开发框架 - Express中文文档 | Express中文网 中文文档&#xff1a; 路由 - Express 中文文档 简单来说&am…

Vulnhub靶机:driftingblues 2

一、介绍 运行环境&#xff1a;Virtualbox 攻击机&#xff1a;kali&#xff08;10.0.2.15&#xff09; 靶机&#xff1a;driftingblues2&#xff08;10.0.2.18&#xff09; 目标&#xff1a;获取靶机root权限和flag 靶机下载地址&#xff1a;https://www.vulnhub.com/entr…

逸学Docker【java工程师基础】3.3Docker安装nacos

docker pull nacos/nacos-server docker network create nacos_network #创建容器网络 docker run -d \ --name nacos \ --privileged \ --cgroupns host \ --env JVM_XMX256m \ --env MODEstandalone \ --env JVM_XMS256m \ -p 8848:8848/tcp \ -p 9848:9848…

一文教你使用 ChatGPT API function calling

一文教你使用 ChatGPT API function calling Function call如何理解Function call如何调用&#xff1f; Function call 如何理解Function call 函式呼叫(function calling) 可说是这次ChatGPT API 更新的杀手级更新。所谓函式呼叫&#xff0c;就是让你把外部函式的形状写入Cha…

【Java IO分类】从传输方式和数据操作上理解 Java IO分类

Java IO - 分类 IO理解分类 - 从传输方式上字节流字符流字节流和字符流的区别字节转字符Input/OutputStreamReader/Writer IO理解分类 - 从数据操作上文件(file)数组([])管道操作基本数据类型缓冲操作打印对象序列化反序列化转换 IO理解分类 - 从传输方式上 从数据传输方式或者…

CSS3十大滤镜效果详解

滤镜效果 类似于美颜相机、美图秀秀这样的美颜工具&#xff0c;能够让我们轻松地应用多种特效&#xff0c;例如转换为黑白照片、复古风格化、调整亮度等。在之前仅凭CSS几乎很难做到这些效果。 但在CSS3的语法中&#xff0c;所有的这些视觉特效都是通过“filter”属性来快速实…

最全 chrome driver

超全chrome和driver的对应链接 https://raw.githubusercontent.com/GoogleChromeLabs/chrome-for-testing/main/data/latest-patch-versions-per-build-with-downloads.json 从这个链接中直接找对应的chrome和driver 复制链接下载。 注意最新版的在一些文章里的链接是找不到的…

论文笔记(四十)Goal-Auxiliary Actor-Critic for 6D Robotic Grasping with Point Clouds

Goal-Auxiliary Actor-Critic for 6D Robotic Grasping with Point Clouds 文章概括摘要1. 介绍2. 相关工作3. 学习 6D 抓握政策3.1 背景3.2 从点云抓取 6D 策略3.3 联合运动和抓握规划器的演示3.4 行为克隆和 DAGGER3.5 目标--辅助 DDPG3.6 对未知物体进行微调的后视目标 4. 实…

深度学习记录--偏差/方差(bias/variance)

误差问题 拟合神经网络函数过程中会出现两种误差&#xff1a;偏差(bias)和方差(variance) 偏差和误差的区别 欠拟合(underfitting) 当偏差(bias)过大时&#xff0c;如左图&#xff0c;拟合图像存在部分不符合值&#xff0c;称为欠拟合(underfitting) 过拟合(overfitting) …

软件测试|使用Python生成PDF文件

简介 PDF&#xff08;Portable Document Format&#xff09;是一种常用的文档格式&#xff0c;具有跨平台兼容性、保真性、安全性和交互性等特点。我们日常生活工作中的合同、报告、论文等通常都采用PDF格式&#xff0c;以确保文档在不同的操作系统&#xff08;例如 Windows、…

鸿蒙开发工程师会不会有很好的就业前景?

一&#xff0c;鸿蒙带动IT开发和应用整体结构的变革 1月11日&#xff0c;以鸿蒙为首的华为概念股大幅走强&#xff0c;创业板创识科技拉升封板&#xff0c;传智教育、智度股份、高新发展、立达信、吉大正元等多股涨停&#xff0c;华亚电子、九联科技、软通动力、辰奕智能、芯海…

正则化方法介绍

在深度学习中&#xff0c;正则化是一种用于防止模型过拟合的技术。过拟合是指模型在训练数据上表现良好&#xff0c;呆在未见过的测试数据集上表现效果较差的情况。正则化的目标是通过对模型性的参数或层的约束&#xff0c;使其在训练数据上表现的不那么复杂&#xff0c;从而提…

【RTOS】快速体验FreeRTOS所有常用API(10)资源管理

目录 十、资源管理10.1 基本概念10.2 关任务调度器10.3 关中断10.4 实例 十、资源管理 该部分在上份代码基础上修改得来&#xff0c;代码下载链接&#xff1a; https://wwzr.lanzout.com/iqBYJ1lauxof 密码:9adb 该代码尽量做到最简&#xff0c;不添加多余的、不规范的代码。 内…

开源云真机平台-Sonic平台-python自定义脚本-批量导出monkey测试日志并抓取logcat日志

【主要功能】 开源云真机平台-Sonic平台-python自定义脚本&#xff1a; 1、批量导出monkey测试日志 2、抓取logcat日志 3、将导出的日志批量复制到局域网内的制定电脑上进行收集整理&#xff1b; 4、每一台设备的monkey日志、logcat日志单独存储&#xff0c;并以日期设备序…

【CSDN年度征文】关于三掌柜2023全年回顾和总结

目录 前言 顺利转正 被任命为项目经理 印象深刻的实战经历&#xff1a;项目重大版本上线 系统学习新技术的心得体会 获得腾讯云开发者社区优秀作者奖 想要安利给所有人的开发工具 技术大会招募 线下沙龙圆桌主持 新书发布上市 受邀直播探会 接受采访 组织1024程序…

css mask使用,背景透明,图片裁切

1 语法 mask: url(image.png); 使用位图来做遮罩mask: url(image.svg#star); 使用SVG图形中的形状来做遮罩mask: linear-gradient(#000 , transparent) 接受类似背景参数的渐变来做遮罩2 示例 2.1 背景透明 {background: url(im…