数据结构 -RB Tree

博客主页:【夜泉_ly

本文专栏:【数据结构

欢迎点赞👍收藏⭐关注❤️

文章目录

  • 💡红黑树
    • [📖 简介](#📖 简介)
      • [✨ 特点](#✨ 特点)
      • [📏 规则](#📏 规则)
      • [🛤️ 路径](#🛤️ 路径)
    • [➕ 插入](#➕ 插入)
      • [📏 规则](#📏 规则)
      • [📝 具体示例](#📝 具体示例)
      • [💻 代码实现](#💻 代码实现)
      • 🖼️示意图

💡红黑树

📖 简介

RBredblack,是的,本文讲红黑树。

和AVL树一样,红黑树也是一颗二叉平衡搜索树。因此,如果对二叉搜索树不熟悉的,可以先去看看我写的数据结构 -Binary Search Tree -1 ;如果对二叉平衡搜索树不熟悉的,可以先去看看我写的数据结构 -AVL Tree

✨ 特点

相较于AVL树,红黑树并没有特别严格的控制左右子树的高度差,而是规定最长路径不超过最短路径的二倍 需注意,这个不超过二倍不是通过控制具体高度得来的,而是在红黑树的一系列规则下自然达到的,因此,只要在插入删除时满足红黑树的规则,就可以达到不超过二倍的效果。而在之后的操作中,我们只需要关注局部符合 红黑树的性质,就能保证整体符合红黑树的性质。

📏 规则

红黑树通过颜色将所有节点分为两类:红节点和黑节点。并通过颜色控制树整体的形状。

现在简单介绍一下红黑树的规则:

性质1. 结点是红色或黑色。

性质2. 根结点是黑色。

性质3. 每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点)

性质4. 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点。

性质5. 所有叶子都是黑色。(叶子是NIL结点)


CV自:百度百科--红黑树

🛤️ 路径

这里重点解释一下性质5。

首先需要知道,NIL节点在此处就是指空节点。

那么为什么要特意强调空节点为黑呢?

这是大佬想让我们正确的认识路径,例如下面这棵树就不是红黑树:

因为如果将空节点补出来,那么将不满足所有路径黑节点相同这一规则:

再来理解一下为什么路径差可以不超过二倍:

首先,有几颗纯黑的树(由于需要满足黑节点数相同,因此都是完全二叉树):

然后在符合规矩的情况下随意插入红色节点,或改变黑节点的颜色:

由于必须满足各路径黑节点数相同 + 红节点不能连续出现:

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

因此,只需要保证颜色符合红黑树的规定,那么高度就自然维持了平衡。

而空节点主要起辅助作用,可以帮助我们更好的理解路径,但在改变颜色和旋转二叉树这块不太需要在意空节点,因此,在之后的图中,我会省略空节点

➕ 插入

在浅浅的理解了路径后,我们来玩牌吧。

现在花火会不断向我们发牌,而我们需要按红黑树的规则将牌摆放在正确的位置。

📏 规则

这里重新声明一下游戏规则:

  • 规则一 :牌面向下对应黑色节点,牌面向上对应红色节点
  • 规则二 :放置在最上方的手牌,牌面向下
  • 规则三 :不能出现连续两张牌面向上的牌(可以认为当放置了一张牌面向上的牌,那么其下面两张牌必定牌面向下)
    错误示例
  • 规则四 :对于每张手牌,左右路径 牌面向下的手牌数相等

📝 具体示例

在插入的过程中,首先需要明确一点------手牌的初始为牌面向上 。(毕竟牌面向下都不能看到点数)

现在花火发下了第一张牌:
7 是根,因此我们把它翻一下:

规律一 :是的,从第一张牌上就能总结出一个规律------如果一张牌为根,即没有父亲牌,则将其置为牌面向下。
第二张牌:

按二叉搜索树的规定,将它摆在 7 的左下。又因为没有违反红黑树的规定,因此维持牌面向上:

第三张牌:

与第二张牌类似,直接放在正确位置即可:

规律二:现在我们可以总结出第二个规律了------如果插入牌的 父亲牌 为 牌面向下,则不用做其他调整。

花火发下第四张牌:

插入后,发现出事了:

按照规定,两张连续的手牌不能同时牌面向上。因此这里必须改变手牌的状态,改 Q明显不行,因为 97 都会违反左右路径 牌面向下的手牌数相同 这条游戏规则(之后简称游戏规则四),因此只能翻 9

现在只有 7 违反游戏规则四,这个简单,翻6就行了:

规律三--其一:经过上面这一次插入,可以总结一个规律,虽然不完整,但值得记录------如果插入牌的 父亲牌 为 牌面向上 ,插入牌的 叔叔牌 也为 牌面向上,那么直接翻转父亲牌与叔叔牌。

花火发下了第五张牌:

插入后,发现又出事了:

首先,插入牌的父亲牌为牌面向上,不能用规律二,且不存在叔叔牌,因此不能用规律三,最最重要的一点:该情况违反了红黑树节点( 9 )到空节点的最长路径不超过最短路径的两倍。

这时就只能进行旋转了------在AVL世界线中,安比、妮可和铃为我们演示了四种旋转,对应一下,发现这里是左单旋,因此操作对象是插入牌的 祖先牌 9RotateL(Card* 9),就能把 9 按下去:

现在需要做的就是翻牌了,为了保持 7 的右边路径只有一张手牌的牌面向下,我们将祖先牌 9 翻转、父亲牌 Q 翻转:

规律四--其一 :依然是个不完整的规律------如果父亲牌为牌面向上,叔叔牌不存在,需要根据形状进行旋转。
规律五--其一:在AVL中,我们关注的是平衡因子,在RB中,我们需要关注牌的状态。而这条规律记录的是旋转后应该如何翻牌------如果是单旋,将祖先牌、父亲牌翻转。

花火发下了第六张牌:

没关系,先插入再说:

我们一看,父亲牌、叔叔牌都为牌面向上,这不是规律三吗,于是将这两张牌翻转:

很显然,违反了游戏规则四,这时如果改插入牌、父亲牌、叔叔牌都不行,这会否定我们总结的规律三,因此只能改祖先牌:

这次就没问题了。
规律三--其二:经过这一次插入,我们发现我们所操作的可能只是牌堆的一部分,此处的祖先牌翻转后可能又会违反游戏规则,因此在调整后可能还会继续向上调整------将祖先牌翻转,并将祖先牌作为插入牌继续调整。

花火发下了第七张牌:

这简单,直接插入就行:

可能是看我们已经很熟练了,花火嘿嘿一笑,发下了第八张牌:

老规矩,不管什么牌,先放在正确位置再谈调整:

首先,我们看到的是父亲牌、叔叔牌的牌面都向上,规律三在此处适用:

现在将 9 作为插入牌,继续调整。

父亲牌为牌面向上,再看看叔叔牌,牌面向下!?之前的规律似乎没有针对这种情况的。

看看高度,好家伙,祖先牌 7 不满足左右高度相等,需要进行旋转。

再看看形状------V字形,符合AVL世界线中的右左双旋,根据安比、妮可、铃的演示,我们对这种旋转有了清晰的认识,因此直接两步到位:

先把插入牌、父亲牌、祖先牌拆开,插入牌与其左路径、右路径也拆开:

祖先牌变插入牌的左子牌,父亲牌变插入牌的右子牌,并重新连接:

最后翻转插入牌 9 ,翻转祖先牌 7,以维持当前局部牌面向下的牌数不变:

规律四--其二 :补充的规律四------如果父亲牌的牌面向上,叔叔牌的牌面向下,也要根据形状进行旋转。
规律五--其二:应该是最后一条规律了------如果进行双旋,将祖先牌、插入牌翻转。

💻 代码实现

花火一看我们竟然完成了较为复杂的插入,于是直接扔出五张牌:

手牌的点数都改了,我们能忍吗?当然不能。

于是掏出了电脑,来搓个红黑树的插入:

首先定义 手牌 Card

cpp 复制代码
template<class K,class V>
struct Card
{
	pair<K, V> _kv;
	Card<K, V>* _pLeft;
	Card<K, V>* _pRight;
	Card<K, V>* _pParent;
	int _status;

	Card(const pair<K,V>& kv)
		:_kv(kv)
		,_pLeft(nullptr)
		,_pRight(nullptr)
		,_pParent(nullptr)
		,_status(UP)
	{}
};

初始状态 _statusUP ,即牌面向上。

为了方便,我搞了几个宏定义:

cpp 复制代码
#define UP 1
#define DOWN -1
#define TURN_OVER ->_status*=-1
#define IS_UP ->_status==UP
#define IS_DOWN ->_status==DOWN

UP即牌面向上,DOWN即牌面向下,TURN_OVER用来翻牌,IS_UPIS_DOWN用来判断手牌的状态。

然后速速搭好树的框架:

cpp 复制代码
template<class K,class V>
class RBTree
{
	typedef Card<K, V> Card;
public:
	//Insert
private:
	Card* _root = nullptr;
};

接下来开始写插入。

首先,对每张手牌,都要先按二叉搜索树的规则放在正确的位置,这里就可以CV一下AVL的了:

cpp 复制代码
bool Insert(const pair<K, V>& kv)
{
	if (_root == nullptr)
	{
		_root = new Card(kv);
		_root->_status = DOWN;
		return true;
	}

	Card* cur = _root;
	Card* p = nullptr;
	while (cur)
	{
		if (cur->_kv.first == kv.first)return false;

		p = cur;
		if (cur->_kv.first < kv.first)cur = cur->_pRight;
		else cur = cur->_pLeft;
	}

	cur = new Card(kv);
	if (p->_kv.first < kv.first)p->_pRight = cur;
	else p->_pLeft = cur;
	cur->_pParent = p;

接下来就是调整手牌的状态了。我把刚刚总结的规律汇总了一下:
规律一 :如果一张牌为根,即 没有父亲牌,则将其置为牌面向下。
规律二 :如果插入牌的 父亲牌 为 牌面向下,则不用做其他调整。
规律三 :如果插入牌的 父亲牌 为 牌面向上 ,插入牌的 叔叔牌 也为 牌面向上,那么翻转父亲牌、叔叔牌、祖先牌,并将祖先牌作为插入牌继续调整。
规律四 :如果父亲牌为牌面向上,叔叔牌 不存在(空) 或 牌面向下,需要根据形状进行旋转。
规律五:如果是单旋,将祖先牌、父亲牌翻转;如果进行双旋,将祖先牌、插入牌翻转。

因此,循环的条件可以写为父亲牌存在且牌面向上:

cpp 复制代码
	while (p && p IS_UP){

然后将祖先牌,叔叔牌拿到:

cpp 复制代码
		Card* g = p->_pParent;
		Card* u = g->_pLeft == p ? g->_pRight : g->_pLeft;

如果叔叔牌存在且牌面向上,翻转三张牌,并继续向上调整(continue)

cpp 复制代码
		if (u && u IS_UP)
		{
			u TURN_OVER, g TURN_OVER, p TURN_OVER;
			cur = g; p = cur->_pParent;
			continue;
		}

如果叔叔牌不存在或牌面向下,需要根据形状进行旋转,并翻转对应的手牌,然后退出循环(break):

cpp 复制代码
		else if (p == g->_pLeft && cur == p->_pLeft)
			RotateR(g), p TURN_OVER, g TURN_OVER;
		else if (p == g->_pRight && cur == p->_pRight)
			RotateL(g), p TURN_OVER, g TURN_OVER;
		else if (p == g->_pLeft && cur == p->_pRight)
			RotateL(p), RotateR(g), cur TURN_OVER, g TURN_OVER;
		else if (p == g->_pRight && cur == p->_pLeft)
			RotateR(p), RotateL(g), cur TURN_OVER, g TURN_OVER;
		break;
	}

有三种方式到达循环外:

  • 一种是进行了翻转,这种情况不需要做任何调整;
  • 还有一种是父亲牌的牌面向下,这种情况也不需要做任何调整;
  • 最后一种情况是没有父亲牌,此时插入的手牌为根,需要将状态调整为牌面向下。

因此,为了不做多余的判断,这里统一让 根 保持牌面向下:

cpp 复制代码
	_root->_status = DOWN;
	return true;
}

旋转的函数去CV一下AVL的就行,记得删掉平衡因子。

🖼️示意图

最后来复个盘:


希望本篇文章对你有所帮助!并激发你进一步探索编程的兴趣!

本人仅是个C语言初学者,如果你有任何疑问或建议,欢迎随时留言讨论!让我们一起学习,共同进步!

相关推荐
‘’林花谢了春红‘’4 小时前
C++ list (链表)容器
c++·链表·list
搬砖的小码农_Sky6 小时前
C语言:数组
c语言·数据结构
机器视觉知识推荐、就业指导6 小时前
C++设计模式:建造者模式(Builder) 房屋建造案例
c++
先鱼鲨生8 小时前
数据结构——栈、队列
数据结构
一念之坤8 小时前
零基础学Python之数据结构 -- 01篇
数据结构·python
IT 青年8 小时前
数据结构 (1)基本概念和术语
数据结构·算法
Yang.998 小时前
基于Windows系统用C++做一个点名工具
c++·windows·sql·visual studio code·sqlite3
熬夜学编程的小王8 小时前
【初阶数据结构篇】双向链表的实现(赋源码)
数据结构·c++·链表·双向链表
zz40_8 小时前
C++自己写类 和 运算符重载函数
c++