当前位置: 首页 > article >正文

【进阶数据结构】——红黑树

🌈感谢阅读East-sunrise学习分享——[进阶数据结构]红黑树
博主水平有限,如有差错,欢迎斧正🙏感谢有你 码字不易,若有收获,期待你的点赞关注💙我们一起进步🚀


🌈我们上一篇博客分享了AVL树,但是关于平衡二叉树,还有一个boss在等着你——红黑树
🎄要是说AVL树利用平衡因子实现的方法,是学霸大佬实现的,那红黑树…可就像是一个天才的产物一般✨因为它是如此地天马行空而又独具优异🔴⚫
开始起飞🚀

目录

  • 一、红黑树
    • 1.1 概念
    • 1.2 性质
    • 1.3 红黑树和AVL树的比较
  • 二、红黑树的插入操作
    • 2.1 u存在且为红
    • 2.2 u不存在 / u存在且为黑
      • 单旋调整
      • 双旋调整
  • 三、红黑树的验证

一、红黑树

1.1 概念

红黑树,是一种二叉搜索树,但在每一个节点上增加一个存储位表示节点的颜色,可以是Red或者Black。通过对任何一条从根到叶子的路径上各个节点着色方式的限制,最长路径不超过最短路径的2倍,因而是接近平衡的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HGV6jb22-1679569401700)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230323143151723.png)]

1.2 性质

红黑树的性质

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

🔴⚫红黑树的性质较为精华的是其性质3和性质4;性质3就限制了一棵红黑树中没有连续的红色节点;性质4就限制了一棵红黑树中,每条路径上都包含了相同数量的黑色节点

那么这样一来,一棵红黑树中的最短路径最长路径会是什么样的呢?

  • 最短路径:全黑,整条路径都只有黑色节点
  • 最长路径:一黑一红相间的路径

1.3 红黑树和AVL树的比较

💭既然红黑树只是接近平衡,AVL树还是绝对平衡呢,那为什么当下流行的是红黑树呢?

因为红黑树和AVL树都是高效的平衡二叉树,增删查改的时间复杂度都是O(logN),红黑树不追求绝对平衡,其只需要保证最长路径不超过最短路径的2倍。那就意味着红黑树的增删查改最坏的情况就是O(2*logN){但是在复杂度的计算规则中,O(2logN)和O(logN)是同一个量级的};那可能在最坏情况下红黑树查找所花的次数是AVL树的2倍呗🎄

但是相对而言,红黑树放松了平衡的要求也就意味着降低了插入和旋转的次数旋转插入也是需要花费代价的。假如你现在插入了一亿个数,查找的时候AVL树可能需要30次,红黑树最多也就60次;但是在插入的过程中,红黑树能够比AVL树少百分之三十的旋转次数;因此在经常进行增删查改的结构中,红黑树的性能是优于AVL树的,而且红黑树实现也较为简单,所以实际中红黑树用的比较多🔴⚫

话不多说,开始我们今天的模拟实现红黑树🎄

二、红黑树的插入操作

红黑树是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树的插入可分为两步:

  1. 按照二叉搜索的树规则插入新节点
  2. 检测新节点插入后,红黑树的性质是否造到破坏

当我们要插入时,随之便迎来了第一个问题
📌新插入的节点要设为红色还是黑色呢?

答案是新节点的默认颜色是红色,因此:如果其父节点的颜色是黑色,没有违反红黑树任何性质,则不需要调整;但当新插入节点的父节点颜色为红色时,就违法了性质三(不能有连续在一起的红色节点),此时便需要开始对红黑叔进行处理

假如新节点的默认颜色是黑色,则不论如何,插入之后必定会违反红黑树的性质四(每条路径都有相同数量的黑色节点);而假如新节点默认颜色是红色,只是有几率违反红黑树的性质🎯因此默认选择红色

当新插入节点的父节点为红时,需要开始对红黑树进行调整🚩
约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点

此时我们发现,需要对红黑树进行调整时:cur为红节点,p也为红节点,那此时只有u是不确定的
所以调整红黑树的关键就在于u节点的情况;根据u节点的不同情况我们分出了几种调整情况

2.1 u存在且为红

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-akhYG3gA-1679569401701)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230322135427975.png)]

🎯解决方法:将p、u改为黑,g改为红,然后把g当成cur,迭代继续向上更新

2.2 u不存在 / u存在且为黑

🎄这里将u的两种情况合为一种大情况是因为,u的情况不影响我们的调整方法

单旋调整

1️⃣当u节点不存在

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LqEKrXI0-1679569401702)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230322141716037.png)]

💭值得思考的是:以上这种u节点不存在的情况,cur一定是新插入的节点;因为如果cur不是新插入节点,则cur和p一定要有一个是黑色节点,否则违反了性质3,而倘若cur和p有一个是黑色节点,则又违反了性质4;所以此情况只有是cur为新节点刚插入的(a、b子树为空)

2️⃣当u节点存在且为黑

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u6G6YwcM-1679569401702)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230322142533006.png)]

💭值得思考的是:当u存在且为黑这种情况,cur节点一定不是新插入的节点并且其原来是黑色;假若不是的话,则违背了性质4;所以如图的情况是cur的子树在调整之后将cur节点的颜色由黑色变为红色

综上所述,虽然u节点有两种情况,但是不论如何都是一种调整操作:

  1. 单旋:以g为轴点进行一次右单旋
  2. 变色:p变黑,g变红

上图只画出了p为g的左孩子一种情况,若p为g的右孩子,则同理并进行左单旋

双旋调整

1️⃣当u不存在

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xYuxG5lK-1679569401702)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230322144355417.png)]

2️⃣当u节点存在且为黑

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oePmOGDO-1679569401702)(C:\Users\DongYu\AppData\Roaming\Typora\typora-user-images\image-20230322212444207.png)]

综上所述,虽然u节点有两种情况,但是不论如何都是一种调整操作:

  1. 双旋:先以p为轴点进行一次左单旋,再以g为轴点进行一次右单旋
  2. 变色:p变黑,g变红

⭕上图只画出了p为g的左孩子一种情况,若p为g的右孩子,则同理并进行右左双旋


有的兄弟会发现,上图的两个情况2️⃣调整后好像路径中的黑色节点并不相同;这是因为情况2️⃣只存在与cur节点也有子树的情况下,所以cur并不是路径的终点,并不会违反性质4的

在2.2这个情况中,我们发现最后经过旋转+变色,根节点是黑色了,也就是说不会对其祖先造成任何违反性质的影响因此经过旋转+变色处理后便不需要再迭代向上更新了…这些归纳和细节在我们的代码实现时均需要体现✏️✏️

代码实现✏️

enum Color
{
	RED,
	BLACK,
};

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


	RBTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _col(RED)//新插入节点默认为红
	{}
};

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

	//插入
	bool Insert(const pair<K, V>& kv)
	{
		//空
		if (_root == nullptr)
		{
			_root = new Node(kv);
			_root->_col = BLACK;
			return true;
		}

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

		//插入
		cur = new Node(kv);
		cur->_col = RED;//新插入节点默认为红色
		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}

		//调整红黑树 
		while (parent && parent->_col == RED)//当parent为空则是在创建根节点
		{
			Node* grandfather = parent->_parent; //要找到祖父才能找到叔叔
			if (parent == grandfather->_left)
			{
				Node* uncle = grandfather->_right;
				if (uncle && uncle->_col == RED)
				{
					//情况一
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					//迭代向上更新
					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					//情况二
					if (cur == parent->_left)
					{
						//单旋
						RotateR(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						//双旋
						RotateL(parent);
						RotateR(grandfather);

						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}
			else
			{
				Node* uncle = grandfather->_left;
				if (uncle && uncle->_col == RED)
				{
					//情况一
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					//迭代向上更新
					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					//情况二
					if (cur == parent->_right)
					{
						//单旋
						RotateL(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						//双旋
						RotateR(parent);
						RotateL(grandfather);

						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}
		}
		_root->_col = BLACK;//不论如何根都为黑色

		return true;
	}

	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		parent->_right = subRL;

		if (subRL)
			subRL->_parent = parent;

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

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

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

		if (subLR)
			subLR->_parent = parent;

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

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

private:
	Node* _root = nullptr;
};

三、红黑树的验证

红黑树的验证分为两步:

  1. 检测其是否满足二叉搜索树(中序遍历是否为有序序列)
  2. 检测其是否满足红黑树的性质

🔴⚫对于红黑树的验证,较为费脑的就是其性质3和性质4

验证性质3还行,就递归遍历检查每个节点的颜色,是否会连续为红
但是验证性质4,也就是需要知道每个路径的黑色节点数量💭一开始博主准备直接在红黑树节点中再增加一个变量用于标记从根到每个节点的黑色节点数;但是觉得这样未免太过杀鸡用牛刀了…接着博主就想到了利用递归:在递归的时候增加一个值传递,注意是值传递不是引用传递变量;如此一来,在递归遍历时每一个函数栈帧都会有一个变量记录了此层递归的节点路径上有多少个黑色节点,然后因为递归遍历能够将整颗树走完(走到空),所以当走到空时便能将此路径的黑色节点记录下来;然后找个容器将每条路径的黑色节点总数记录起来再比较即可🎯

最后博主想着不想再增加其空间复杂度,既然要求每个路径黑色节点需要相同,那么我们可以在检查时先直接迭代统计一条路径的黑色节点数量作为一个参考值,之后在检查时每条路径的黑色节点数与参考值比较即可🔴⚫

✏️上代码

void _Inorder(Node* root)
{
	if (root == nullptr)
		return;

	_Inorder(root->_left);
	cout << root->_kv.first << ":" << root->_kv.second << endl;
	_Inorder(root->_right);
}

bool Check(Node* root, int BlackNum, int ref)
{
	if (root == nullptr)
	{
		if (BlackNum != ref)
		{
			cout << "违反性质,本路径黑色节点数与最左路径不同" << endl;
			return false;
		}
		return true;
	}

	if (root->_col == RED && root->_parent->_col == RED)
	{
		cout << "违反性质:连续出现红色节点" << endl;
		return false;
	}

	if (root->_col == BLACK)
		++BlackNum;

	return Check(root->_left, BlackNum, ref) && Check(root->_right, BlackNum, ref);
}

bool isBalance()
{
	if (_root == nullptr)
		return true;

	if (_root->_col != BLACK)
		return false;
		
	int ref = 0;
	Node* left = _root;
	while (left)
	{
		if (left->_col == BLACK)
			++ref;
		left = left->_left;
	}

	return Check(_root,0,ref);
}

🌈🌈写在最后 我们今天的学习分享之旅就到此结束了
🎈感谢能耐心地阅读到此
🎈码字不易,感谢三连
🎈关注博主,我们一起学习、一起进步


http://www.kler.cn/a/2873.html

相关文章:

  • 【ChatGPT】 如何让ChatGPT分析数据并得出结论
  • NVIDIA Isaac Sim 仿真平台体验测评
  • 【日志】392.判断子序列
  • 24/11/12 算法笔记<强化学习> Policy Gradient策略梯度
  • activiti5基础和springboot整合
  • 超市里的货物架调整(算法解析)|豆包MarsCode AI刷题
  • 人员玩手机离岗识别检测系统 yolov5
  • DBA如何定制自动化巡检工具
  • Java开发一年不到,来面试居然敢开口要20K,面完连8K都不想给~
  • 【Linux】SIGCHLD信号
  • 基于微信小程序的小区疫情防控小程序
  • 【算法】核心排序算法之堆排序原理及实战
  • 第七讲 贪心
  • Vue.js语法详解:从入门到精通
  • 如何利用WDM波分复用技术来扩展光纤容量?
  • Vector - CAPL - 检测报文周期
  • Vue2.x源码:new Vue()做了啥?
  • 给程序加个进度条吧,1行Python代码,快速添加~
  • c++ 一些常识 2
  • XILINX关于DDR2的IP的读写控制仿真
  • 【Spring Cloud Alibaba】2.服务注册与发现(Nacos安装)
  • 部署私有npm 库
  • 水文-编程命令快查手册
  • 支持RT-Thread最新版本的瑞萨RA2E1开发板终于要大展身手了
  • 学习 Python 之 Pygame 开发魂斗罗(十)
  • 如何系统型地学习深度学习?