C++【红黑树】

✨个人主页: 北 海
所属专栏: C++修行之路
操作环境: Visual Studio 2019 版本 16.11.17

成就一亿技术人


文章目录

  • 前言
  • ️正文
    • 1、认识红黑树
      • 1.1、红黑树的定义
      • 1.2、红黑树的性质
      • 1.3、红黑树的特点
    • 2、红黑树的插入操作
      • 2.1、抽象图
      • 2.2、插入流程
      • 2.3、单纯染色
      • 2.4、左单旋 + 染色
      • 2.5、右左双旋 + 染色
      • 2.6、具体实现代码
      • 2.6、注意事项及调试技巧
    • 3、AVL树 VS 红黑树
      • 3.1、红黑树的检验
      • 3.2、性能对比
  • 总结


前言

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

C++【红黑树】_第1张图片


️正文

1、认识红黑树

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

C++【红黑树】_第2张图片
红黑树 在原 二叉搜索树 节点的基础上,加上了 颜色 Color 这个新成员,并通过一些规则,降低二叉树的高度

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

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

先来一睹 红黑树 的样貌

C++【红黑树】_第3张图片

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

1.1、红黑树的定义

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

C++【红黑树】_第4张图片

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

//枚举出 红、黑 两种颜色
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 节点,并且默认为黑色,此处黑色仅用于路径判断,不具备其他含义

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

C++【红黑树】_第5张图片

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

1.3、红黑树的特点

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

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

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

C++【红黑树】_第6张图片

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

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

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

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

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

C++【红黑树】_第7张图片

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

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

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

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

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


2、红黑树的插入操作

2.1、抽象图

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

C++【红黑树】_第8张图片

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

2.2、插入流程

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

插入流程:

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

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

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++【AVL树】》

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

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

2.3、单纯染色

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

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

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

单纯染色 的操作如下:

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


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

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

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

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

//在右半区操作
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 前统一设置即可

2.4、左单旋 + 染色

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

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

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

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

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

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

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

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

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

//在右半区操作
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;
}

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

2.5、右左双旋 + 染色

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

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

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

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

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

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

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

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

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

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 进行左单旋

2.6、具体实现代码

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

插入

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;
}

左单旋

//左单旋
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;
	}
}

右单旋

//右单旋
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++实现)》


3、AVL树 VS 红黑树

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

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

3.1、红黑树的检验

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

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

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

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

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

  • 所以当节点为 红色 时,判断父亲是否为黑色,如果不是,则非法!
		//合法性检验
		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 个随机数,验证是否是红黑树

C++【红黑树】_第9张图片

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

3.2、性能对比

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

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;
}

C++【红黑树】_第10张图片

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

红黑树还是有实力的

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

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


总结

以上就是本次关于 C++【红黑树】的全部内容了,在本文中,我们首先了解了什么是 红黑树,然后对其进行了实现,作为数据结构中的大哥,红黑树 还是有一定难度的,作为被广泛使用的优秀数据结构,简单掌握还是很有必要的


星辰大海

相关文章推荐

C++ 进阶知识

C++【AVL树】

C++【set 和 map 学习及使用】

C++【二叉搜索树】

C++【多态】

C++【继承】

STL 之 泛型思想

C++【模板进阶】

C++【模板初阶】

你可能感兴趣的:(C++修行之路,c++,开发语言,数据结构)