[C++——lesson30.数据结构进阶——「红黑树」]

🛸个人主页: dragoooon34
🚁所属专栏: C++
🚀操作环境: Visual Studio 2022

目录

🌞前言

🌜正文

一、😀认识红黑树

[1-1 🍕红黑树的定义](#1-1 🍕红黑树的定义)

[1-2 🍔红黑树的性质](#1-2 🍔红黑树的性质)

[1-3 🍟红黑树的特点](#1-3 🍟红黑树的特点)

二、😁红黑树的插入操作

[2-1 🌭抽象图](#2-1 🌭抽象图)

[2-2 🍿插入流程](#2-2 🍿插入流程)

[2-3 🧂单纯染色](#2-3 🧂单纯染色)

[2-4 🥓左单旋 + 染色](#2-4 🥓左单旋 + 染色)

[2-5 🥚右左双旋 + 染色](#2-5 🥚右左双旋 + 染色)

[2-6 🍳具体实现代码](#2-6 🍳具体实现代码)

[2-6 🧇注意事项及调试技巧](#2-6 🧇注意事项及调试技巧)

[三、😂AVL树 VS 红黑树](#三、😂AVL树 VS 红黑树)

[3-1 🥞红黑树的检验](#3-1 🥞红黑树的检验)

[3-2 🧈性能对比](#3-2 🧈性能对比)

🔥提炼与总结🔥


🌞前言

红黑树是平衡二叉搜索树中的一种,红黑树性能优异,广泛用于实践中,比如 Linux 内核中的 CFS 调度器就用到了红黑树,由此可见红黑树的重要性。红黑树在实现时仅仅依靠 红 与 黑 两种颜色控制高度,当触发特定条件时,才会采取 旋转 的方式降低树的高度,使其平衡

🌜正文

一、😀认识红黑树

红黑树德国·慕尼黑大学Rudolf Bayer 教授于 1978 年发明,后来被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的 红黑树


红黑树 在原 二叉搜索树 节点的基础上,加上了 颜色 Color 这个新成员,并通过一些规则,降低二叉树的高度

如果说 AVL 树是天才设计,那么 红黑树 就是 天才中的天才设计,不同于 AVL 树的极度自律,红黑树 只在条件符合时,才会进行 旋转降高度,因为旋转也是需要耗费时间的

红黑树在减少旋转次数时,在整体性能上仍然没有落后 AVL 树太多

先来一睹 红黑树 的样貌

注:红黑树在极限场景下,与 AVL 树的性能差不超过 2 倍

1-1 🍕红黑树的定义

红黑树 也是 三叉链 结构,不过它没有 平衡因子 ,取而代之的是 颜色

红黑树 的节点定义如下:(这里是通过 枚举 定义的颜色)

cpp 复制代码
//枚举出 红、黑 两种颜色
enum Color
{
            
            
      
	RED, BLACK
};

//红黑树的节点类
template<class K, class V>
struct RBTreeNode
{
            
            
      
	RBTreeNode(std::pair<K, V> kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _col(RED)	//默认新节点为红色,有几率被调整
	{
            
            
      }

	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	std::pair<K, V> _kv;

	Color _col;
};

注意: 定义新节点时,颜色可以为 红 也可以为 黑,推荐为 红色,具体原因后面解释

1-2 🍔红黑树的性质

结构上的绝妙注定了其离不开规则的限制红黑树 有以下几条性质:

  1. 每个节点不是 红色 就是 黑色
  2. 根节点是 黑色 的
  3. 如果一个节点是 红色 的,那么它的两个孩子节点都不能是 红色 的(不能出现连续的红节点)
  4. 对于每个节点,从该节点到其所有后代的 NIF 节点的简单路径上,都包含相同数目的黑色节点(每条路径上都有相同数目的 黑色 节点)
  5. 每个叶子节点的 nullptr 称为 NIF 节点,并且默认为黑色,此处黑色仅用于路径判断,不具备其他含义

在这些规则的限制之下,红黑树 就诞生了

红黑树 的性质还是比较重要的,可以花点时间结合图示深入理解

说明:《算法导论》等书籍上补充了⼀条每个叶⼦结点(NIL)都是⿊⾊的规则。他这⾥所指的叶⼦结点不是传统的意义上的叶⼦结点,⽽是我们说的空结点,有些书籍上也把NIL叫做外部结点。NIL是为了⽅便准确的标识出所有路径,《算法导论》在后续讲解实现的细节中也忽略了NIL结点,所以我们知道⼀下这个概念即可。

1-3 🍟红黑树的特点

红黑树 在插入节点后,可以选择新节点为

  • 如果为 红 ,可能违反原则3,需要进行调整
  • 如果为 黑,必然违反原则4,并且因为这一条路,影响了其他所有路径,调整起来比较麻烦

因此 推荐在插入时,新节点默认为 红色 ,插入后,不一定调整,即使调整,也不至于 影响全局

显然,红黑树 中每条路径都是 红黑相间 的,因为不能出现连续的 红节点 ,所以 黑节点的数量 >= 红节点

也就是说:红黑树中,最长路径至多为最短路径的两倍

  • 最长路径:红黑相间
  • 最短路径:全是黑节点

上图中的 最短路径 为 3,最长路径 为 4,当然,最短路径 可以为 2

对于 AVL 树来说,下面这个场景必然 旋转降高度 ,但 红黑树 就不必,因为 没有违背性质

综上, 红黑树 是一种折中且优雅的解决方案,不像 AVL 树 那样极端(动不动就要旋转),而是只有触发特定条件时,才会发生旋转,并且在极端场景下, 两者查询速度差异不过 2 倍,但在插入、删除、修改等可能涉及旋转的操作中,红黑树就领先太多了

假设在约 10 亿 大小的数据中进行查找

  • AVL 树至多需要 30 次出结果
  • 红黑树至多需要 60 次出结果

但是,区区几十次的差异,对于 CPU 来说几乎无感,反而是频繁的旋转操作令更费时间

记住:红黑树在实际中性能更好,适用性更强;AVL 树适用存储静态、不轻易修改的数据

二、😁红黑树的插入操作

2-1 🌭抽象图

在演示 红黑树 的插入操作时,也需要借助 抽象图 ,此时的 抽象图 不再代表高度,而是代表 黑色节点 的数量

抽象图中已关注的是 黑色节点 的数量

2-2 🍿插入流程

红黑树 的插入流程也和 二叉搜索树 基本一致,先找到合适的位置,然后插入新节点,当节点插入后,需要对颜色进行判断,看看是否需要进行调整

插入流程:

  • 判断根是否为空,如果为空,则进行第一次插入,成功后返回 true
  • 找到合适的位置进行插入,如果待插入的值比当前节点值大,则往 右 路走,如果比当前节点值小,则往 左 路走
  • 判断父节点与新节点的大小关系,根据情况判断链接至 左边 还是 右边
  • 根据颜色,判断是否需要进行 染色、旋转 调整高度

整体流程如下(不包括染色调整的具体实现)

cpp 复制代码
bool Insert(const std::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);
	if (parent->_kv.first < kv.first)
		parent->_right = cur;
	else
		parent->_left = cur;
	cur->_parent = parent;

	//判断是否需要 染色、旋转
	while (parent && parent->_col == RED)
	{
            
            
      
		Node* grandfather = parent->_parent;	//祖父节点
		//......
	}
	
	return true;
}

红黑树 如何调整取决于 叔叔 ,即 父亲兄弟节点

并且如果 父亲,直接插入就行了,不必调整

如果 父亲为红 并且 叔叔也为红 ,可以只通过 染色 解决当前问题,然后向上走,继续判断是否需要调整

如果 父亲为红 并且 叔叔为黑 或者 叔叔不存在 ,此时需要 旋转 + 染色根据当前节点与父亲的位置关系,选择 单旋 或 双旋 ,值得一提的是 旋转 + 染色 后,不必再向上判断,可以直接结束调整

关于旋转的具体实现,这里不再展开叙述,可以复用 AVL 中的旋转代码,并且最后不需要调整平衡因子

[C++------lesson29.数据结构进阶------「AVL树」]

注意: 红黑树的调整可以分为 右半区 和 左半区 两个方向(根据 grandfather 与 parent 的位置关系而定),每个方向中都包含三种情况:单纯染色、单旋+染色、双旋+染色,逐一讲解费时费力,并且两个大方向的代码重复度极高,因此 下面的旋转操作基于 右半区

左半区 的操作和 右半区 基本没啥区别,可以去完整代码中求证

2-3 🧂单纯染色

如果 父亲 为黑色,则不需要调整,不讨论这种情况,下面三种情况基本要求都是:父亲为红

当新节点插入后,如果 叔叔 节点也为 红色 ,那么可以通过将 祖父 节点的黑色素下放给 父亲和叔叔祖父节点 变为 红色 ,这样调整仍可确保 每条路径中的黑色节点数目相同

单次染色还不够,需要从 grandfather 处继续向上判断是否需要 调整 ,单纯染色后,向上判断可能会变成其他情况,这是不确定的,具体情况具体分析

单纯染色 的操作如下:

注意:c 表示当前节点,p 表示父亲节点,u 表示叔叔节点,g 表示祖父节点


修正: 动图中语句修正为 "父亲为红,叔叔也为红,直接染色即可"

单次染色 结束后,更新 cur 至 grandfather 的位置,并同步更新 parent,继续判断是需要进行 单纯染色单旋 + 染色 还是 双旋 + 染色

本质:将公共的黑色下放给两个孩子

代码片段如下(右半分区)

cpp 复制代码
//在右半区操作
Node* uncle = grandfather->_left;	//叔叔节点

if (uncle && uncle->_col == RED)
{
            
            
      
	//染色、向上更新即可
	grandfather->_col = RED;
	parent->_col = uncle->_col = BLACK;
	cur = grandfather;
	parent = cur->_parent;
}
else
{
            
            
      
	//此时需要 旋转 + 染色
	//......
}

叔叔 存在且为 很好处理,难搞的是 叔叔 不存在或 叔叔 ,需要借助 旋转 降低高度

注意: 此时的五个抽象图,都代表同一个具象图;如果 parent 为空,证明 cur 为根节点,此时需要把根节点置为 黑色,在返回 true 前统一设置即可


c为红,p为红,g为⿊,u存在且为红,则将p和u变⿊,g变红。在把g当做新的c,继续往上更新。
分析:因为p和u都是红⾊,g是⿊⾊,把p和u变⿊,左边⼦树路径各增加⼀个⿊⾊结点,g再变红,相当于保持g所在⼦树的⿊⾊结点的数量不变,同时解决了c和p连续红⾊结点的问题,需要继续往上更新是因为,g是红⾊,如果g的⽗亲还是红⾊,那么就还需要继续处理;如果g的⽗亲是⿊⾊,则处理结束了;如果g就是整棵树的根,再把g变回⿊⾊。
情况1只变⾊,不旋转。所以⽆论c是p的左还是右,p是g的左还是右,都是上⾯的变⾊处理⽅式。


•跟AVL树类似,图0我们展⽰了⼀种具体情况,但是实际中需要这样处理的有很多种情况。
•图1将以上类似的处理进⾏了抽象表达,d/e/f代表每条路径拥有hb个⿊⾊结点的⼦树,a/b代表每条路径拥有hb-1个⿊⾊结点的根为红的⼦树,hb>=0。
•图2/图3/图4,分别展⽰了hb == 0/hb == 1/hb == 2的具体情况组合分析,当hb等于2时,这⾥组合情况上百亿种,这些样例是帮助我们理解,不论情况多少种,多么复杂,处理⽅式⼀样的,变⾊再继续往上处理即可,所以我们只需要看抽象图即可

2-4 🥓左单旋 + 染色

单旋:右右、左左 ,此时在 右半区 ,所以当 叔叔 不存在或者为 黑色 且节点位于 父亲右边 时,可以通过 左单旋 降低高度

如果在左半区,节点位于父亲的左边时,则使用 右单旋 降低高度

在高度降低后,需要使用 染色 确保符合 红黑树 的性质

旋转 思想很巧妙,在 旋转 + 染色 后,可以跳出循环,结束调整

左旋转 + 染色 的操作如下:

注意c 表示当前节点,p 表示父亲节点,u 表示叔叔节点,g 表示祖父节点

显然,旋转 + 染色 后,parent 是一定会被修改为 黑色 的,所以不必再往上判断调整,因为现在已经很符合性质了(即使 parent 的父亲是 红色 ,也不会出现连续的 红色节点

本质将 parent 的左孩子托付给 grandfather 后,parent 往上提,并保证不违背性质

代码片段如下(右半分区)

cpp 复制代码
//在右半区操作
Node* uncle = grandfather->_left;	//叔叔节点

if (uncle && uncle->_col == RED)
{
            
            
      
	//染色、向上更新即可
	//......
}
else
{
            
            
      
	//此时需要 旋转 + 染色
	if (parent->_right == cur)
	{
            
            
      
		//右右,左单旋 ---> parent 被提上去了
		RotateL(grandfather);
		grandfather->_col = RED;
		parent->_col = BLACK;
		cur->_col = RED;
	}
	else
	{
            
            
      
		//右左,右左双旋 ---> cur 被提上去了
		//......
	}

	//旋转后,保持平衡,可以结束调整
	break;
}

注意: 这种情况多半是由 单纯染色 转变而来的,所以不同区域的抽象图有不同的情况,必须确保能符合红黑树的性质


c为红,p为红,g为⿊,u不存在或者u存在且为⿊,u不存在,则c⼀定是新增结点,u存在且为⿊,则c⼀定不是新增,c之前是⿊⾊的,是在c的⼦树中插⼊,符合情况1,变⾊将c从⿊⾊变成红⾊,更新上来的。 分析:p必须变⿊,才能解决,连续红⾊结点的问题,u不存在或者是⿊⾊的,这⾥单纯的变⾊⽆法解决问题,需要旋转+变⾊。

如果p是g的左,c是p的左,那么以g为旋转点进⾏右单旋,再把p变⿊,g变红即可。p变成课这颗树新的根,这样⼦树⿊⾊结点的数量不变,没有连续的红⾊结点了,且不需要往上更新,因为p的⽗亲是⿊⾊还是红⾊或者空都不违反规则


如果p是g的右,c是p的右,那么以g为旋转点进⾏左单旋,再把p变⿊,g变红即可。p变成课这颗树新的根,这样⼦树⿊⾊结点的数量不变,没有连续的红⾊结点了,且不需要往上更新,因为p的⽗亲是⿊⾊还是红⾊或者空都不违反规则。

2-5 🥚右左双旋 + 染色

双旋右左、左右 ,此时在 右半区 ,所以当 叔叔 不存在或者为 黑色 且节点位于 父亲左边 时,可以通过 右左双旋 降低高度

如果在左半区,节点位于父亲的右边时,则使用 左右双旋 降低高度

在高度降低后,需要使用 染色 确保符合 红黑树 的性质

旋转 思想很巧妙,在 旋转 + 染色 后,可以跳出循环,结束调整

右左双旋 + 染色 的操作如下:

注意:c 表示当前节点,p 表示父亲节点,u 表示叔叔节点,g 表示祖父节点

双旋 其实就是两个不同的 单旋 ,不过对象不同而已,先 右旋转 parent ,再 左旋转 grandfather 就是 右左双旋

本质将 cur 的右孩子托付给 parent,左孩子托付给 grandfather 后,把 cur 往上提即可,并保证不违背 红黑树 的性质

代码片段如下(右半分区)

cpp 复制代码
Node* grandfather = parent->_parent;	//祖父节点
if (grandfather->_right == parent)
{
            
            
      
	//在右半区操作
	Node* uncle = grandfather->_left;	//叔叔节点

	if (uncle && uncle->_col == RED)
	{
            
            
      
		//染色、向上更新即可
		//......
	}
	else
	{
            
            
      
		//此时需要 旋转 + 染色
		if (parent->_right == cur)
		{
            
            
      
			//右右,左单旋 ---> parent 被提上去了
			//......
		}
		else
		{
            
            
      
			//右左,右左双旋 ---> cur 被提上去了
			RotateR(parent);
			RotateL(grandfather);
			grandfather->_col = RED;
			parent->_col = RED;
			cur->_col = BLACK;
		}

		//旋转后,保持平衡,可以结束调整
		break;
	}

注意: 双旋的情况也可以由 单纯变色 转变而来,同样的,不同区域的抽象图代表不同的含义;对 parent 进行右单旋,对 grandfather 进行左单旋


c为红,p为红,g为⿊,u不存在或者u存在且为⿊,u不存在,则c⼀定是新增结点,u存在且为⿊,则c⼀定不是新增,c之前是⿊⾊的,是在c的⼦树中插⼊,符合情况1,变⾊将c从⿊⾊变成红⾊,更新来的。
分析:p必须变⿊,才能解决,连续红⾊结点的问题,u不存在或者是⿊⾊的,这⾥单纯的变⾊⽆法解决问题,需要旋转+变⾊。

2-6 🍳具体实现代码

总的来说,红黑树 的插入操作其实比 AVL 树 还要略显简单,画图分析后,确认如何 染色 就行了,下面是插入操作的完整源码(包括左、右单旋)

插入

cpp 复制代码
bool Insert(const std::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);
	if (parent->_kv.first < kv.first)
		parent->_right = cur;
	else
		parent->_left = cur;
	cur->_parent = parent;

	//判断是否需要 染色、旋转
	while (parent && parent->_col == RED)
	{
            
            
      
		Node* grandfather = parent->_parent;	//祖父节点
		if (grandfather->_right == parent)
		{
            
            
      
			//在右半区操作
			Node* uncle = grandfather->_left;	//叔叔节点

			if (uncle && uncle->_col == RED)
			{
            
            
      
				//染色、向上更新即可
				grandfather->_col = RED;
				parent->_col = uncle->_col = BLACK;
				cur = grandfather;
				parent = cur->_parent;
			}
			else
			{
            
            
      
				//此时需要 旋转 + 染色
				if (parent->_right == cur)
				{
            
            
      
					//右右,左单旋 ---> parent 被提上去了
					RotateL(grandfather);
					grandfather->_col = RED;
					parent->_col = BLACK;
					cur->_col = RED;
				}
				else
				{
            
            
      
					//右左,右左双旋 ---> cur 被提上去了
					RotateR(parent);
					RotateL(grandfather);
					grandfather->_col = RED;
					parent->_col = RED;
					cur->_col = BLACK;
				}

				//旋转后,保持平衡,可以结束调整
				break;
			}
		}
		else
		{
            
            
      
			//在左半区操作
			Node* uncle = grandfather->_right;	//叔叔节点

			//同理,进行判断操作
			if (uncle && uncle->_col == RED)
			{
            
            
      
				//直接染色
				grandfather->_col = RED;
				parent->_col = uncle->_col = BLACK;
				cur = grandfather;
				parent = cur->_parent;
			}
			else
			{
            
            
      
				//需要 旋转 + 染色
				if (parent->_left == cur)
				{
            
            
      
					//左左,右单旋 ---> parent 被提上去
					RotateR(grandfather);
					grandfather->_col = RED;
					parent->_col = BLACK;
					cur->_col = RED;
				}
				else
				{
            
            
      
					//左右,左右双旋 ---> cur 被提上去
					RotateL(parent);
					RotateR(grandfather);
					grandfather->_col = RED;
					parent->_col = RED;
					cur->_col = BLACK;
				}

				break;
			}
		}
	}

	//再次更新根节点的颜色,避免出问题
	_root->_col = BLACK;
	return true;
}

左单旋

cpp 复制代码
//左单旋
void RotateL(Node* parent)
{
            
            
      
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	//将 subR 的孩子交给 parent
	parent->_right = subRL;
	if (subRL != nullptr)
		subRL->_parent = parent;

	//提前保存 parent 的父节点信息
	Node* pparent = parent->_parent;

	//将 parent 变成 subR 的左孩子
	subR->_left = parent;
	parent->_parent = subR;

	//更新 subR 的父亲
	if (pparent == nullptr)
	{
            
            
      
		//此时 parent 为根,需要改变根
		_root = subR;
		_root->_parent = nullptr;
	}
	else
	{
            
            
      
		//根据不同情况进行链接
		if (pparent->_right == parent)
			pparent->_right = subR;
		else
			pparent->_left = subR;
		subR->_parent = pparent;
	}
}

右单旋

cpp 复制代码
//右单旋
void RotateR(Node* parent)
{
            
            
      
	//基本原理和左单旋一致

	Node* subL = parent->_left;
	Node* subLR = subL->_right;

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

	Node* pparent = parent->_parent;

	subL->_right = parent;
	parent->_parent = subL;

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

关于左右单旋的详细细节,可以去隔壁 AVL 树的文章查看,红黑树 这里不必阐述

2-6 🧇注意事项及调试技巧

红黑树 这里也涉及很多等式 == 判断,一定要多多注意,不要漏写 =

三叉链 结构,要注意 父指针 的调整

红黑树 的调整情况如下:

  • 右半区
    • 右右 左单旋
    • 右左,右左双旋
  • 左半区
    • 左左,右单旋
    • 左右,左右双旋

得益于前面 AVL 树旋转操作的学习,红黑树 这里在编写 旋转 相关代码时,没什么大问题

红黑树 的 DeBug 逻辑与 AVL 树一致,这里额外分享一个 DeBug 技巧:

  • 当 随机插入 数据出错时,可以借助文件读写操作,将出错的数据保存下来,然后再次输入,反复进行调试,即可找出 Bug
  • 因为是 随机插入 时出现的问题,所以需要保存一下数据样本

关于 红黑树 详细操作可以参考这篇 Blog:《红黑树(C++实现)

三、😂AVL树 VS 红黑树

AVL 树 和 红黑树平衡二叉搜索树 的两种优秀解决方案,既然两者功能一致,那么它们的实际表现如何呢?可以通过大量随机数插入,得出结果

当然,在切磋之前,需要先验证一下之前写的 红黑树 的正确性

3-1 🥞红黑树的检验

可以借助红黑树 的性质,从下面这三个方面进行检验:

  • 验证根节点是否为 黑色节点
  • 验证是否出现连续的 红色节点
  • 验证每条路径中的 黑色节点 数量是否一致

判断黑色节点数量,需要先获取 基准值

  • 简单,先单独遍历一遍,其中的路径,这里选择了最左路径,将这条路径中获取的黑色节点数作为基准值,传给函数判断使用

孩子不一定存在,但父亲一定存在(当前节点为 红色 的情况下)

  • 所以当节点为 红色 时,判断父亲是否为黑色,如果不是,则非法!
cpp 复制代码
		//合法性检验
		bool IsRBTree() const
		{
            
            
      
			if (_root->_col != BLACK)
			{
            
            
      
				std::cerr << "根节点不是黑色,违反性质二" << std::endl;
				return false;
			}

			//先统计最左路径中的黑色节点数量
			int benchMark = 0;	//基准值
			Node* cur = _root;
			while (cur)
			{
            
            
      
				if (cur->_col == BLACK)
					benchMark++;
				cur = cur->_left;
			}

			//统计每条路径的黑色节点数量,是否与基准值相同
			int blackNodeSum = 0;
			return _IsRBTree(_root, blackNodeSum, benchMark);
		}

protected:
		bool _IsRBTree(Node* root, int blackNodeSum, const int benchMark) const
		{
            
            
      
			if (root == nullptr)
			{
            
            
      
				if (blackNodeSum != benchMark)
				{
            
            
      
					std::cerr << "某条路径中的黑色节点数出现异常,违反性质四" << std::endl;
					return false;
				}
				return true;
			}

			if (root->_col == BLACK)
			{
            
            
      
				blackNodeSum++;
			}
			else
			{
            
            
      
				//检查当前孩子的父节点是否为 黑节点
				if (root->_parent->_col != BLACK)
				{
            
            
      
					std::cerr << "某条路径中出现连续的红节点,违反性质三" << std::endl;
					return true;
				}
			}

			return _IsRBTree(root->_left, blackNodeSum, benchMark) && _IsRBTree(root->_right, blackNodeSum, benchMark);
		}

通过代码插入约 10000 个随机数,验证是否是红黑树

鉴定为 合法,并且高度有 16,比 AVL 树略高一层(情理之中)

3-2 🧈性能对比

红黑树 不像 AVL 树那样过度自律,其主要优势体现在 插入数据 时的效率之上,可以通过程序对比一下

cpp 复制代码
void RBTreeTest2()
{
            
            
      
	srand((size_t)time(NULL));

	AVLTree<int, int> av;
	RBTree<int, int> rb;

	int begin1, begin2, end1, end2, time1 = 0, time2 = 0;

	int n = 100000;
	int sum = 0;
	for (int i = 0; i < n; i++)
	{
            
            
      
		int val = (rand() + i) * sum;
		sum ++;

		begin1 = clock();
		av.Insert(make_pair(val, val));
		end1 = clock();
		
		begin2 = clock();
		rb.Insert(make_pair(val, val));
		end2 = clock();

		time1 += (end1 - begin1);
		time2 += (end2 - begin2);
	}

	cout << "插入 " << sum << " 个数据后" << endl;
	cout << "AVLTree 耗时: " << time1 << "ms" << endl;
	cout << "RBTree 耗时: " << time2 << "ms" << endl;
	cout << "=================================" << endl;
	cout << "AVLree: " << av.IsAVLTree() << " | " << "高度:" << av.getHeight() << endl;
	cout << "RBTree: " << rb.IsRBTree() << " | " << "高度:" << rb.getHeight() << endl;
}

此时数据量太小了,还不能体现 红黑树 的价值,还好这次测试,红黑 比 AVL 强

红黑树还是有实力的

红黑树 是 set 和 map 的底层数据结构,在下篇文章中,将会进一步完善 红黑树 ,并用我们自己写的 红黑树 封装 set / map,最后可以和库中的切磋一下~

本文中涉及的源码:《RBTree 博客

🔥提炼与总结🔥

红黑树是自平衡二叉搜索树,核心价值是通过规则维持平衡,兼顾查找、插入、删除高效性。

1️⃣核心认知:红黑树的本质与核心规则

1. 本质:基于BST扩展,通过红/黑节点标记及规则实现近似平衡,避免退化为线性链表。

2. 核心性质(平衡基石)

① 节点非红即黑;

② 根、叶子(NIL)为黑;

③ 红节点子节点必为黑;

④ 任意节点到叶子路径黑节点数相同。

**3. 特点:**近似平衡减少旋转开销,插入/删除性能优于严格平衡树;查找复杂度O(logn),是Java TreeMap等的常用结构。

2️⃣操作核心:插入操作的逻辑与关键技巧

1. 插入原则:新节点默认染红(减少性质破坏),插入后通过染色或旋转+染色修正平衡。

2. 修正方案

① 叔节点红:父、叔染黑,祖父染红(单纯染色);

② 叔节点黑:按插入位置选左单旋/右左双旋,配合染色调整。

**3. 实践要点:**先按BST规则插入,调试重点校验5条性质;代码封装旋转/染色方法,规避边界情况

3️⃣对比优势:红黑树与AVL树的核心差异

**1. 平衡标准:**AVL严格平衡(高度差≤1),红黑树近似平衡(黑高一致)。

**2. 性能:**查找AVL略优(高度更矮),但均为O(logn);插入/删除红黑树更优(旋转次数少),高频更新场景优势明显。

**3. 红黑树检验:**需同时满足BST左小右大(结构合法)和5条颜色性质(颜色合法)。

4️⃣核心提炼:红黑树的价值与应用场景

1. 核心逻辑:以"BST结构+颜色约束"为核心,通过"插入染红→校验→修正"实现自平衡,保障操作O(logn)复杂度。

**2. 核心优势:**平衡维护成本与操作效率均衡,适合高频更新、低频查找场景,支持有序性与范围查询。

**3. 应用启示:**体现"近似平衡优于严格平衡"的工程思维,兼顾有序性、高效查找与更新时,是更优选择。

结束语

以上就是我对于【C++】STL 学习------「红黑树」的理解

感谢你的三连支持!!!

相关推荐
rainFFrain2 小时前
QT显示类控件---QSlider
开发语言·qt
云泽8082 小时前
C++ STL 栈与队列完全指南:从容器使用到算法实现
开发语言·c++·算法
郑州光合科技余经理2 小时前
实战:攻克海外版同城生活服务平台开发五大挑战
java·开发语言·javascript·数据库·git·php·生活
长孙阮柯2 小时前
Java进阶篇(五)
java·开发语言
⑩-2 小时前
Blocked与Wati的区别
java·开发语言
IManiy3 小时前
Java表达式引擎技术选型分析(SpEL、QLExpress)
java·开发语言
历程里程碑3 小时前
C++ 17异常处理:高效捕获与精准修复
java·c语言·开发语言·jvm·c++
雨雨雨雨雨别下啦3 小时前
ssm复习总结
java·开发语言
xu_yule3 小时前
算法基础(背包问题)—分组背包和混合背包
c++·算法·动态规划·分组背包·混合背包