【C++STL】红黑树(RBTree)

目录

一.什么是红黑树

二.红黑树和AVL树的区别

三.红黑树的性质

四.红黑树的插入操作

4.1.按照二叉搜索树的方式插入新节点

4.2.检测插入的节点是否破坏了红黑树的规则

4.2.1.场景1:红黑树为空树

4.2.2.情景2:插入节点的父节点为黑色

4.2.3.情景3:插入节点的父节点为红色

4.2.4.1.场景3.1:父亲和叔叔为红色节点

[4.2.4.2. 场景3.2:叔叔为黑色,父亲为红色,并且父亲节点是祖父节点的左子节点](#4.2.4.2. 场景3.2:叔叔为黑色,父亲为红色,并且父亲节点是祖父节点的左子节点)

4.2.4.3.情景3.3:叔叔为黑节点,父亲为红色,并且父亲节点是祖父节点的右子节点

4.3.插入操作完整代码

五.旋转操作

5.1.左旋

5.2.右旋

六.检查操作

七.完整代码+测试


一.什么是红黑树

红⿊树(简称RBT),也是⼀棵二叉搜索树。

它是在搜索树的基础上,使每个结点上增加⼀个存储位表⽰ 结点的颜⾊,可以是Red或者Black,通过对任意⼀条从根到叶⼦的路径上各个结点着⾊⽅式的限 制,确保没有⼀条路径会⽐其他路径⻓出2倍,因⽽是**⼀棵接近平衡的二叉搜索树**。

在⼀棵红⿊树中,需要满⾜下⾯⼏条规则,在每次插⼊和删除之后,都应该让红⿊树满⾜下⾯的规 则:

    1. 每个结点要么是红⾊要么是⿊⾊;
    1. 根节点和叶⼦结点这⾥的叶⼦结点不是常规意义上的叶⼦结点,⽽是空结点 ,如下图中的NIL) 是⿊⾊的;
  • 3. 如果⼀个结点是红⾊的,则它的两个孩⼦结点必须是⿊⾊的 ,也就是说任意⼀条路径不会有连续的红⾊结点;
  • 4. 对于任意⼀个结点,从该结点到其所有叶⼦结点的路径上,均包含相同数量的⿊⾊结点。

注意NIL节点是空节点,不是普通意义上的叶子节点。

例如下⾯的红⿊树:

注意NIL是空结点。

简单练习:判断下⾯的树,是否是红⿊树,为了简介,叶⼦结点就不画了

  • 图一不是红黑树,因为出现两个连续的红色

  • 图二不是红黑树,因为根节点应该是黑色的

  • 图三不是红黑树,因为从根节点到叶子节点的所有路径中的黑色节点数目不同

  • 图四不是红黑树,红黑树的首要前提应该是二叉搜索树

图五图六均是红黑树

二.红黑树和AVL树的区别

红黑树和AVL树都是自平衡的二叉搜索树,它们通过维护某种平衡性质来保证查找、插入和删除操作的时间复杂度为 O(log n)。但它们维护平衡的严格程度和实现方式有所不同,这也导致了它们在性能和应用场景上的差异。

核心区别:平衡标准

  • AVL树 :是严格平衡的。它要求任何节点的左右子树的高度差(平衡因子)的绝对值不超过1。这是一种非常严格的平衡条件。

  • 红黑树 :是弱平衡 的。它不追求绝对的"高度平衡",而是通过一套基于颜色的规则(节点非红即黑)来确保最长路径不超过最短路径的两倍 。这是一种近似平衡,但足以保证对数级别的时间复杂度。

红⿊树相对于AVL树来说,牺牲了部分平衡性以换取插⼊/删除操作时少量的旋转操作,整体来说性能 要优于AVL树。

特性 AVL树 红黑树
平衡标准 高度平衡:任意节点左右子树高度差 ≤ 1。 弱平衡:确保从根到叶子的最长路径不超过最短路径的2倍。
查询效率 更高。因为树更矮、更平衡,查找次数(比较次数)更少。 相对较低。树的高度理论上可能比AVL树略高(最多高一倍),但依然是对数级别,在百万级数据中,多几次比较的性能差异微乎其微。
插入/删除效率 较低。为了维护严格的平衡,插入和删除后可能需要更多的旋转操作(最多 O(log n) 次),尤其是在删除节点时。 更高。因为规则相对宽松,插入和删除操作破坏平衡的概率更低,需要的旋转次数更少(插入最多2次旋转,删除最多3次旋转)。
实现复杂度 相对简单。平衡规则(高度差)直观易懂,实现起来逻辑清晰。 相对复杂。需要处理红黑节点的颜色变换和多种旋转情况,逻辑分支较多。
  • **AVL树更适合查询多、修改少的场景。**因为它更严格的平衡性带来了最优的查找性能。例如,数据库中的索引(如MySQL的InnoDB引擎曾对AVL树进行过优化),或者一次构建后很少变动的查找表。
  • **红黑树更适合插入和删除操作频繁的场景。**因为它牺牲了部分查找性能,换取了更高效的修改效率。这也是为什么在绝大多数编程语言的标准库中(如Java的TreeMap、TreeSet,C++ STL的std::map、std::set),以及Linux内核的进程调度和内存管理中,都选择使用红黑树作为底层实现。

总的来说,选择哪种树取决于你的应用场景。如果对查找性能要求极致,可以考虑AVL树;如果插入删除操作也很多,红黑树是更平衡、更通用的选择。

三.红黑树的性质

根据红黑树的规则,我们可以得出红黑树的两个重要性质:

1. 从根结点到叶结点的最⻓路径不⼤于最短路径的2倍。

红黑树的四条规则确保了从根到叶子的最长路径不超过最短路径的两倍,具体原因如下:

  • **从任意节点到其所有叶子节点的路径上,黑色节点数量相同。**设从根到叶子的每条路径都有 bb 个黑色节点。

  • **红色节点不能有红色子节点,即路径上不能出现连续红色。**因此,在一条路径中,红色节点只能出现在黑色节点之间,且最多在每两个黑色节点之间插入一个红色。

  • 由于根节点和叶子节点(NIL)都是黑色(规则2),每条路径以黑色开始和结束,所以路径上的黑色节点数为 bb,则红色节点数最多为 b−1b−1(例如:黑-红-黑-红-...-黑)。

  • 因此,最长路径的总节点数为 b+(b−1)=2b−1,而最短路径(全黑)长度为 bb。显然 2b−1<2b,即最长路径小于最短路径的两倍,满足"不大于2倍"的要求。

这一性质保证了红黑树的平衡性,使得树的高度始终保持在 O(log⁡n) 级别。

直接看一个具体的例子感受感受。

如下图所示,最短路径最短有 3 个结点,全是黑色。因为不能出现连续的红色,所以想要最长,必须得是红⿊相间的形式,最长就是 5,不会超过最短路径的 2 倍。


2. 有 n 个结点的红⿊树,高度 h ≤ 2log2(n+1),这意味着查找时间复杂度为 O(logN)。


接下来我们就来实现一下我们的基本的红黑树,

首先我们需要枚举出红黑树的颜色

cpp 复制代码
// 枚举颜色:红色和黑色
enum Colour
{
	RED,   // 红色
	BLACK  // 黑色
};

还有红黑树节点的模板,我们也是需要去搞出来的。

cpp 复制代码
// 红黑树节点模板结构体
template<class T>
struct RBTreeNode
{
	RBTreeNode<T>* _left;   // 左孩子指针
	RBTreeNode<T>* _right;  // 右孩子指针
	RBTreeNode<T>* _parent; // 父节点指针
	
	T _data;                // 节点存储的数据
	Colour _col;            // 节点颜色

	// 构造函数,使用数据初始化节点
	RBTreeNode(const T& data)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_data(data)
		,_col(RED)          // 新节点默认为红色(红黑树性质规定)
	{}
};

然后我们就来定义这个红黑树的模板类

cpp 复制代码
// 红黑树模板类
template<class K, class V>
struct RBTree
{
    typedef RBTreeNode<K, V> Node;  // 节点类型别名

public:
......

private:
    Node* _root = nullptr;  // 根节点指针

public:
    int _rotateCount = 0;  // 旋转次数计数器(可用于性能分析)
};

四.红黑树的插入操作

4.1.按照二叉搜索树的方式插入新节点

第⼀步,也是先按照⼆叉搜索树的插⼊⽅式插⼊新的结点。

在这个过程中,我们其实需要思考一个问题:

新插入的节点是黑色的好还是红色的好?

这是一个非常经典且核心的问题。要理解为什么红黑树默认新插入的节点是红色 ,我们需要从红黑树的核心约束最小代价原则来思考。

简单直接的结论是:为了在插入时不破坏"根节点到所有叶子节点的路径上黑色节点数量相等"这一最难的规则。

如果插入的是黑色节点,会100%破坏这个规则,修复成本极高;如果插入的是红色节点,只可能会破坏"不能有连续红色节点"的规则,而这个规则相对容易修复。

下面我们来详细拆解这个过程。

  1. 先复习红黑树的五大规则

红黑树是一种自平衡的二叉搜索树,它的平衡依赖于以下五个规则:

  1. 节点颜色:每个节点要么是红色,要么是黑色。

  2. 根节点颜色:根节点是黑色。

  3. 叶子节点:所有叶子节点(NIL节点,即空节点)都是黑色。

  4. 红色节点规则(最难维护的规则):红色节点不能有红色的父节点和红色的子节点。(即不能出现连续的红色节点)。

  5. 黑色节点数量规则(最核心的平衡规则) :从任一节点到其每个叶子节点的所有路径,都包含相同数目的黑色节点。这也被称为黑高相等

  6. 假设插入的是黑节点

假设我们违背常规,强行插入一个黑色节点。

  • 发生了什么? 当我们把一个黑色节点插入到树中时,对于其他所有路径来说,黑色节点数不变,但这条新路径凭空多了一个黑色节点。

  • 结果 :这立即且100%地破坏了规则5(黑高相等)

  • 修复难度:这种破坏是"全局性"的。为了让所有路径的黑高重新相等,你可能需要从插入点一直回溯到根节点,调整整条路径甚至整棵树的颜色,甚至需要进行复杂的旋转操作。这就像是在一座平衡的天平一端突然加了一个很重的砝码,需要大动干戈才能恢复平衡。

  1. 假设插入的是红节点

现在,我们来分析默认做法:插入红色节点。

  • 发生了什么?

    • 规则5(黑高相等):红色节点的插入,并没有改变任何路径上黑色节点的数量。因此,规则5被完美地保留了下来

    • 规则2和3:与根节点和叶子节点无关,暂时不考虑。

  • 结果

    • 唯一可能被破坏的规则是规则4(不能有连续的红色节点)

    • 如果新插入的红色节点的父节点是黑色,那么万事大吉,所有规则都满足,插入完成。

    • 如果新插入的红色节点的父节点恰好也是红色,那么规则4被破坏。这种情况就是我们通常所说的"需要修复"。

  1. 为什么选择红色?------ 局部性修复的优势

通过上面的对比,可以清晰地看到两种选择的差异:

  • 插黑色 :必然破坏规则5,需要全局修复,成本极高。

  • 插红色 :可能破坏规则4,但规则5(最重要的平衡规则)依然保持。破坏规则4的修复是局部修复

"局部修复"具体指什么?

当出现"红-红"冲突时,我们可以通过变色旋转来解决。有趣的是,这些操作通常只涉及当前节点、父节点、叔父节点和祖父节点。

红黑树的修复算法设计得非常巧妙,它总是在小范围内(局部)解决问题,并将可能产生的"冲突"向上层(祖父节点)推移。

  • 如果叔父节点是红色,通过变色就能解决,然后把祖父节点当作新插入的节点继续检查(冲突上移)。

  • 如果叔父节点是黑色/空,通过1-2次旋转加变色就能解决,且子树恢复平衡后,黑色节点数不变,不会影响树的其余部分。


因此红⿊树的插⼊过程⼤致为:

  • 1. 按照⼆叉搜索树的插⼊⽅式插⼊新的结点;
  • 2. 默认该点是红⾊,如果破坏了红⿊树的规则,然后就分情况讨论。

接下来,就详细讨论插⼊新结点之后会遇到的所有情况,以及每种情况需要如何调整。 为了后续叙述⽅便,标记新插⼊的结点为c(cur),⽗结点为p(parent),⽗亲的⽗结点为 g(grandfather),⽗结点的兄弟为u(uncle)。

4.2.检测插入的节点是否破坏了红黑树的规则

4.1.1 情况⼀:插⼊的是根节点

这是第⼀次插⼊结点,直接将结点的颜⾊变成⿊⾊即可。

4.1.2 情况⼆:叔叔是红⾊

这种情况下,不需要旋转,只需要不断变⾊即可。

具体的策略是:

• ⽗亲、叔叔和爷爷同时变⾊,然后将爷爷看做新插⼊的结点,继续向上判断。

  1. p和u变成⿊⾊,g变成红⾊。这样就相当于g所在的⼦树增加⼀个⿊⾊结点,⼜减少⼀个⿊⾊结 点,整个路径⿊⾊结点的数量就不会改变。
  1. 但是,由于g变成红⾊,有可能g的⽗亲也是红⾊,或者g是⼀个根,此时就需要把g当成新插⼊ 的结点,继续判断。

4.1.3 情况三:叔叔是⿊⾊

这种情况需要继续分类讨论,根据祖⽗、⽗亲、新结点三者的位置,分情况旋转+变⾊。这⼀块的旋 转操作和平衡⼆叉树的旋转⼀样,⽆⾮就是多了⼀个变⾊,因此不需要有太⼤的⼼理负担~

LL 型-右单旋+⽗爷变⾊

如果⽗亲和新结点的位置关系相对于爷爷呈现:新结点是爷爷的左孩⼦的左孩⼦,仅需两步:

  • 右旋⽗亲结点;

  • 然后将⽗亲和爷爷变⾊

RR 型-左单旋+⽗爷变⾊

如果⽗亲和新结点的位置关系相对于爷爷呈现:新结点是爷爷的右孩⼦的右孩⼦,仅需两步:

  1. 左旋⽗亲结点;
  2. 然后将⽗亲和爷爷变⾊

LR 型-左右双旋+⼉爷变⾊

如果⽗亲和新结点的位置关系相对于爷爷呈现:新结点是爷爷的左孩⼦的右孩⼦,仅需两步:

  1. 新结点先左旋,再右旋;
  2. 然后将新结点和爷爷结点变⾊

RL 型-右左双旋+⼉爷变⾊

如果⽗亲和新结点的位置关系相对于爷爷呈现:新结点是爷爷的右孩⼦的左孩⼦,仅需两步:

  1. 新结点先右旋,再左旋;
  2. 然后将新结点和爷爷结点变⾊

4.2.1.场景1:红黑树为空树

直接把插入结点作为根节点就可以了

另外:根据红黑树的性质:根节点是黑色的。还需要把插入节点设置为黑色。

4.2.2.情景2:插入节点的父节点为黑色

由于插入的节点是红色的,当插入节点的父节点是黑色时,不会影响红黑树的平衡,

所以: 直接插入无需做自平衡

4.2.3.情景3:插入节点的父节点为红色

那么针对上面情景1和情景2我们是不做太多处理的。那么我们特别需要注意的就是插入节点的父节点为红色这个情况。这个情况就复杂的多。


根据红黑树的性质:根节点是黑色。

如果插入节点的父节点为红色节点,那么该父节点不可能为根节点,所以插入节点总是存在祖父节点(三代关系)。

根据性质:每个红色 节点的两个子节点一定是黑色 的。不能有两个红色节点相连

此时会出现两种状态:

  • 父亲和叔叔为红色

  • 父亲为红色,叔叔为黑色

如图

4.2.4.1.场景3.1:父亲和叔叔为红色节点

根据性质4:红色节点不能相连 ==》祖父节点肯定为黑色节点:

父亲为红色,那么此时该插入子树的红黑树层数的情况是:黑红红。

因为不可能同时存在两个相连的红色节点,需要进行 变色, 显然处理方式是把其改为:红黑红

变色 处理:黑红红 ==> 红黑红

1.将F和V节点改为黑色

2.将P改为红色

3.将P设置为当前节点,进行后续处理

可以看到,将P设置为红色了,

如果P的父节点是黑色,那么无需做处理;

但如果P的父节点是红色,则违反红黑树性质了,所以需要将P设置为当前节点,继续插入操作, 作自平衡处理,直到整体平衡为止。

无论插入节点的父节点是祖父节点的左孩子还是右孩子,都是按照上面这些步骤来进行处理的。

有人可能会好奇:为什么在红黑树插入修复中,当父亲和叔叔都是红色节点时(即"黑红红"结构),正确的变色处理是将父亲和叔叔改为黑色,祖父改为红色(得到"红黑黑"),而不是其他方案,比如将新插入的节点改为黑色(得到"黑红黑")?

要理解这一点,关键在于维护红黑树的核心性质:从任一节点到其每个叶子节点的所有路径都包含相同数目的黑色节点。我们以一个具体的局部结构为例,假设祖父节点为黑色,父亲和叔叔为红色,新插入的节点为红色(即"黑红红")。为简化分析,暂不考虑更深层的子树,并假设这些节点的叶子(NIL)均为黑色。

初始状态(黑红红)

  • 祖父(黑)

  • 父亲(红)

  • 叔叔(红)

  • 新节点(红)

  • 从祖父到各个叶子的路径上的黑色节点数(包括叶子本身):

    • 经过父亲再到新节点:祖父(黑)→父亲(红)→新节点(红)→叶子(黑),黑色节点:祖父、叶子,共2个。

    • 经过父亲到另一个叶子(假设父亲有另一个孩子为叶子):祖父(黑)→父亲(红)→叶子(黑),黑色节点:祖父、叶子,共2个。

    • 经过叔叔到叶子:祖父(黑)→叔叔(红)→叶子(黑),黑色节点:祖父、叶子,共2个。

      所有路径黑色节点数均为2,满足性质5。

方案一:将新节点改为黑色(得到"黑红黑")

  • 祖父(黑)

  • 父亲(红)

  • 叔叔(红)

  • 新节点(黑)

  • 此时路径黑色节点数:

    • 经过父亲再到新节点:祖父(黑)→父亲(红)→新节点(黑)→叶子节点(黑),黑色节点:祖父、新节点、叶子,共3个。

    • 经过父亲到另一个叶子:祖父(黑)→父亲(红)→叶子节点(黑),黑色节点:祖父、叶子,共2个。

    • 经过叔叔到叶子:祖父(黑)→叔叔(红)→叶子节点(黑),黑色节点:祖父、叶子,共2个。

    • 可见,经过新节点的路径黑色节点数变成了3,而其他路径仍为2,破坏了红黑树的性质:从任一节点到其每个叶子节点的所有路径都包含相同数目的黑色节点。因此不可行。

方案二:将父亲和叔叔改为黑色,祖父保持黑色(得到"黑黑红")

  • 祖父(黑)

  • 父亲(黑)

  • 叔叔(黑)

  • 新节点(红)

  • 路径黑色节点数:

    • 经过父亲再到新节点:祖父(黑)→父亲(黑)→新节点(红)→叶子(黑),黑色节点:祖父、父亲、叶子,共3个。

    • 经过父亲到另一个叶子:祖父(黑)→父亲(黑)→叶子(黑),黑色节点:祖父、父亲、叶子,共3个。

    • 经过叔叔到叶子:祖父(黑)→叔叔(黑)→叶子(黑),黑色节点:祖父、叔叔、叶子,共3个。

    • 所有路径黑色节点数均变为3,局部看似平衡,但请注意:从祖父出发的每条路径都增加了1个黑色节点。如果祖父不是根节点那么从根到祖父的路径上的黑色节点数并未改变,而经过这个子树的路径却多了一个黑 ,导致整棵树中,经过该子树的路径比其他路径多一个黑节点,同样破坏了红黑树全局性质:从任一节点到其每个叶子节点的所有路径都包含相同数目的黑色节点。因此,必须保持从祖父出发的路径黑色节点数不变,才能避免向上层扩散影响。

正确方案:将父亲和叔叔改为黑色,祖父改为红色(得到"红黑黑")

  • 祖父(红)

  • 父亲(黑)

  • 叔叔(黑)

  • 新节点(红)

  • 路径黑色节点数:

    • 经过父亲再到新节点:祖父(红)→父亲(黑)→新节点(红)→叶子(黑),黑色节点:父亲、叶子,共2个。

    • 经过父亲到另一个叶子:祖父(红)→父亲(黑)→叶子(黑),黑色节点:父亲、叶子,共2个。

    • 经过叔叔到叶子:祖父(红)→叔叔(黑)→叶子(黑),黑色节点:叔叔、叶子,共2个。

    • 所有路径黑色节点数仍为2,与初始相同,局部平衡得以维持。虽然祖父变为红色可能与其父节点产生新的红色冲突,但这正是将问题向上传递的机制,通过后续迭代修复即可最终达到全局平衡。

注意:我们上面说的叶子节点就是NIL结点。

4.2.4.2. 场景3.2:叔叔为黑色,父亲为红色,并且父亲节点是祖父节点的左子节点

叔叔为黑色,或者不存在(NIL)也是黑节点,并且节点的父亲节点是祖父节点的左子节点

注意:单纯从插入来看,叔叔节点非红即黑(NIL节点),否则破坏了红黑树性质:从任一节点到其每个叶子节点的所有路径都包含相同数目的黑色节点,此时路径会比其他路径多一个黑色节点。

场景4.2.1 LL型失衡

细分场景 1: 新插入节点,为其父节点的左子节点(LL红色情况), 插入后 就是LL 型失衡

自平衡处理:

  • 1.变颜色:将F设置为黑色,将P设置为红色
  • 2.对F节点进行右旋

场景4.2.2 LR型失衡

细分场景 2: 新插入节点,为其父节点的右子节点(LR红色情况), 插入后 就是LR 型失衡

自平衡处理:

  • 1.对F结点进行左旋
  • 2.变色:P结点变为红色,K结点变为黑色
  • 3.对P结点进行右旋
4.2.4.3.情景3.3:叔叔为黑节点,父亲为红色,并且父亲节点是祖父节点的右子节点

这个情况其实也分两种情况:

情景1:RR型失衡

新插入节点,为其父节点的右子节点(RR红色情况)

自平衡处理:

  • 1.变色:将F设置为黑色,将P设置为红色
  • 2.对P节点进行左旋

情景2:RL型失衡

新插入节点,为其父节点的左子节点(RL红色情况)

自平衡处理:

  • 1.对F结点进行右旋
  • 2.变色:P结点变为红色,K结点变为黑色
  • 3.对P结点进行左旋

4.3.插入操作完整代码

那么按照上面的分析,我们也就很快就能写出下面这个代码

cpp 复制代码
// 插入函数,返回是否插入成功(键不能重复)
    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;
        }
        else
        {
            parent->_left = cur;
        }
        cur->_parent = parent;  // 设置新节点的父指针

		//二.默认新插入的结点是红⾊,如果破坏了红⿊树的规则,然后就分情况讨论。

        // 插入后修复红黑树性质(防止连续红色节点)
		//注意:在整个循环里面cur一直都是是红色节点
        while (parent && parent->_col == RED)  // 父节点为红色,说明出现了连续的两个红结点,那么我们就需要调整
        {
            Node* grandfather = parent->_parent;  // 祖父节点
            if (parent == grandfather->_left)     // 父节点是祖父的左孩子
            {
                Node* uncle = grandfather->_right; // 叔叔节点(祖父的右孩子)
                // 情况1:叔叔存在且为红色,对应博客上的场景3.1
                if (uncle && uncle->_col == RED)
                {
                    // 变色:父和叔变黑,祖父变红
                    parent->_col = uncle->_col = BLACK;
                    grandfather->_col = RED;

                    // 继续向上处理,将祖父作为新的当前节点
                    cur = grandfather;
                    parent = cur->_parent;
                }
                else // 叔叔不存在或为黑色,对应博客上的场景3.2.
                {
                    if (cur == parent->_left)  // 情况2:cur是父的左孩子(LL型)
                    {
                        //     g
                        //   p
                        // c
                        // 变色:父变黑,祖父变红
                        parent->_col = BLACK;
                        grandfather->_col = RED;
                        // 右旋祖父
                        RotateR(grandfather);      
                    }
                    else // 情况3:cur是父的右孩子(LR型)
                    {
                        //     g
                        //   p
                        //		c
                        // 先左旋父
                        RotateL(parent);
                        // 变色:cur变黑,祖父变红
                        cur->_col = BLACK;
                        grandfather->_col = RED;
                        //再右旋祖父
                        RotateR(grandfather);
                    }
                    // 调整完成,退出循环
                    break;
                }
            }
            else // parent == grandfather->_right,父节点是祖父的右孩子(对称情况)
            {
                Node* uncle = grandfather->_left; // 叔叔节点(祖父的左孩子)
                // 情况1:叔叔存在且为红色,对应博客上的场景3.1
                if (uncle && uncle->_col == RED)
                {
                    // 变色:父和叔变黑,祖父变红
                    parent->_col = uncle->_col = BLACK;
                    grandfather->_col = RED;

                    // 继续向上处理
                    cur = grandfather;
                    parent = cur->_parent;
                }
                else // 叔叔不存在或为黑色
                {
                    if (cur == parent->_right) // 情况2:cur是父的右孩子(直线型)
                    {
                        // g
                        //	  p
                        //       c
                        // 变色:父变黑,祖父变红
                        grandfather->_col = RED;
                        parent->_col = BLACK;
                        // 左旋祖父
                        RotateL(grandfather);
                    }
                    else // 情况3:cur是父的左孩子(折线型)
                    {
                        // g
                        //	  p
                        // c
                        // 先右旋父
                        RotateR(parent);
                        // 变色:cur变黑,祖父变红
                        cur->_col = BLACK;
                        grandfather->_col = RED;
                        //左旋祖父
                        RotateL(grandfather);
                    }
                    // 调整完成
                    break;
                }
            }
        }

        // 根节点必须始终为黑色
        _root->_col = BLACK;

        return true;
    }

五.旋转操作

5.1.左旋

那么对于红黑树的左旋,其实和AVL树是类似的。只不过AVL树需要处理平衡因子。但是我们红黑树就没有必要处理平衡因子了。

我们可以简单的看看这个左旋的示意图

具体步骤:

  1. b成为X的右子树
  2. X成为Y的左子树
  3. 处理X结点和X结点原来的父节点的关系
  4. 处理Y结点和X结点原来的父节点的关系

注意我们代码里面

  • parent指向的是X
  • cur指向的是Y
  • curleft指向的是b
cpp 复制代码
// 左旋转操作(以parent为轴)
    void RotateL(Node* parent)
    {
        ++_rotateCount;  // 统计旋转次数(辅助调试)

        Node* cur = parent->_right;   // cur是parent的右孩子
        Node* curleft = cur->_left;    // cur的左子树

        // 步骤1:将cur的左子树链接为parent的右子树
        parent->_right = curleft;
        if (curleft)
        {
            curleft->_parent = parent;
        }

        // 步骤2:将parent变为cur的左孩子
        cur->_left = parent;

        // 步骤3:处理parent原来的父节点
        Node* ppnode = parent->_parent;  // 保存parent的原父节点
        parent->_parent = cur;            // parent的父指针指向cur

        // 步骤4:将cur与上层链接
        if (parent == _root)  // 如果parent是根,旋转后cur成为新根
        {
            _root = cur;
            cur->_parent = nullptr;
        }
        else  // 否则将上层节点的相应孩子指向cur
        {
            if (ppnode->_left == parent)
            {
                ppnode->_left = cur;
            }
            else
            {
                ppnode->_right = cur;
            }
            cur->_parent = ppnode;
        }
    }

5.2.右旋

我们看看右旋的示意图

具体步骤:

  1. b成为Y的左子树
  2. Y成为X的右子树
  3. 处理Y结点和Y结点原来的父节点的关系
  4. 处理X结点和Y结点原来的父节点的关系

注意我们代码里面

  • parent指向Y
  • cur指向的是X
  • curright指向的是b
cpp 复制代码
// 右旋转操作(以parent为轴)
    void RotateR(Node* parent)
    {
        ++_rotateCount;  // 统计旋转次数

        Node* cur = parent->_left;    // cur是parent的左孩子
        Node* curright = cur->_right;  // cur的右子树

        // 步骤1:将cur的右子树链接为parent的左子树
        parent->_left = curright;
        if (curright)
        {
            curright->_parent = parent;
        }

        // 步骤2:将parent变为cur的右孩子
        cur->_right = parent;

        // 步骤3:处理parent原来的父节点
        Node* ppnode = parent->_parent;
        parent->_parent = cur;

        // 步骤4:将cur与上层链接
        if (ppnode == nullptr)  // 如果parent是根,旋转后cur成为新根
        {
            _root = cur;
            cur->_parent = nullptr;
        }
        else
        {
            if (ppnode->_left == parent)
            {
                ppnode->_left = cur;
            }
            else
            {
                ppnode->_right = cur;
            }
            cur->_parent = ppnode;
        }
    }

六.检查操作

首先,我们需要检测这个红黑树是不是符合它的几条性质。

检测性质1:红色节点不能连续出现(即红色节点的父节点和子节点不能是红色)

cpp 复制代码
if (root->_col == RED && root->_parent && root->_parent->_col == RED)
{
    cout << root->_kv.first << "出现连续红色节点" << endl;
    return false;
}

该检查确保在任意路径上,不会出现两个相邻的红色节点。如果当前节点为红色且其父节点也存在且为红色,则违反性质,函数返回 false 并输出提示。

检测性质2:从任一节点到其每个叶子节点的所有路径都包含相同数目的黑色节点(即黑高相等)

在递归过程中,用参数 blacknum 记录从根到当前节点路径上遇到的黑色节点个数。

当递归到空节点(即叶子节点)时,将当前路径的黑色节点数 blacknum 与基准值 benchmark 进行比较:

cpp 复制代码
if (root == nullptr)
{
    if (blacknum != benchmark)
        return false;
    return true;
}

基准值 benchmark 通常是在调用此函数前,通过从根一直向左走(或任意路径)计算出的标准黑色节点数。函数确保每一条从根到叶子的路径上的黑色节点数都与该基准值相等,从而满足性质5。

完整代码

cpp 复制代码
// 递归检查节点的颜色和黑高是否满足红黑树性质
    bool CheckColour(Node* root, int blacknum, int benchmark)
    {
        if (root == nullptr)  // 到达空节点,检查当前路径的黑节点数是否等于基准值
        {
            if (blacknum != benchmark)
                return false;
            return true;
        }

        if (root->_col == BLACK)  // 遇到黑节点,累加计数
        {
            ++blacknum;
        }

        // 检查是否存在连续的红色节点
        if (root->_col == RED && root->_parent && root->_parent->_col == RED)
        {
            cout << root->_kv.first << "出现连续红色节点" << endl;
            return false;
        }

        // 递归检查左右子树
        return CheckColour(root->_left, blacknum, benchmark)
            && CheckColour(root->_right, blacknum, benchmark);
    }

那么问题来了,这个基准值 benchmark从哪里来??那么其实还需要引入一个函数

cpp 复制代码
// 检查以root为根的子树是否平衡
    bool IsBalance(Node* root)
    {
        if (root == nullptr)
            return true;

        // 根节点必须为黑色
        if (root->_col != BLACK)
        {
            return false;
        }

        // 计算基准黑高:任意一条路径上的黑节点数(这里取最左路径)
        int benchmark = 0;
        Node* cur = _root;
        while (cur)
        {
            if (cur->_col == BLACK)
                ++benchmark;
            cur = cur->_left;
        }

        // 调用递归检查函数
        return CheckColour(root, 0, benchmark);
    }

这个函数里面额外检测了:根节点必须是黑色

七.完整代码+测试

RBTree.hpp

cpp 复制代码
#pragma once
#include<iostream>
using namespace std;

// 枚举颜色,用于表示红黑树节点的颜色
enum Colour
{
    RED,   // 红色
    BLACK  // 黑色
};

// 红黑树节点模板结构体
template<class K, class V>
struct RBTreeNode
{
    RBTreeNode<K, V>* _left;   // 左孩子指针
    RBTreeNode<K, V>* _right;  // 右孩子指针
    RBTreeNode<K, V>* _parent; // 父节点指针

    pair<K, V> _kv;            // 键值对
    Colour _col;               // 节点颜色

    // 构造函数,使用键值对初始化,默认颜色为红色
    RBTreeNode(const pair<K, V>& kv)
        :_left(nullptr)
        ,_right(nullptr)
        ,_parent(nullptr)
        ,_kv(kv)
        ,_col(RED)
    {}
};

// 红黑树模板类
template<class K, class V>
struct 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;
        }
        else
        {
            parent->_left = cur;
        }
        cur->_parent = parent;  // 设置新节点的父指针

		//二.默认新插入的结点是红⾊,如果破坏了红⿊树的规则,然后就分情况讨论。

        // 插入后修复红黑树性质(防止连续红色节点)
		//注意:在整个循环里面cur一直都是是红色节点
        while (parent && parent->_col == RED)  // 父节点为红色,说明出现了连续的两个红结点,那么我们就需要调整
        {
            Node* grandfather = parent->_parent;  // 祖父节点
            if (parent == grandfather->_left)     // 父节点是祖父的左孩子
            {
                Node* uncle = grandfather->_right; // 叔叔节点(祖父的右孩子)
                // 情况1:叔叔存在且为红色,对应博客上的场景3.1
                if (uncle && uncle->_col == RED)
                {
                    // 变色:父和叔变黑,祖父变红
                    parent->_col = uncle->_col = BLACK;
                    grandfather->_col = RED;

                    // 继续向上处理,将祖父作为新的当前节点
                    cur = grandfather;
                    parent = cur->_parent;
                }
                else // 叔叔不存在或为黑色,对应博客上的场景3.2.
                {
                    if (cur == parent->_left)  // 情况2:cur是父的左孩子(LL型)
                    {
                        //     g
                        //   p
                        // c
                        // 变色:父变黑,祖父变红
                        parent->_col = BLACK;
                        grandfather->_col = RED;
                        // 右旋祖父
                        RotateR(grandfather);      
                    }
                    else // 情况3:cur是父的右孩子(LR型)
                    {
                        //     g
                        //   p
                        //		c
                        // 先左旋父
                        RotateL(parent);
                        // 变色:cur变黑,祖父变红
                        cur->_col = BLACK;
                        grandfather->_col = RED;
                        //再右旋祖父
                        RotateR(grandfather);
                    }
                    // 调整完成,退出循环
                    break;
                }
            }
            else // parent == grandfather->_right,父节点是祖父的右孩子(对称情况)
            {
                Node* uncle = grandfather->_left; // 叔叔节点(祖父的左孩子)
                // 情况1:叔叔存在且为红色,对应博客上的场景3.1
                if (uncle && uncle->_col == RED)
                {
                    // 变色:父和叔变黑,祖父变红
                    parent->_col = uncle->_col = BLACK;
                    grandfather->_col = RED;

                    // 继续向上处理
                    cur = grandfather;
                    parent = cur->_parent;
                }
                else // 叔叔不存在或为黑色
                {
                    if (cur == parent->_right) // 情况2:cur是父的右孩子(直线型)
                    {
                        // g
                        //	  p
                        //       c
                        // 变色:父变黑,祖父变红
                        grandfather->_col = RED;
                        parent->_col = BLACK;
                        // 左旋祖父
                        RotateL(grandfather);
                    }
                    else // 情况3:cur是父的左孩子(折线型)
                    {
                        // g
                        //	  p
                        // c
                        // 先右旋父
                        RotateR(parent);
                        // 变色:cur变黑,祖父变红
                        cur->_col = BLACK;
                        grandfather->_col = RED;
                        //左旋祖父
                        RotateL(grandfather);
                    }
                    // 调整完成
                    break;
                }
            }
        }

        // 根节点必须始终为黑色
        _root->_col = BLACK;

        return true;
    }

    // 公开的平衡性检查接口
    bool IsBalance()
    {
        return IsBalance(_root);
    }

    // 公开的树高度计算接口
    int Height()
    {
        return Height(_root);
    }

    // 中序遍历打印红黑树(键升序)
    void InOrder()
    {
        _InOrder(_root);
        cout << endl;
    }

private:


private:

    // 左旋转操作(以parent为轴)
    void RotateL(Node* parent)
    {
        ++_rotateCount;  // 统计旋转次数(辅助调试)

        Node* cur = parent->_right;   // cur是parent的右孩子
        Node* curleft = cur->_left;    // cur的左子树

        // 步骤1:将cur的左子树链接为parent的右子树
        parent->_right = curleft;
        if (curleft)
        {
            curleft->_parent = parent;
        }

        // 步骤2:将parent变为cur的左孩子
        cur->_left = parent;

        // 步骤3:处理parent原来的父节点
        Node* ppnode = parent->_parent;  // 保存parent的原父节点
        parent->_parent = cur;            // parent的父指针指向cur

        // 步骤4:将cur与上层链接
        if (parent == _root)  // 如果parent是根,旋转后cur成为新根
        {
            _root = cur;
            cur->_parent = nullptr;
        }
        else  // 否则将上层节点的相应孩子指向cur
        {
            if (ppnode->_left == parent)
            {
                ppnode->_left = cur;
            }
            else
            {
                ppnode->_right = cur;
            }
            cur->_parent = ppnode;
        }
    }

    // 右旋转操作(以parent为轴)
    void RotateR(Node* parent)
    {
        ++_rotateCount;  // 统计旋转次数

        Node* cur = parent->_left;    // cur是parent的左孩子
        Node* curright = cur->_right;  // cur的右子树

        // 步骤1:将cur的右子树链接为parent的左子树
        parent->_left = curright;
        if (curright)
        {
            curright->_parent = parent;
        }

        // 步骤2:将parent变为cur的右孩子
        cur->_right = parent;

        // 步骤3:处理parent原来的父节点
        Node* ppnode = parent->_parent;
        parent->_parent = cur;

        // 步骤4:将cur与上层链接
        if (ppnode == nullptr)  // 如果parent是根,旋转后cur成为新根
        {
            _root = cur;
            cur->_parent = nullptr;
        }
        else
        {
            if (ppnode->_left == parent)
            {
                ppnode->_left = cur;
            }
            else
            {
                ppnode->_right = cur;
            }
            cur->_parent = ppnode;
        }
    }

    // 递归检查节点的颜色和黑高是否满足红黑树性质
    bool CheckColour(Node* root, int blacknum, int benchmark)
    {
        if (root == nullptr)  // 到达空节点,检查当前路径的黑节点数是否等于基准值
        {
            if (blacknum != benchmark)
                return false;
            return true;
        }

        if (root->_col == BLACK)  // 遇到黑节点,累加计数
        {
            ++blacknum;
        }

        // 检查是否存在连续的红色节点
        if (root->_col == RED && root->_parent && root->_parent->_col == RED)
        {
            cout << root->_kv.first << "出现连续红色节点" << endl;
            return false;
        }

        // 递归检查左右子树
        return CheckColour(root->_left, blacknum, benchmark)
            && CheckColour(root->_right, blacknum, benchmark);
    }

    // 检查以root为根的子树是否平衡
    bool IsBalance(Node* root)
    {
        if (root == nullptr)
            return true;

        // 根节点必须为黑色
        if (root->_col != BLACK)
        {
            return false;
        }

        // 计算基准黑高:任意一条路径上的黑节点数(这里取最左路径)
        int benchmark = 0;
        Node* cur = _root;
        while (cur)
        {
            if (cur->_col == BLACK)
                ++benchmark;
            cur = cur->_left;
        }

        // 调用递归检查函数
        return CheckColour(root, 0, benchmark);
    }

    // 递归中序遍历辅助函数
    void _InOrder(Node* root)
    {
        if (root == nullptr)
            return;
        _InOrder(root->_left);
        // 输出格式: (键, 值, 颜色)
        cout << "(" << root->_kv.first << ", " << root->_kv.second << ", "
             << (root->_col == RED ? "RED" : "BLACK") << ") "<<endl;
        _InOrder(root->_right);
    }

    // 递归计算树的高度
    int Height(Node* root)
    {
        if (root == nullptr)
            return 0;

        int leftHeight = Height(root->_left);
        int rightHeight = Height(root->_right);

        return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
    }

private:
    Node* _root = nullptr;  // 根节点指针

public:
    int _rotateCount = 0;  // 旋转次数计数器(可用于性能分析)
};

至于这里出现了我上面没有提及的操作,都是和二叉搜索树差不多的。

test.cpp

cpp 复制代码
#include "RBTree.hpp"
#include <iostream>
#include <vector>
#include <cstdlib>
#include <ctime>
using namespace std;

// 辅助函数:打印分隔线
void printSeparator() {
    cout << "\n----------------------------------------\n";
}

int main() {
    // 1. 测试基本插入和平衡性
    cout << "=== 测试1:顺序插入 1~10 ===" << endl;
    RBTree<int, int> rbt1;
    for (int i = 1; i <= 10; ++i) {
        cout << "插入 (" << i << ", " << i * 10 << ")" << endl;
        rbt1.Insert({i, i * 10});
        // 每次插入后检查平衡性
        if (rbt1.IsBalance()) {
            cout << "  平衡性 OK" << endl;
        } else {
            cout << "  平衡性 失败!" << endl;
        }
    }
    cout << "树高度: " << rbt1.Height() << endl;
    cout << "旋转次数: " << rbt1._rotateCount << endl;
    cout << "中序遍历结果: "<<std::endl;
    rbt1.InOrder();
    printSeparator();

    // 2. 测试随机插入(包含重复键)
    cout << "=== 测试2:随机插入10个值(可能重复)===" << endl;
    RBTree<int, string> rbt2;
    srand(static_cast<unsigned>(time(nullptr)));
    vector<int> keys;
    for (int i = 0; i < 10; ++i) {
        int key = rand() % 20;  // 随机键范围0~19
        string value = "val" + to_string(key);
        cout << "尝试插入 (" << key << ", " << value << ")";
        bool success = rbt2.Insert({key, value});
        if (success) {
            cout << " 成功" << endl;
            keys.push_back(key);
        } else {
            cout << " 失败(键已存在)" << endl;
        }
    }
    cout << "树高度: " << rbt2.Height() << endl;
    cout << "旋转次数: " << rbt2._rotateCount << endl;
    cout << "平衡性检查: " << (rbt2.IsBalance() ? "通过" : "失败") << endl;
    cout << "中序遍历结果: ";
    rbt2.InOrder();
    printSeparator();

    // 3. 测试极端情况:递减序列
    cout << "=== 测试3:递减序列 10~1 ===" << endl;
    RBTree<int, int> rbt3;
    for (int i = 10; i >= 1; --i) {
        cout << "插入 (" << i << ", " << i * 10 << ")" << endl;
        rbt3.Insert({i, i * 10});
    }
    cout << "树高度: " << rbt3.Height() << endl;
    cout << "旋转次数: " << rbt3._rotateCount << endl;
    cout << "平衡性检查: " << (rbt3.IsBalance() ? "通过" : "失败") << endl;
    cout << "中序遍历结果: ";
    rbt3.InOrder();
    printSeparator();

    // 4. 测试重复键插入
    cout << "=== 测试4:重复键插入 ===" << endl;
    RBTree<int, string> rbt4;
    cout << "第一次插入 (5, \"five\"): ";
    bool first = rbt4.Insert({5, "five"});
    cout << (first ? "成功" : "失败") << endl;
    cout << "第二次插入 (5, \"five again\"): ";
    bool second = rbt4.Insert({5, "five again"});
    cout << (second ? "成功" : "失败(预期失败)") << endl;
    cout << "树高度: " << rbt4.Height() << endl;
    cout << "中序遍历结果: ";
    rbt4.InOrder();
    printSeparator();

    // 5. 测试空树和中序遍历
    cout << "=== 测试5:空树 ===" << endl;
    RBTree<int, int> rbt5;
    cout << "树高度: " << rbt5.Height() << endl;
    cout << "平衡性检查: " << (rbt5.IsBalance() ? "通过" : "失败") << endl;
    cout << "中序遍历结果: ";
    rbt5.InOrder();  // 应该只输出换行
    printSeparator();

    return 0;
}
相关推荐
我笑了OvO1 小时前
常见位运算及其经典算法题(1)
c++·算法·算法竞赛
Zevalin爱灰灰1 小时前
方法论——如何设计控制策略架构
算法·架构·嵌入式
wostcdk1 小时前
基础算法学习1
算法
Zik----1 小时前
Leetcode20 —— 有效的括号(栈解法)
数据结构
Yzzz-F1 小时前
2026牛客寒假算法基础集训营1
算法
野犬寒鸦1 小时前
Java8 ConcurrentHashMap 深度解析(底层数据结构详解及方法执行流程)
java·开发语言·数据库·后端·学习·算法·哈希算法
兩尛1 小时前
155最小栈/c++
开发语言·c++
白太岁1 小时前
Muduo:(2) EPollPoller 实现 epoll 封装、 fd 事件监听与事件通知
网络·c++·网络协议·tcp/ip
m0_531237171 小时前
C语言-函数递归练习
算法