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

在这里插入图片描述

👦个人主页:@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.mfbz.cn/a/327484.html

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

相关文章

C++多线程学习[三]:成员函数作为线程入口

一、成员函数作为线程入口 #include<iostream> #include<thread> #include<string>using namespace std;class Mythread { public:string str;void Test(){cout << str << endl;} }; int main() {Mythread test;test.str "Test";thr…

Mybatis的一些问答记录

0、delegate属性即装饰器,调用cache中的任意一个方法都会沿着链条往下依次执行。 1、JDBC的PreparedStatement(一次编译多次执行)每执行一次executeQuery()就会清空上一次的参数&#xff0c;执行完executeQuery()后就可以获取结果。 ReuseExecutor可重用执行器可以指定使用JDB…

105、Zero-1-to-3: Zero-shot One Image to 3D Object

简介 官网  使用合成数据集来学习相对摄像机视点的控制&#xff0c;这允许在指定的摄像机变换下生成相同对象的新图像&#xff0c;用于从单个图像进行三维重建的任务。 实现流程 输入图像 x ∈ R H W 3 x \in \R^{H \times W \times 3} x∈RHW3&#xff0c;所需视点的相…

无人机视角、多模态、模型剪枝、国产AI芯片部署

无人机视角、多模态、模型剪枝、国产AI芯片部署是当前无人机技术领域的重要研究方向&#xff0c;其原理和应用价值在以下几个方面进行详细讲述。 一、无人机视角&#xff1a;无人机视角是指在无人机上搭载摄像头等设备&#xff0c;通过航拍图像获取环境信息&#xff0c;并进行…

生产力与生产关系 —— 浅析爱泼斯坦事件 之 弱电控制强电原理

据网络文字与视频资料&#xff0c;爱泼斯坦事件是犹太精英阶层&#xff0c;为了掌控美国国家机器为犹太利益集团服务&#xff0c;而精心设下的一个局。本文先假设这个结论成立&#xff0c;并基于此展开讨论。 我们知道&#xff0c;弱电管理强电是电气工程中的一门专门学问&…

6 - 常用工具类

目录 1. Scanner 扫描控制台输入 1.1 扫描控制台输入 1&#xff09;nextLine 2&#xff09;nextInt 3&#xff09;其他方法 1.2 扫描文件 1.3 查找匹配项 2. Arrays 数组工具 2.1 创建数组 1&#xff09;copyOf 2&#xff09;copyOfRange 3&#xff09;fill 2.2 比…

GPT的版本发展历史及特点

版本介绍 GPT&#xff08;Generative Pre-trained Transformer&#xff09;是一系列基于Transformer架构的预训练语言模型&#xff0c;由OpenAI推出。以下是GPT的版本发展、特点和区别&#xff1a; GPT-1 GPT-1是最早发布的版本&#xff0c;于2018年发布。它具有1.17亿个参数&…

【Spring 篇】走进SpringMVC的世界:舞动Web的激情

嗨&#xff0c;亲爱的小白们&#xff01;欢迎来到这篇关于SpringMVC的博客&#xff0c;让我们一起探索这个舞动Web的框架&#xff0c;感受它带来的激情和便利。在这个世界里&#xff0c;我们将学到SpringMVC的概述、开发步骤以及如何快速入门&#xff0c;一切都是如此的令人兴奋…

科研绘图(五)玫瑰图

柱状图的高级平替可视化 “玫瑰图”&#xff0c;通常也被称为“科克斯图”。它类似于饼图&#xff0c;但不同之处在于每个部分&#xff08;或“花瓣”&#xff09;的角度相同&#xff0c;半径根据它表示的值而变化。这种可视化工具对于周期性地显示信息非常有用&#xff0c;比…

bash shell基础命令(一)

1.shell启动 shell提供了对Linux系统的交互式访问&#xff0c;通常在用户登录终端时启动。系统启动的shell程序取决于用户账户的配置。 /etc/passwd/文件包含了所有用户的基本信息配置&#xff0c; $ cat /etc/passwd root:x:0:0:root:/root:/bin/bash ...例如上述root账户信…

使用 Apache POI 更新/覆盖 特定的单元格

使用 Apache POI 更新特定的单元格 一. 需求二. 实现三. 效果 一. 需求 将以下表中第4行&#xff0c;第4列的单元格由“张宇”更新为“汤家凤”&#xff0c;并将更行后的结果写入新的Excel文件中&#xff1b; 二. 实现 使用Apache POI&#xff0c;可以精确定位到需要更改的单…

非递归实现归并排序

目录 非递归的归并排序 非递归的归并排序 实现流程参考图&#xff1a; 1、像递归实现归并排序一样&#xff0c;开辟n个空间大小的临时数组 2、利用变量gap模仿递归的过程&#xff0c;gap表示归并时的每组数据的个数 3、利用while循环实现归并&#xff0c;并且每一次我们要的…

鸿蒙开发笔记(三):页面和自定义组件生命周期

先明确自定义组件和页面的关系&#xff1a; 自定义组件&#xff1a;Component装饰的UI单元&#xff0c;可以组合多个系统组件实现UI的复用。 页面&#xff1a;即应用的UI页面。可以由一个或者多个自定义组件组成&#xff0c;Entry装饰的自定义组件为页面的入口组件&#xff0c…

Linux环境基础开发工具的使用(下)

文章目录 Linux编译器 - gcc/ggcc/g如何使用预处理阶段编译阶段汇编阶段链接阶段gcc选项汇总静态库与动态库gdb命令汇总 Linux项目自动化构建工具 - make/Makefilemake/Makefile的意义使用make/makefile原理 Linux编译器 - gcc/g 背景知识 我们知道一个代码写完要变为可执行程…

OpenHarmony—编译构建指导

概述 OpenHarmony编译子系统是以GN和Ninja构建为基座&#xff0c;对构建和配置粒度进行部件化抽象、对内建模块进行功能增强、对业务模块进行功能扩展的系统&#xff0c;该系统提供以下基本功能&#xff1a; 以部件为最小粒度拼装产品和独立编译。支持轻量、小型、标准三种系…

大厂咋做支付系统的核对?

核对是保障资金安全的重要机制。 时效角度&#xff0c;主要有&#xff1a; &#xff08;准&#xff09;实时核对 准确性不如离线核对&#xff0c;且需相应实时核对平台建设 离线核对&#xff08;如 T1 核对&#xff09; 主要问题是发现问题的时机较为后置&#xff0c;部分场景…

微信小程序-----全局配置与页面配置

目录 前言 全局配置文件 一、window 1. 小程序窗口的组成部分 2. window 节点常用的配置项 3. 设置导航栏的标题 4. 设置导航栏的背景色 5. 设置导航栏的标题颜色 6. 全局开启下拉刷新功能 7. 设置下拉刷新时窗口的背景色 8. 设置下拉刷新时 loading 的样式 9. 设置…

蓝桥杯备赛 | 洛谷做题打卡day2

​ 蓝桥杯备赛 | 洛谷做题打卡day2 嵌套循环yyds&#xff01;&#xff01; 题目来源&#xff1a;洛谷P2670 [NOIP2015 普及组] 扫雷游戏 题目背景 NOIP2015 普及组 T2 题目描述 扫雷游戏是一款十分经典的单机小游戏。在 n n n 行 m m m 列的雷区中有一些格子含有地雷&am…

跨域请求的API接口调用流程

在Web开发中&#xff0c;前端和后端相互通信是非常常见的需求。通常情况下&#xff0c;前端通过发送HTTP请求调用后端的API接口来获取数据或执行某些操作。然而&#xff0c;由于同源策略的限制&#xff0c;浏览器默认情况下不允许跨域请求&#xff0c;即前端不能直接从一个域名…

48 WAF绕过-权限控制之代码混淆及行为造轮子

目录 Safedog代码层手写及脚本绕过BT Aliyun代码层手写及脚本绕过safedog&#xff0c;BT&#xff0c;Aliyun-基于覆盖加密变异下编码解码绕过-代码层Safedog&#xff0c;BT&#xff0c;Aliyun-基于冰蝎新型控制器绕过全面测试-行为层Safedog,BT,Aliyun-基于手写新型控制器绕过全…
最新文章