从零开始 C++-----十一【C++ 数据结构】红黑树全解析:从定义到工程实现(一文搞定,十分详细)

系列文章目录

提示:这里是系列文章的专栏

并不喜欢吃鱼的C++专栏


提示:以下是文章目录哦!

文章目录

目录

系列文章目录

文章目录

前言

一、为什么需要红黑树?

二、红黑树的核心概念与规则

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

[2. 红黑树的五大核心规则(重点)](#2. 红黑树的五大核心规则(重点))

[3. 红黑树如何保证最长路径不超过最短路径的 2 倍?(关键问题)](#3. 红黑树如何保证最长路径不超过最短路径的 2 倍?(关键问题))

[4. 红黑树的效率分析](#4. 红黑树的效率分析)

5.红黑树实际例子理解

[图 1:插入更多红色节点后的状态](#图 1:插入更多红色节点后的状态)

[图 2:插入新节点 6、22 后的状态](#图 2:插入新节点 6、22 后的状态)

[图 3:插入大量红色节点后的 "满二叉树" 状态](#图 3:插入大量红色节点后的 “满二叉树” 状态)

[图 4:全黑的 "理想初始状态"(无红节点)](#图 4:全黑的 “理想初始状态”(无红节点))

[图 5:插入红色节点后的状态(带 NIL 节点)](#图 5:插入红色节点后的状态(带 NIL 节点))

三、红黑树的核心实现

1.红黑树主体的实现

[1.1 模块 :颜色枚举定义](#1.1 模块 :颜色枚举定义)

[1.2 模块 :红黑树节点结构体定义](#1.2 模块 :红黑树节点结构体定义)

[1.3 模块 :红黑树类的框架定义](#1.3 模块 :红黑树类的框架定义)

2.红黑树插入功能的实现

[2.1 插入操作的整体流程](#2.1 插入操作的整体流程)

[2.2 插入的三种核心情况(带图示讲解)](#2.2 插入的三种核心情况(带图示讲解))

[情况 1:叔叔节点存在且为红色(变色即可解决)](#情况 1:叔叔节点存在且为红色(变色即可解决))

eg1.特殊情况:

eg2.一般情况:

[图 1:入门级场景(hb=0,最简单的首次插入)](#图 1:入门级场景(hb=0,最简单的首次插入))

[图 2:基础场景(单次插入 + 递归起点)](#图 2:基础场景(单次插入 + 递归起点))

[图 3:进阶场景(hb=1,黑高为 1 的子树扩展)](#图 3:进阶场景(hb=1,黑高为 1 的子树扩展))

[图 4:终极场景(hb=2,多层递归的复杂场景)](#图 4:终极场景(hb=2,多层递归的复杂场景))

[情况 2:叔叔节点不存在 / 为黑,且新节点是父节点的左孩子、父节点是祖父节点的左孩子(同侧单旋 + 变色)](#情况 2:叔叔节点不存在 / 为黑,且新节点是父节点的左孩子、父节点是祖父节点的左孩子(同侧单旋 + 变色))

[图1:最简场景(首次插入触发情况 2)](#图1:最简场景(首次插入触发情况 2))

[图 2:通用场景(递归过程中触发情况 2)](#图 2:通用场景(递归过程中触发情况 2))

[图 3:带黑高说明的详细场景](#图 3:带黑高说明的详细场景)

[情况 3:叔叔节点不存在 / 为黑,且新节点是父节点的左孩子、父节点是祖父节点的右孩子(双旋 + 变色)](#情况 3:叔叔节点不存在 / 为黑,且新节点是父节点的左孩子、父节点是祖父节点的右孩子(双旋 + 变色))

[图1:最简场景(首次插入触发情况 3)](#图1:最简场景(首次插入触发情况 3))

图2:通用场景(带子树的递归情况)

图3:带黑高说明的详细场景

[2.3 插入操作的完整代码实现](#2.3 插入操作的完整代码实现)

[模块 1:插入函数入口 & 空树处理](#模块 1:插入函数入口 & 空树处理)

[模块 2:BST 查找插入位置](#模块 2:BST 查找插入位置)

[模块 3:创建新节点并挂到父节点上](#模块 3:创建新节点并挂到父节点上)

[模块 4:核心:插入后红黑树规则调整(三种情况)](#模块 4:核心:插入后红黑树规则调整(三种情况))

[模块 5:收尾:保证根节点为黑色](#模块 5:收尾:保证根节点为黑色)

完整插入代码

四、红黑树的其他基础操作

[1. 红黑树的查找操作](#1. 红黑树的查找操作)

[2. 红黑树的验证(如何判断一棵树是不是合法的红黑树?)](#2. 红黑树的验证(如何判断一棵树是不是合法的红黑树?))

[2.1 递归检查函数 Check](#2.1 递归检查函数 Check)

[2.2 对外接口 IsBalance](#2.2 对外接口 IsBalance)

[2.3 完整代码如下](#2.3 完整代码如下)

[3. 删除操作](#3. 删除操作)

五、总结与拓展



前言

提示:这里可以添加本文要记录的大概内容:

前面我们已经系统学习了普通二叉搜索树(BST)和 AVL 树两种自平衡结构。我们知道,BST 在极端情况下会退化成链表,导致查询效率骤降;而 AVL 树通过严格的高度差规则,实现了完美平衡,但也带来了频繁旋转的额外开销

而红黑树,正是为了在「平衡效率」和「维护成本」之间找到最佳平衡点而诞生的。它不追求像 AVL 树那样的绝对平衡,而是通过一套巧妙的颜色规则,保证树的最长路径不超过最短路径的两倍,从而实现了接近 O (logN) 的稳定效率,同时大幅降低了旋转调整的频率。正因如此,它被选为了 C++ STL 中std::map和std::set的底层实现

这篇博客,我们将从红黑树的核心规则入手,一步步拆解它的平衡原理、插入时的变色与旋转调整,并最终给出完整的代码实现,帮你彻底搞懂这一经典数据结构


提示:以下是本篇文章正文内容

一、为什么需要红黑树?

回顾普通二叉搜索树的痛点:极端情况下会退化成链表,时间复杂度从 O(logN) 变成 O(N)

引出平衡二叉树的概念:AVL 树 vs 红黑树

  • AVL 树:严格平衡(左右子树高度差≤1),插入删除需要频繁旋转,适合读多写少场景
  • 红黑树近似平衡 ,通过 "颜色规则" 实现弱平衡,旋转次数更少,性能更稳定,因此被广泛应用(如 C++ STLmap/set、Java 的 TreeMap、Linux 内核进程调度)

二、红黑树的核心概念与规则

1. 红黑树的定义

红黑树是一种自平衡的二叉搜索树,每个节点额外存储一个 "颜色标记(红 / 黑)",通过一套颜色规则,保证树的整体平衡,避免退化成链表

2. 红黑树的五大核心规则(重点)

  • 规则 1:每个节点,非黑即红
  • 规则 2:根节点必须是黑色
  • 规则 3:所有叶子节点(NIL 空节点)都是黑色
  • 规则 4:红色节点的两个子节点,必须都是黑色(不能出现两个连续的红色节点)
  • 规则 5:从任意节点出发,到其所有叶子节点的路径上,黑色节点的数量必须相同(黑高相同)

3. 红黑树如何保证最长路径不超过最短路径的 2 倍?(关键问题)

  • 最短路径:全是黑色节点**,准确点就是路径上红色节点数量最少的那条路径** (黑高为 h
  • 最长路径:红黑交替(黑高同样为 h,路径长度为 2h
  • 结论:最长路径长度 ≤ 2 × 最短路径长度,因此树的高度始终维持在 O(logN),保证了查找效率

4. 红黑树的效率分析

  • 查找:O(logN),和 AVL 树一样
  • 插入 / 删除:O(logN),但平均旋转次数比 AVL 树少,写操作性能更优
  • 适用场景:读、写操作都频繁的场景,是工程中更常用的平衡树实现

我们来尝试推导一下:

先设两个未知数:

  • N:红黑树中实际节点的总数(不算 NIL 空叶子节点)
  • h:红黑树的黑高(根节点到 NIL 叶子的路径上,黑色节点的数量,所有路径黑高都相等)

我们要证明的是:红黑树的高度始终是 O (logN),因此增删查改的时间复杂度都是 O (logN)

1. 公式 1:2^h - 1 ≤ N(下界,最少节点数)

  • 这是红黑树节点数的理论下限 : 当红黑树里没有任何红色节点时,它就变成了一棵完全由黑色节点组成的满二叉树
  • 一棵黑高为h的满二叉树,最少需要 2^h - 1 个节点(比如 h=3 时,节点数是 7)
  • 而你的红黑树实际节点数N,一定大于等于这个最小值,所以 2^h - 1 ≤ N

2. 公式 2:N < 2^(2h) - 1(上界,最多节点数)

  • 这是红黑树节点数的理论上限 : 红黑树的规则限制了红色节点的数量,它的最长路径也只能是 "黑红交替" 的形式
    • 黑高为h时,路径上最多有h个黑色节点,再插入h个红色节点(黑红交替),路径总长度就是2h
    • 这时候,整棵树的节点数最多接近一棵高度为2h的满二叉树,而满二叉树的节点数是2^(2h) - 1,所以 N < 2^(2h) - 1

3. 把两个不等式结合起来

我们把两个式子合在一起:

我们对两边同时取对数(以 2 为底):

  • 对左边:2^h ≤ N + 1h ≤ log₂(N+1)h ≈ logN
  • 对右边:N + 1 < 2^(2h)log₂(N+1) < 2hh > (log₂(N+1))/2

所以最终得到:

红黑树的增删查改操作,都需要从根节点走到叶子节点,时间复杂度和树的高度成正比

  • 我们已经证明,树的黑高h本身就是 O(logN)
  • 最坏情况下,我们需要走的是最长路径,也就是 2h(黑红交替的路径)
  • 2h 依然是 O(logN) 级别(常数系数不影响时间复杂度的阶)

所以,红黑树的增删查改操作,最坏时间复杂度依然是 O (logN)


5.红黑树实际例子理解

图 1:插入更多红色节点后的状态

这棵树里,红色节点变多了,我们来验证规则和路径:

  • 规则检查:
    • 无连续红节点:30 (红) 的父 18 (黑)、子 25 (黑)/40 (黑);15 (红) 的父 10 (黑);35/50 (红) 的父 40 (黑),全部满足规则 4。
    • 黑高检查:所有路径的黑高都是 3(根 18、10/30/40 分支的黑节点数)。
  • 路径长度对比
    • 最短路径:18→10→NIL(路径长度 2,黑高 3)
    • 最长路径:18→30→40→35→NIL(路径长度 4,黑高 3)
    • 此时最长路径 = 2 × 最短路径,刚好触达了红黑树的路径上限。

图 2:插入新节点 6、22 后的状态

这是插入新节点后的中间状态,我们可以看到调整的痕迹:

  • 节点 6 (红)、22 (红) 的父节点都是黑色,依然满足规则 4。
  • 黑高仍然保持一致:所有路径的黑高都是 3。
  • 路径变化
    • 新增路径 18→10→6→NIL(路径长度 3,黑高 3)
    • 新增路径 18→30→25→22→NIL(路径长度 4,黑高 3)
    • 最长路径依然是最短路径的 2 倍,没有超过上限。

图 3:插入大量红色节点后的 "满二叉树" 状态

这是红黑树路径的极端情况,也是理解 "最长路径≤2 倍最短路径" 的关键例子:

  • 规则检查:
    • 红色节点 10、30 的子节点都是黑色;所有叶子节点 1、8、12、16、20、22、35、50 都是红色,父节点都是黑色,无连续红节点。
    • 黑高检查:所有路径的黑高都是 3(18→6/15/25/40 分支,黑节点数一致)。
  • 路径长度对比
    • 最短路径:18→10→6→NIL(路径长度 3,黑高 3)
    • 最长路径:18→10→6→1→NIL(路径长度 4,黑高 3)
    • 所有路径中,最长路径始终没有超过最短路径的 2 倍,完美体现了红黑树的平衡特性。

图 4:全黑的 "理想初始状态"(无红节点)

这是一棵完全由黑色节点组成的二叉树,是理解红黑树路径的基础

  • 它天然满足所有规则:根黑、无红节点、黑高处处相等
  • 路径特点:所有路径的长度完全相同,最短路径 = 最长路径 = 3(节点数)
  • 这是 "最短路径" 的极端情况:路径上全是黑色节点,黑高为 3

图 5:插入红色节点后的状态(带 NIL 节点)

规则 3 提到的 NIL 黑色叶子节点,是标准的红黑树表示法。

  • 插入了红色节点 15 和 40,满足规则 4:红色节点的父 / 子节点都是黑色。
  • 我们来算一下黑高:
    • 根节点 18 → 左路径:18 (黑)→10 (黑)→15 (红)→NIL (黑),黑高为 3(18、10、NIL)
    • 根节点 18 → 右路径:18 (黑)→30 (黑)→40 (红)→NIL (黑),黑高同样为 3
  • 路径长度对比
    • 最短路径:18→10→NIL(黑高 3,路径长度 2)
    • 最长路径:18→10→15→NIL(黑高 3,路径长度 3)
    • 此时最长路径 = 1.5 × 最短路径,已经体现了 "红节点延长路径但不增加黑高" 的特点,以及,最长路径长度 ≤ 2 × 最短路径长度

-------------------------------------------------红黑树代码实现讲解-----------------------------------------------------

三、红黑树的核心实现

1.红黑树主体的实现

1.1 模块 :颜色枚举定义

  • enum 定义了两种颜色,方便给节点标记颜色
  • 为什么用枚举?一是代码可读性更好,二是避免直接用数字(0/1)导致写错

1.2 模块 :红黑树节点结构体定义

  • 为什么要有 _parent 指针?
    • 红黑树插入后,需要向上回溯父节点、叔叔节点、祖父节点来调整颜色和结构,没有父指针就做不到
  • 为什么新节点默认是红色?
    • 如果设为黑色,会直接破坏红黑树的「黑高一致」规则,需要从根节点开始全局调整,成本极高;设为红色只会破坏「不能连续红节点」规则,调整成本低很多

1.3 模块 :红黑树类的框架定义

  • typedef 是为了简化代码,不然每次都要写 RBTreeNode<K, V>,太麻烦了
  • _root 是整个树的入口,初始为 nullptr 表示空树

总代码如下:

cpp 复制代码
#include <iostream>
#include <utility> // 包含pair头文件,用于键值对

// 模块1:颜色枚举定义
enum Colour
{
    RED,    // 红色节点
    BLACK   // 黑色节点
};

// 模块2:红黑树节点结构体定义
template<class K, class V>
struct RBTreeNode
{
    // 存储键值对数据
    std::pair<K, V> _kv;

    // 左右孩子指针 + 父节点指针
    RBTreeNode<K, V>* _left;
    RBTreeNode<K, V>* _right;
    RBTreeNode<K, V>* _parent;

    // 节点颜色
    Colour _col;

    // 节点构造函数
    RBTreeNode(const std::pair<K, V>& kv)
        : _kv(kv)
        , _left(nullptr)
        , _right(nullptr)
        , _parent(nullptr)
        , _col(RED) // 新节点默认红色
    {}
};

// 模块3:红黑树类框架
template<class K, class V>
class RBTree
{
private:
    // 简化节点类型名
    typedef RBTreeNode<K, V> Node;

public:
    // 后面会在这里实现:插入、查找、验证、旋转等函数

private:
    // 红黑树根节点
    Node* _root = nullptr;
};

// 测试用例(空树测试)
int main()
{
    // 创建一个空的红黑树
    RBTree<int, int> tree;

    std::cout << "红黑树初始化完成,根节点地址:" << &tree << std::endl;
    std::cout << "当前根节点是否为空:" << (tree._root == nullptr ? "是" : "否") << std::endl;

    return 0;
}

2.红黑树插入功能的实现

2.1 插入操作的整体流程

  1. 按照普通二叉搜索树的规则,找到合适的位置插入新节点(默认红色)
  2. 插入后,判断是否破坏了红黑树的规则(主要是规则 4:不能连续红节点)
  3. 根据父节点、叔叔节点的颜色,分三种情况进行调整(变色 / 旋转 + 变色)

2.2 插入的三种核心情况(带图示讲解)

情况 1:叔叔节点存在且为红色(变色即可解决)
  • 场景:父节点红,叔叔节点红,祖父节点黑
  • 调整方法:父节点变黑,叔叔节点变黑,祖父节点变红;然后把祖父节点当成新节点,向上递归调整
  • 特点:不需要旋转,仅通过变色解决问题
eg1.特殊情况:

在这张图里,我们先把关键节点的身份标清楚:

  • x新插入的节点(默认红色,红黑树的规则)
  • px父节点(Parent)
  • up的兄弟节点,也就是x叔叔节点(Uncle)
  • gp的父节点,也就是x祖父节点(Grandparent)

因为新节点x是红色,父节点p也是红色,直接违反了红黑树的规则 4:不能有连续的红色节点。 我们的目标是:在不破坏其他规则的前提下,修正这个冲突

情况 1 的核心解法,就是变色三步骤,图里的箭头已经标出来了:

  1. 父节点p变黑 :解决了xp连续红节点的问题
  2. 叔叔节点u变黑 :因为父和叔原本都是红色,一起变黑,能保证从祖父节点g到所有叶子的黑高不变
  3. 祖父节点g变红 :把pu变黑带来的 "额外黑节点" 抵消掉,维持黑高平衡

为什么要把祖父节点当成新节点,向上递归?

调整完之后,g变成了红色,而它的父节点(比如图里的节点 18)可能也是红色,就会出现新的连续红节点问题。 所以我们需要:

  • g当成新的 "冲突节点",继续向上检查父、叔、祖父节点
  • 重复变色 / 旋转逻辑,直到没有冲突,或者走到根节点为止
eg2.一般情况:
图 1:入门级场景(hb=0,最简单的首次插入)

这张图是情况 1 的「Hello World」版本,帮你建立最基础的认知。

场景描述

  • hb=0:所有子节点都是空 NIL 节点,黑高为 0
  • 初始树:祖父g=10(黑),父p=6(红),叔u=15(红)
  • 操作:在父节点p下插入新节点x(默认红色),触发连续红节点冲突

调整过程

  1. 插入红色节点x,此时px都是红色,违反规则 4
  2. p变黑、叔u变黑,祖父g变红
  3. 此时g变成红色,需要作为新的冲突节点,继续向上检查

关键结论

  • 无论新节点x是父节点的左孩子还是右孩子,只要满足「父红、叔红、祖父黑」,处理逻辑完全一致
  • 变色的核心是:父和叔同时变黑,抵消了祖父变红带来的黑高变化,所有路径的黑高依然相等

图 2:基础场景(单次插入 + 递归起点)

这张图分上下两部分,帮你理解「首次插入」和「递归过程中」的两种情况 1 触发场景。

上半部分:首次插入触发情况 1

  • 和图 2 逻辑完全一致,就是更通用的版本。
  • 新节点x插入后,父p和叔u都是红色,触发变色调整。
  • 调整后祖父g变红,作为新节点向上递归

下半部分:递归过程中触发情况 1

  • 注意文字说明:c不是新增节点,c是之前的g
  • 这里的x不是刚插入的节点,而是上一轮调整后变红的祖父节点,现在向上检查时,再次遇到了「父红、叔红、祖父黑」的情况
  • 处理逻辑和上半部分完全一样,重复「父叔变黑、祖父变红」的步骤,继续向上递归

关键结论

  • 情况 1 不是一次性操作,它可以在递归过程中连续触发,直到遇到非红的叔叔节点,或者走到根节点

图 3:进阶场景(hb=1,黑高为 1 的子树扩展)

这张图帮你理解:叔叔节点下面的子树结构,完全不影响情况 1 的处理逻辑

场景描述

  • hb=1:叔叔节点u下面的子树d/e/f,是黑高为 1 的合法红黑树(比如左上角的 x/y/z/m 四种结构)。
  • 初始状态:祖父g=10(黑),父p=6(红),叔u=15(红),父节点下的x=3(上一轮调整后变红的节点)。

调整过程

  1. 在父节点p的子树中插入新节点,触发连续红节点冲突。
  2. p变黑、叔u变黑,祖父g变红,g成为新的冲突节点向上递归。

关键点

  • 叔叔节点的子树可以是任意黑高为 1 的红黑树,组合数高达 256 种,但处理逻辑完全不变。
  • 变色操作只会影响父、叔、祖父三个节点的颜色,子树的结构和黑高不会被破坏。
图 4:终极场景(hb=2,多层递归的复杂场景)

这张图是情况 1 的「终极形态」,展示了树更深、黑高更高时的处理逻辑

场景描述

  • hb=2:叔叔节点u下面的子树d/e/f,是黑高为 2 的红黑树;父节点p下面的a/b,是黑高为 1 的红黑树
  • 这种场景通常是多次递归调整后出现的,组合数高达百亿级,说明情况 1 在复杂树中也会频繁触发

调整过程

  1. 在父节点p的子树中插入新节点,触发冲突
  2. 第一次变色调整:父p变黑、叔u变黑、祖父g变红,g成为新节点向上递归
  3. 递归过程中,可能再次遇到父红、叔红的情况,再次触发情况 1,继续变色调整

关键结论

  • 无论树的层级多深、结构多复杂,只要满足「父红、叔红、祖父黑」,就可以用情况 1 的变色方法解决问题
  • 递归向上调整,是情况 1 的灵魂,它能把冲突从叶子节点一直传递到根节点,最终被完全解决

情况 2:叔叔节点不存在 / 为黑,且新节点是父节点的左孩子、父节点是祖父节点的左孩子(同侧单旋 + 变色)
  • 场景:父节点红,叔叔节点黑,新节点和父节点在同一侧(父左,新左)
  • 调整方法:对祖父节点进行右单旋 ;父节点变黑,祖父节点变红
图1:最简场景(首次插入触发情况 2)

1. 初始状态

  • 祖父节点g=10(黑)
  • 父节点p=6(红,是g的左孩子)
  • 叔叔节点u不存在(空 NIL,相当于黑色)
  • 新节点c=3(红,是p的左孩子)

此时问题:pc都是红色,违反了红黑树「不能连续红节点」的规则。

2. 调整步骤(右单旋 + 变色)

  1. 对祖父节点g做右单旋
    • p=6变成这棵子树的新根
    • g=10变成p的右孩子
    • p原来的右孩子(如果有的话),过继给g当左孩子(这张图里没有,所以省略)
  2. 变色
    • 新根p=6变黑
    • 原来的祖父节点g=10变红

3. 调整后结果

  • 新节点c=3(红)的父节点p=6(黑),不再是连续红节点,规则 4 被修复
  • 从这棵子树往上看,黑高和调整前完全一致,规则 5 没有被破坏

图 2:通用场景(递归过程中触发情况 2)

这张图是情况 2 的通用版本,展示了叔叔节点u存在(但为黑)、且子树有结构的情况

1. 初始状态

  • 祖父节点g=10(黑)
  • 父节点p=6(红,左孩子)
  • 叔叔节点u=15(黑,右孩子)
  • p下面的节点x=3(红,是上一轮调整后变红的节点,现在作为冲突节点c
  • p的子树a/bu的子树d/e/f,都是合法的红黑树

2. 调整过程

  1. 插入新节点,导致x=3变红,和父节点p=6形成连续红节点冲突
  2. 对祖父节点g=10做右单旋:
    • p=6成为新根,g=10变成p的右孩子
    • p原来的右孩子(如果有的话),过继给g当左孩子(这张图里是d
  3. 变色:
    • 新根p=6变黑
    • 原来的祖父节点g=10变红

3. 关键结论

  • 叔叔节点只要是黑色,不管它下面的子树是什么结构,处理逻辑都是一样的:单旋 + 变色
  • 调整后,所有路径的黑高依然保持一致,没有破坏红黑树的规则

图 3:带黑高说明的详细场景

这张图是情况 2 的进阶说明,帮你理解「为什么单旋 + 变色不会破坏黑高」

1. 初始状态(黑高 hb=1)

  • 祖父节点g=10(黑),父p=6(红),叔u=15(黑)
  • p的子树a/bu的子树d/e/f,都是黑高为 1 的红黑树
  • x=3是上一轮调整后变红的节点,作为冲突节点c

2. 调整过程

  1. g=10做右单旋,p=6成为新根
  2. p=6变黑,g=10变红
  3. 此时:
    • p的分支:黑高 + 1(p从红变黑)
    • g的分支:黑高不变(g从黑变红,但它的父节点现在是变黑的p,整体黑高和原来一致)

3. 为什么不需要向上递归?

调整后,新根p变成了黑色,它和它的父节点(上一层的节点)不会形成连续红节点冲突,所以情况 2 调整完成后,不需要再向上递归,直接结束即可。


情况 3:叔叔节点不存在 / 为黑,且新节点是父节点的左孩子、父节点是祖父节点的右孩子(双旋 + 变色)
  • 场景:父节点红,叔叔节点黑,新节点和父节点在不同侧(父左,新右)
  • 调整方法:先对父节点进行左单旋 ,转化为情况 2;再对祖父节点进行右单旋,然后变色
  • 核心:双旋的本质是把 "拐弯" 的结构拉成 "直线",再用单旋解决

同理,父节点是祖父节点右孩子的镜像场景,逻辑完全对称,一并讲解即可

图1:最简场景(首次插入触发情况 3)

1. 初始状态

  • 祖父节点 g=10(黑)
  • 父节点 p=6(红,是 g 的左孩子)
  • 叔叔节点 u 不存在(空 NIL,相当于黑色)
  • 新节点 c=8(红,是 p 的右孩子)

此时的问题:

  • p=6c=8 都是红色,违反了「不能连续红节点」的规则
  • 而且 cp 在不同侧,没法直接对 g 做单旋,必须先把结构 "掰直"

2. 调整步骤(双旋 + 变色)

  1. 第一步:对父节点 p 做左单旋

    • c=8 旋转上去,变成 p 原来的位置
    • p=6 变成 c 的左孩子
    • 此时结构变成:g=10 的左孩子是 c=8c 的左孩子是 p=6
    • 这一步的目的,是把 "拐弯" 的结构,变成和情况 2 一样的 "直线" 结构
  2. 第二步:对祖父节点 g 做右单旋

    • c=8 旋转上去,变成这棵子树的新根
    • g=10 变成 c 的右孩子
  3. 第三步:变色

    • 新根 c=8 变黑
    • 原来的祖父节点 g=10 变红

3. 调整后结果

  • 没有连续的红色节点,规则 4 被修复
  • 所有路径的黑高和调整前一致,规则 5 没有被破坏
  • 新根 c=8 是黑色,不会和上层节点形成新的冲突,调整直接结束

图2:通用场景(带子树的递归情况)

这张图是情况 3 的通用版本,展示了叔叔节点存在(但为黑)、且子树有结构的情况。

1. 初始状态

  • 祖父节点 g=10(黑),父节点 p=6(红,左孩子),叔叔节点 u=15(黑,右孩子)
  • p 的右孩子 x=8 是冲突节点(上一轮调整后变红的节点),x 的子树是 a/b
  • p 的左孩子是 du 的子树是 e/f

2. 调整过程

  1. 插入新节点,导致 x=8 变红,和父节点 p=6 形成连续红节点冲突,且 xp 的右孩子(异侧)
  2. 第一步:对父节点 p 做左单旋
    • x=8 变成 p 的位置,p=6 变成 x 的左孩子
    • x 原来的子树 a/b,变成 p 的右孩子
  3. 第二步:对祖父节点 g=10 做右单旋
    • x=8 变成新根,g=10 变成 x 的右孩子
    • g 原来的左孩子(空),变成 x 的右孩子
  4. 第三步:变色
    • 新根 x=8 变黑
    • 原来的祖父节点 g=10 变红

3. 关键结论

  • 叔叔节点只要是黑色,不管它下面的子树是什么结构,处理逻辑都是一样的:先对父节点单旋,再对祖父节点反方向单旋,最后变色
  • 调整后,所有路径的黑高依然保持一致,且新根为黑色,不需要向上递归

图3:带黑高说明的详细场景

这张图是情况 3 的进阶说明,帮你理解双旋过程中子树的变化和黑高的一致性。

1. 初始状态(黑高 hb=1

  • 祖父 g=10(黑),父 p=6(红),叔 u=15(黑)
  • p 的左孩子 d(黑),右孩子 x=8(黑),x 的子树 a/b(黑高为 1)
  • 插入新节点后,x=8 变红,触发异侧冲突

2. 调整过程

  1. 对父节点 p 做左单旋:x=8 上移,p=6 成为其左孩子,a/b 成为 p 的右孩子
  2. 对祖父节点 g=10 做右单旋:x=8 成为新根,g=10 成为其右孩子
  3. 变色:x=8 变黑,g=10 变红

3. 为什么不需要向上递归?

调整后,新根 x=8 是黑色,它和上层父节点不会形成连续红节点冲突,所以情况 3 调整完成后,不需要再向上递归,直接结束即可。


-------------------------------------------------红黑树插入功能实现-----------------------------------------------------

2.3 插入操作的完整代码实现

模块 1:插入函数入口 & 空树处理

空树时,直接创建根节点,并且必须设为黑色(红黑树规则 2)

模块 2:BST 查找插入位置

和普通二叉搜索树的插入逻辑完全一致,找到父节点位置,同时判断是否重复

模块 3:创建新节点并挂到父节点上
  • 新节点默认红色:如果设为黑色,会直接破坏黑高规则,需要全局调整,成本极高。
  • 挂到父节点后,设置好_parent指针,为后续向上回溯调整做准备。
模块 4:核心:插入后红黑树规则调整(三种情况)
cpp 复制代码
 // 4. 插入后调整红黑树规则,循环向上处理,直到父节点为黑色(无冲突)
    while (parent && parent->_col == RED)
    {
        // 父节点是红色,说明一定存在祖父节点(根节点是黑色)
        Node* grandfather = parent->_parent;

        // 情况分支1:父节点是祖父节点的左孩子
        if (parent == grandfather->_left)
        {
            // 找到叔叔节点(父节点的兄弟节点,即祖父的右孩子)
            Node* uncle = grandfather->_right;

            // -------------------- 情况1:叔叔节点存在且为红色(仅变色) --------------------
            if (uncle && uncle->_col == RED)
            {
                // 父节点变黑,叔叔节点变黑,祖父节点变红
                parent->_col = uncle->_col = BLACK;
                grandfather->_col = RED;

                // 把祖父节点当成新的冲突节点,向上继续调整
                cur = grandfather;
                parent = cur->_parent;
            }
            else
            {
                // -------------------- 情况2:叔叔节点为黑/不存在,且cur和parent同侧(单旋+变色) --------------------
                if (cur == parent->_left)
                {
                    // 对祖父节点做右单旋
                    RotateRight(grandfather);
                    // 父节点变黑,祖父节点变红
                    parent->_col = BLACK;
                    grandfather->_col = RED;
                }
                // -------------------- 情况3:叔叔节点为黑/不存在,且cur和parent异侧(双旋+变色) --------------------
                else
                {
                    // 第一步:对父节点做左单旋,把结构掰直
                    RotateLeft(parent);
                    // 第二步:对祖父节点做右单旋
                    RotateRight(grandfather);
                    // 新节点cur变黑,祖父节点变红
                    cur->_col = BLACK;
                    grandfather->_col = RED;
                }
                // 情况2/3调整完成后,不会再向上产生冲突,直接break结束循环
                break;
            }
        }
        // 情况分支2:父节点是祖父节点的右孩子(上面的镜像场景)
        else
        {
            // 找到叔叔节点(父节点的兄弟节点,即祖父的左孩子)
            Node* uncle = grandfather->_left;

            // -------------------- 情况1:叔叔节点存在且为红色(仅变色) --------------------
            if (uncle && uncle->_col == RED)
            {
                parent->_col = uncle->_col = BLACK;
                grandfather->_col = RED;

                // 向上继续调整
                cur = grandfather;
                parent = cur->_parent;
            }
            else
            {
                // -------------------- 情况2:叔叔节点为黑/不存在,且cur和parent同侧(单旋+变色) --------------------
                if (cur == parent->_right)
                {
                    // 对祖父节点做左单旋
                    RotateLeft(grandfather);
                    // 父节点变黑,祖父节点变红
                    parent->_col = BLACK;
                    grandfather->_col = RED;
                }
                // -------------------- 情况3:叔叔节点为黑/不存在,且cur和parent异侧(双旋+变色) --------------------
                else
                {
                    // 第一步:对父节点做右单旋,把结构掰直
                    RotateRight(parent);
                    // 第二步:对祖父节点做左单旋
                    RotateLeft(grandfather);
                    // 新节点cur变黑,祖父节点变红
                    cur->_col = BLACK;
                    grandfather->_col = RED;
                }
                // 情况2/3调整完成,直接break
                break;
            }
        }
    }
  • 循环条件:只要父节点是红色,就存在连续红节点冲突,必须调整
  • 核心逻辑:分「父为左孩子 / 父为右孩子」两大分支,每个分支下处理三种情况
  • 情况 1:叔叔为红,仅变色,向上递归
  • 情况 2/3:叔叔为黑,单旋 / 双旋 + 变色,调整完成直接结束
模块 5:收尾:保证根节点为黑色

调整过程中根节点可能被设为红色,最后强制设为黑色,确保符合红黑树规则 2

完整插入代码
cpp 复制代码
#include <iostream>
#include <utility>
using namespace std;

// 颜色枚举
enum Colour
{
    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;
    Colour _col;

    RBTreeNode(const pair<K, V>& kv)
        : _kv(kv)
        , _left(nullptr)
        , _right(nullptr)
        , _parent(nullptr)
        , _col(RED)
    {}
};

// 红黑树类
template<class K, class V>
class RBTree
{
private:
    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;
        }

        // 1. 按BST找到插入位置
        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; // 键重复
            }
        }

        // 2. 创建新节点并挂到父节点
        cur = new Node(kv);
        if (parent->_kv.first < kv.first)
        {
            parent->_right = cur;
        }
        else
        {
            parent->_left = cur;
        }
        cur->_parent = parent;

        // 3. 调整红黑树规则
        while (parent && parent->_col == RED)
        {
            Node* grandfather = parent->_parent;
            if (parent == grandfather->_left)
            {
                // 父为左孩子,叔叔为祖父的右孩子
                Node* uncle = grandfather->_right;

                // 情况1:叔叔存在且为红
                if (uncle && uncle->_col == RED)
                {
                    parent->_col = uncle->_col = BLACK;
                    grandfather->_col = RED;
                    cur = grandfather;
                    parent = cur->_parent;
                }
                else
                {
                    // 情况2:同侧(cur是parent的左孩子),单旋
                    if (cur == parent->_left)
                    {
                        RotateRight(grandfather);
                        parent->_col = BLACK;
                        grandfather->_col = RED;
                    }
                    // 情况3:异侧(cur是parent的右孩子),双旋
                    else
                    {
                        RotateLeft(parent);
                        RotateRight(grandfather);
                        cur->_col = BLACK;
                        grandfather->_col = RED;
                    }
                    break;
                }
            }
            else
            {
                // 父为右孩子,叔叔为祖父的左孩子
                Node* uncle = grandfather->_left;

                // 情况1:叔叔存在且为红
                if (uncle && uncle->_col == RED)
                {
                    parent->_col = uncle->_col = BLACK;
                    grandfather->_col = RED;
                    cur = grandfather;
                    parent = cur->_parent;
                }
                else
                {
                    // 情况2:同侧(cur是parent的右孩子),单旋
                    if (cur == parent->_right)
                    {
                        RotateLeft(grandfather);
                        parent->_col = BLACK;
                        grandfather->_col = RED;
                    }
                    // 情况3:异侧(cur是parent的左孩子),双旋
                    else
                    {
                        RotateRight(parent);
                        RotateLeft(grandfather);
                        cur->_col = BLACK;
                        grandfather->_col = RED;
                    }
                    break;
                }
            }
        }

        // 强制根节点为黑色
        _root->_col = BLACK;
        return true;
    }

private:
    // 右单旋(和AVL树的旋转逻辑一致)
    void RotateRight(Node* parent)
    {
        Node* leftChild = parent->_left;
        Node* rightChildOfLeft = leftChild->_right;

        parent->_left = rightChildOfLeft;
        if (rightChildOfLeft)
        {
            rightChildOfLeft->_parent = parent;
        }

        leftChild->_right = parent;
        leftChild->_parent = parent->_parent;

        if (parent == _root)
        {
            _root = leftChild;
        }
        else
        {
            if (parent == parent->_parent->_left)
            {
                parent->_parent->_left = leftChild;
            }
            else
            {
                parent->_parent->_right = leftChild;
            }
        }

        parent->_parent = leftChild;
    }

    // 左单旋(和AVL树的旋转逻辑一致)
    void RotateLeft(Node* parent)
    {
        Node* rightChild = parent->_right;
        Node* leftChildOfRight = rightChild->_left;

        parent->_right = leftChildOfRight;
        if (leftChildOfRight)
        {
            leftChildOfRight->_parent = parent;
        }

        rightChild->_left = parent;
        rightChild->_parent = parent->_parent;

        if (parent == _root)
        {
            _root = rightChild;
        }
        else
        {
            if (parent == parent->_parent->_left)
            {
                parent->_parent->_left = rightChild;
            }
            else
            {
                parent->_parent->_right = rightChild;
            }
        }

        parent->_parent = rightChild;
    }

private:
    Node* _root = nullptr;
};

// 测试代码
int main()
{
    RBTree<int, int> tree;
    tree.Insert({10, 10});
    tree.Insert({6, 6});
    tree.Insert({15, 15});
    tree.Insert({3, 3});
    tree.Insert({8, 8});
    tree.Insert({12, 12});
    tree.Insert({18, 18});
    cout << "红黑树插入完成" << endl;
    return 0;
}

四、红黑树的其他基础操作

1. 红黑树的查找操作

  • 和普通二叉搜索树完全一致,递归 / 迭代实现都可以,简单带过,不过多赘述

2. 红黑树的验证(如何判断一棵树是不是合法的红黑树?)

  1. 验证是否满足二叉搜索树的性质(中序遍历是否有序)
  2. 验证根节点是否为黑色
  3. 验证是否存在连续的红色节点
  4. 验证所有路径的黑高是否一致
  • 可以写一个验证函数,配合插入操作的测试用例,验证你的实现是否正确

这张图很清晰的可以看出我们这里采用前序遍历

2.1 递归检查函数 Check
cpp 复制代码
// 递归检查红黑树是否合法的核心函数
// root:当前检查的节点
// blackNum:当前路径上已统计的黑色节点数量
// refNum:参考黑高(以最左路径的黑高为标准)
template<class K, class V>
bool RBTree<K, V>::Check(Node* root, int blackNum, const int refNum)
{
    // 1. 递归终止条件:走到空节点(一条路径结束)
    if (root == nullptr)
    {
        // 路径结束时,检查当前路径的黑高是否等于参考黑高
        if (refNum != blackNum)
        {
            cout << "存在黑色结点的数量不相等的路径" << endl;
            return false;
        }
        // 黑高一致,这条路径合法
        return true;
    }

    // 2. 检查规则4:不能存在连续的红色节点
    // 如果当前节点是红色,且父节点也是红色 → 违规
    if (root->_col == RED && root->_parent->_col == RED)
    {
        cout << root->_kv.first << " 存在连续的红色结点" << endl;
        return false;
    }

    // 3. 统计当前路径的黑色节点数量
    if (root->_col == BLACK)
    {
        blackNum++;
    }

    // 4. 递归检查左子树和右子树,只有左右都合法,才返回true
    return Check(root->_left, blackNum, refNum) 
        && Check(root->_right, blackNum, refNum);
}

Check 递归函数:核心验证逻辑

① 路径黑高检查

当递归走到空节点时,代表一条路径遍历完成。此时会对比这条路径的黑高 blackNum 和参考黑高 refNum,如果不一致,说明违反了红黑树规则 5

② 连续红节点检查

红黑树规则 4 禁止两个连续的红色节点。这里通过检查当前节点和它父节点的颜色,直接发现违规

③ 统计黑高并递归

如果当前节点是黑色,就把 blackNum 加 1,然后递归检查左右子树。只有左右子树都合法,才会返回 true


2.2 对外接口 IsBalance
cpp 复制代码
// 对外接口:判断整个红黑树是否合法
template<class K, class V>
bool RBTree<K, V>::IsBalance()
{
    // 情况1:空树也是合法的红黑树
    if (_root == nullptr)
    {
        return true;
    }

    // 情况2:检查规则2:根节点必须是黑色
    if (_root->_col == RED)
    {
        cout << "根节点不是黑色,违规" << endl;
        return false;
    }

    // 情况3:计算参考黑高refNum(以最左路径的黑高为标准)
    int refNum = 0;
    Node* cur = _root;
    while (cur)
    {
        if (cur->_col == BLACK)
        {
            refNum++;
        }
        cur = cur->_left;
    }

    // 调用递归检查函数,验证整棵树
    return Check(_root, 0, refNum);
}

IsBalance 函数:验证的入口

它负责做三件事:

  1. 空树检查 :空树天然合法,直接返回 true
  2. 根节点检查:红黑树规则 2 强制根节点为黑色,否则直接违规。
  3. 计算参考黑高:从根节点一直往左走,统计最左路径上的黑色节点数量,作为后续所有路径的标准。

2.3 完整代码如下
cpp 复制代码
template<class K, class V>
class RBTree
{
private:
    typedef RBTreeNode<K, V> Node;

    // 递归检查函数
    bool Check(Node* root, int blackNum, const int refNum)
    {
        if (root == nullptr)
        {
            if (refNum != blackNum)
            {
                cout << "存在黑色结点的数量不相等的路径" << endl;
                return false;
            }
            return true;
        }

        if (root->_col == RED && root->_parent->_col == RED)
        {
            cout << root->_kv.first << " 存在连续的红色结点" << endl;
            return false;
        }

        if (root->_col == BLACK)
        {
            blackNum++;
        }

        return Check(root->_left, blackNum, refNum) 
            && Check(root->_right, blackNum, refNum);
    }

public:
    // 对外接口
    bool IsBalance()
    {
        if (_root == nullptr)
        {
            return true;
        }

        if (_root->_col == RED)
        {
            cout << "根节点不是黑色,违规" << endl;
            return false;
        }

        int refNum = 0;
        Node* cur = _root;
        while (cur)
        {
            if (cur->_col == BLACK)
            {
                refNum++;
            }
            cur = cur->_left;
        }

        return Check(_root, 0, refNum);
    }

    // 其他成员函数(Insert、Find等)
    // ...

private:
    Node* _root = nullptr;
};

3. 删除操作

  • 红黑树的删除逻辑比插入复杂很多,这里就不展开来讲解了,有兴趣的可以自行了解:
    1. 先回顾 BST 的删除逻辑(叶子节点、单孩子节点、双孩子节点)
    2. 删除后,处理 "双黑节点" 的调整问题
    3. 分四种情况讲解双黑节点的修复(兄弟节点为黑 / 红,侄子节点颜色不同)

五、总结与拓展

  1. 红黑树的核心思想:用颜色规则约束,以少量的调整(变色 / 旋转)换取近似平衡
  2. 红黑树 vs AVL 树的对比表格
  3. 红黑树的实际应用场景(STL、Linux 内核等)
  4. 学习建议:先理解 BST,再学 AVL 树的旋转,最后再啃红黑树的调整逻辑,配合画图理解三种插入情况
相关推荐
星恒随风1 小时前
C语言数据结构排序算法详解(上):从插入排序、希尔排序到选择排序、堆排序
c语言·数据结构·笔记·学习·排序算法
不会C语言的男孩1 小时前
C++ Primer Plus 第7章:函数——C++的编程模块
开发语言·c++
方也_arkling1 小时前
【Java-Day09】继承
java·开发语言
迈巴赫车主1 小时前
蓝桥杯21247弹跳鞋java
java·开发语言·数据结构·算法·职场和发展·蓝桥杯
kebeiovo1 小时前
C++与 Lua的交互
c++·lua
SoftLipaRZC2 小时前
C语言数据在内存中的存储:整型与浮点型的秘密
c语言·开发语言
悟乙己2 小时前
python DoWhy 库使用案例: SaaS 公司的客服案例
开发语言·python
就叫_这个吧2 小时前
JavaScript基础数据类型、运算符、数组、函数的定义及DOM方式应用
开发语言·前端·javascript
basketball6162 小时前
Golang:基本输入输出使用方法总结
开发语言·golang·xcode