【C++】21. 红黑树的实现

上一章节我们实现了AVL树,这一章节我们就来实现一下红黑树,同样这里我们只介绍插入和查找的接口,插入是构建红黑树的关键,同时也是常考的点,至于为什么删除会显得"并不重要",原因如下:

I. 应用场景中插入频率远高于删除

  • 数据结构的典型使用模式

    在多数使用红黑树的场景(如数据库索引、内存管理、缓存系统)中,插入操作通常比删除更频繁。例如:

    • 数据库索引需要不断插入新记录,而删除可能通过标记(如逻辑删除)延迟处理。

    • 缓存系统(如Redis)中数据自动过期,实际显式删除较少。

  • 动态数据流

    实时数据流(如日志、传感器数据)通常以插入为主,删除操作可能集中在后期批量处理。


II. 删除操作可通过策略优化绕过复杂度

  • 惰性删除(Lazy Deletion)

    许多系统采用"标记删除"而非立即调整树结构。例如:

    • 将节点标记为"已删除",实际保留在树中,后续插入时复用空间。

    • 减少实时调整的开销,尤其在高并发场景(如Java的ConcurrentHashMap)。

  • 批量处理

    删除操作积累到一定阈值后批量执行,分摊调整成本(如B树的分裂/合并优化)。


III. 删除的实现复杂度更高,但触发条件更少

  • 插入修复的确定性

    插入破坏红黑树性质的情况相对固定,修复逻辑集中在父节点和叔节点的颜色组合(4种经典Case),且修复过程可能向上递归的范围有限。

  • 删除修复的多样性

    删除黑色节点后,需处理兄弟节点及其子树的复杂情况(6种以上Case),修复可能波及整条路径,甚至需要多次旋转和重新着色。例如:

    • 兄弟节点为红色时,需旋转父节点并重新着色。

    • 兄弟节点为黑色且侄子节点全黑时,需向上递归处理。

  • 实际触发概率

    由于红黑树的平衡性,删除导致结构破坏的概率较低。即使触发修复,多数情况在局部即可解决。


IV. 性能优化的侧重点不同

  • 插入优化更直接影响用户体验

    实时系统(如游戏、交易系统)对插入延迟敏感,需优先保证插入效率。

  • 删除的代价可被其他机制吸收

    例如,数据库通过预写日志(WAL)和后台线程处理删除,避免阻塞主线程。


V. 红黑树的设计哲学

  • 权衡平衡与操作成本

    红黑树允许一定程度的不平衡(最长路径≤2倍最短路径),以换取更少的旋转操作。这一设计使得:

    • 插入的调整成本较低(平均旋转次数更少)。

    • 删除的调整成本虽高,但总体仍能保持O(log n),且实际应用中删除频率低,综合效率更高。


总结:为什么删除显得"不重要"?

维度 插入 删除
频率 高(实时数据流、动态更新) 低(常被惰性删除或批量处理)
修复复杂度 简单(4种Case,局部修复) 复杂(6+种Case,可能递归调整)
优化策略 直接决定实时性能 常被延迟或分摊处理
设计目标 确保高频操作高效 容忍低频操作的较高成本

1. 红黑树的概念

红黑树是一种二叉搜索树,每个节点新增一个存储位来表示节点颜色,可以是红色或黑色。

通过对从根节点到叶节点的每条路径上的节点颜色进行约束,红黑树确保没有路径会比其他路径长两倍以上,因此是近似平衡的。

1.1 红黑树的规则:

  1. 每个节点必须是红色或黑色
  2. 根节点必须是黑色
  3. 红色节点的两个子节点必须都是黑色(即不允许出现连续红色节点)
  4. 从任意节点到其所有NULL节点的简单路径上,黑色节点数量必须相同

以上这些都是红黑树

注:《算法导论》等书籍补充了一条规则:所有叶节点(NIL)必须是黑色。这里的叶节点并非传统意义上的叶节点,而是指空节点(也被称为外部节点)。NIL节点的引入是为了准确标识所有路径,但在实际实现细节中《算法导论》也忽略了NIL节点,了解这个概念即可。


1.2 思考:红黑树如何保证最长路径不超过最短路径的两倍?

红黑树通过以下规则保证其平衡性:

  1. 根据规则4(每个节点到其所有后代NULL节点的路径包含相同数量的黑色节点),我们可以推导出:

    • 设从根节点到NULL节点的最短路径为全黑色节点路径,其黑色高度为bh
    • 示例:在一个bh=3的红黑树中,最短路径形式为:黑→黑→黑→NULL
  2. 结合规则2(根节点是黑色)和规则3(红色节点的子节点必须是黑色):

    • 任何路径上都不会出现连续的红色节点
    • 最长路径必须由黑色和红色节点严格交替组成
    • 最长路径的理论最大长度为:黑→红→黑→红→黑→红→黑→NULL(即2*bh-1)
    • 但实际上,由于根节点必须是黑色,典型最长路径为:黑→红→黑→红→黑→NULL(即2*bh)
  3. 路径长度的数学约束:

    • 对于任意路径x,满足:bh ≤ x ≤ 2*bh
    • 示例场景:
      • 当bh=2时:
        • 最短路径:黑→黑→NULL(长度2)
        • 最长路径:黑→红→黑→红→NULL(长度4)
      • 当bh=4时:
        • 最短路径:黑→黑→黑→黑→NULL(长度4)
        • 最长路径:黑→红→黑→红→黑→红→黑→红→NULL(长度8)
  4. 实际应用中的表现形式:

    • 并非所有红黑树都会达到理论极值
    • 插入/删除操作时,通过旋转和重新着色来维持这个比例关系
    • 在数据库索引等实际应用中,这种约束确保了查询效率的稳定性

1.3 红黑树的效率分析:

设N为红黑树中的节点总数,h为红黑树的最短路径长度(即黑色高度)。根据红黑树的以下关键性质:

  1. 每个节点非红即黑
  2. 根节点是黑色
  3. 红色节点的子节点必须是黑色
  4. 从任一节点到其每个叶子节点的路径包含相同数量的黑色节点

可以得出数学关系式: 2^h - 1 ≤ N < 2^(2h) - 1

这个不等式右边2^(2h)是因为红黑树的最长路径可能达到2h(红黑交替)。通过数学推导可得树的近似高度范围: h ≈ log₂N → 2log₂N

这意味着红黑树的增删查改操作在最坏情况下只需遍历最长路径(高度不超过2log₂N),时间复杂度保持为O(logN)。例如:

  • 包含100万个节点的红黑树,查找操作最多只需40次比较
  • 包含10亿个节点时,最多只需60次比较

与AVL树的对比分析:

  1. 平衡机制差异:

    • AVL树:严格平衡,左右子树高度差不超过1
    • 红黑树:通过颜色规则实现近似平衡,最长路径不超过最短路径的两倍
  2. 操作效率比较:

    • 查找:AVL树略优(更平衡)
    • 插入/删除:红黑树更优(旋转次数更少)
    • 综合性能:红黑树更适合频繁修改的场景
  3. 旋转操作统计:

    • 实验数据显示,插入相同数量节点时,红黑树的旋转次数约为AVL树的1/3
    • 典型场景:插入10000个随机节点,AVL树平均需要8000次旋转,红黑树仅需2500次

实际应用中的选择标准:

  • 内存数据库索引:常选用AVL树(查询密集)
  • 文件系统、STL map/set:多采用红黑树(修改频繁)
  • Java TreeMap、Linux内核:均使用红黑树实现

2. 红黑树的实现

2.1 红黑树的结构

1. 颜色枚举 Colour

cpp 复制代码
enum Colour
{
    RED,
    BLACK
};
  • 作用:定义红黑树节点的颜色状态。

  • 设计意图:红黑树节点必须为红色或黑色,符合红黑树的性质。


2. 节点结构体 RBTreeNode

cpp 复制代码
template<class K, class V>
struct RBTreeNode
{
    RBTreeNode(const pair<K, V>& kv)
        :_kv(kv)
        ,_left(nullptr)
        ,_right(nullptr)    
        ,_parent(nullptr)
        ,_col(RED)          // 新节点默认红色(符合红黑树插入规则)
    {}

    pair<K, V> _kv;          // 键值对
    RBTreeNode<K, V>* _left; // 左子节点
    RBTreeNode<K, V>* _right;// 右子节点
    RBTreeNode<K, V>* _parent; // 父节点
    Colour _col;             // 节点颜色
};

关键点

  1. 键值对存储 :使用 pair<K, V> 存储键值。

  2. 三叉链结构 :每个节点包含 _left_right_parent 指针,便于回溯和调整树结构。

  3. 颜色初始化:新节点默认红色(插入后可能触发颜色调整)。


3. 红黑树类 RBTree

cpp 复制代码
template<class K, class V>
class RBTree
{
    using Node = RBTreeNode<K, V>;  // 类型别名简化代码
public:
    
private:
    Node* _root = nullptr;  // 根节点初始化为空
};

设计分析

  1. 模板化设计 :支持泛型键值类型(KV),类似 STL 的 map

  2. 根节点管理 :私有成员 _root 表示树的根,初始为空。

  3. 待实现方法

    • 插入(Insert)需处理颜色调整和旋转。

    • 查找(Find)基于二叉搜索树规则。

    • 旋转操作(左旋 RotateLeft、右旋 RotateRight)。

具体代码:

cpp 复制代码
enum Colour
{
	RED,
	BLACK
};

template<class K, class V>
struct RBTreeNode
{
    RBTreeNode(const pair<K, V>& kv)
        :_kv(kv)
        , _left(nullptr)
        , _right(nullptr)
        , _parent(nullptr)
        , _col(RED)          // 新节点默认红色(符合红黑树插入规则)
    {}

    pair<K, V> _kv;          // 键值对
    RBTreeNode<K, V>* _left; // 左子节点
    RBTreeNode<K, V>* _right;// 右子节点
    RBTreeNode<K, V>* _parent; // 父节点
    Colour _col;             // 节点颜色
};

template<class K, class V>
class RBTree
{
	using Node = RBTreeNode<K, V>;
public:

private:
	Node* _root = nullptr;
};

2.2 旋转

旋转和AVL树的逻辑是一样的,不过只需要旋转树节点,不需要去维护平衡因子,所以具体细节就不再细讲,感兴趣可以去上一章节AVL树的实现中查看

旋转代码:

cpp 复制代码
// 右单旋
void RotateR(Node* parent)
{
	Node* subL = parent->_left;// parent的左子节点(旋转后的新根)
	Node* subLR = subL->_right;// subL的右子节点(可能为空)

	// 旋转节点
	subL->_right = parent;
	parent->_left = subLR;

	// 维护父指针
	Node* pParent = parent->_parent;
	parent->_parent = subL;
	if (subLR) //若subLR存在,更新其父指针,避免堆空指针解引用
	{
		subLR->_parent = parent;
	}
	subL->_parent = pParent;

	// 维护parent的父节点
	if (parent == _root) // parent为根节点的情况
	{
		_root = subL;
	}
	else // parent是一棵局部子树的情况
	{
		if (pParent->_left == parent)
		{
			pParent->_left = subL;
		}
		else
		{
			pParent->_right = subL;
		}
	}
}

// 左单旋
void RotateL(Node* parent)
{
	Node* subR = parent->_right;// parent的右子节点(旋转后的新根)
	Node* subRL = subR->_left;// subR的左子节点(可能为空)

	// 旋转节点
	subR->_left = parent;
	parent->_right = subRL;

	// 维护父指针
	Node* pParent = parent->_parent;
	parent->_parent = subR;
	if (subRL) //若subRL存在,更新其父指针,避免堆空指针解引用
	{
		subRL->_parent = parent;
	}
	subR->_parent = pParent;

	// 维护parent的父节点
	if (parent == _root) // parent为根节点的情况
	{
		_root = subR;
	}
	else // parent是一棵局部子树的情况
	{
		if (pParent->_left == parent)
		{
			pParent->_left = subR;
		}
		else
		{
			pParent->_right = subR;
		}
	}
}

2.3 红黑树的插入

2.3.1 红黑树插入值的基本流程

红黑树的插入操作分为两个主要阶段:首先执行标准的二叉搜索树插入,然后进行颜色调整和旋转操作以维持红黑树性质。具体步骤如下:

  1. 标准BST插入阶段:

    • 从根节点开始,比较待插入值与当前节点值
    • 根据比较结果向左或向右子树递归查找插入位置
    • 在适当位置创建新节点并建立父子关系
  2. 颜色修正阶段:

    • 新插入节点初始着色规则:
      • 空树插入时,新增节点作为根节点必须为黑色(满足规则2)
      • 非空树插入时,新增节点必须为红色(避免立即违反规则4的要求)
    • 检查红黑树性质是否被破坏:
      • 若父节点为黑色:插入完成(所有性质均保持)
      • 若父节点为红色:需要进一步处理(违反了规则3的"无连续红色节点"要求)

处理违反规则3的情况时,需要考察以下家族关系:

  • c(cur):当前新增的红色节点
  • p(parent):c的红色父节点
  • g(grandfather):p的父节点(必然为黑色,否则在插入前就违反了规则)
  • u(uncle):p的兄弟节点

根据叔节点u的不同状态,存在三种主要处理情形:

情形1:u存在且为红色

  • 将p和u改为黑色
  • 将g改为红色
  • 将g视为新的当前节点继续向上调整

情形2:u为黑色或不存在,且c-p-g形成"直线型"(左左或右右)

  • 对g执行单旋转(p为左子则右旋,p为右子则左旋)
  • 交换p和g的颜色

情形3:u为黑色或不存在,且c-p-g形成"之字形"(左右或右左)

  • 先对p执行单旋转(转化为情形2)
  • 然后按情形2处理

注:图示约定说明

  • 使用标准家族关系标记法: c:当前节点(current) p:父节点(parent) g:祖父节点(grandparent) u:叔节点(uncle)

2.3.2 情况1:变色处理

当满足以下条件时:

  • c为红色
  • p为红色
  • g为黑色
  • u存在且为红色

处理步骤:

  1. 将p和u变为黑色
  2. 将g变为红色
  3. 将g作为新的c节点继续向上更新

原理分析:

  • 由于p和u都是红色,g是黑色,将p和u变黑会增加左侧子树的黑色节点数
  • g变红后,整个子树的黑色节点数保持不变
  • 这样既解决了c和p连续红色的问题,又保持了平衡
  • 需要继续向上更新是因为g变为红色后,如果其父节点也是红色就需要进一步处理

特殊情况:

  • 若g的父节点是黑色,处理结束
  • 若g是根节点,最后将其变回黑色

说明:

  1. 情况1仅涉及变色操作,不进行旋转
  2. 无论c是p的左/右子节点,p是g的左/右子节点,处理方式相同
  3. 类似AVL树,图0展示了一个具体实例,但实际存在多种类似情况
  4. 图1进行了抽象表示:
    • d/e/f表示每条路径含hb个黑色节点的子树
    • a/b表示每条路径含hb-1个黑色节点的红色根子树(hb≥0)
  5. 图2/3/4分别展示了hb=0/1/2的具体组合情况
  6. 当hb=2时,组合情况可达上百亿种
  7. 这些示例说明无论情况多么复杂,处理方式都相同:变色后继续向上处理
  8. 因此只需关注抽象图即可理解核心原理

图0

图1

图2

图3

图4

2.3.3 情况2:单旋+变色

当满足以下条件时:

  • 结点c为红色
  • 父结点p为红色
  • 祖父结点g为黑色
  • 叔结点u不存在或存在且为黑色

若u不存在,则c必定为新增结点;若u存在且为黑色,则c原本为黑色结点,因其子树中插入新结点导致c变为红色(符合情况1:变色处理的变色规则)。

解决方案分析: 由于存在连续的红色结点(p和c),仅通过变色无法解决问题,需要进行旋转操作。具体处理方式如下:

情况1(LL型): 当p是g的左孩子且c是p的左孩子时:

  1. 以g为旋转点进行右单旋
  2. 将p变为黑色,g变为红色 结果:
  • p成为新的子树根结点
  • 子树黑色结点数量保持不变
  • 消除连续红色结点问题
  • 无需继续向上调整

情况2(RR型): 当p是g的右孩子且c是p的右孩子时:

  1. 以g为旋转点进行左单旋
  2. 将p变为黑色,g变为红色 结果:
  • p成为新的子树根结点
  • 子树黑色结点数量保持不变
  • 消除连续红色结点问题
  • 无需继续向上调整

2.3.4 情况3:双旋+变色

条件:

  • c为红色,p为红色,g为黑色
  • u不存在,或u存在且为黑色
    • 若u不存在,则c必定是新增节点
    • 若u存在且为黑色,则c原本为黑色(因子树插入符合情况1,通过变色将c从黑色变为红色)

分析: 必须将p变为黑色以解决连续红色节点问题。由于u不存在或为黑色,单纯变色无法解决问题,需要执行旋转+变色操作。

操作示例1(p为g的左子节点,c为p的右子节点):

  1. 以p为旋转点进行左单旋
  2. 以g为旋转点进行右单旋
  3. 将c变为黑色,g变为红色 最终c成为子树的新根,保持:
  • 子树黑色节点数量不变
  • 消除连续红色节点
  • 无需向上更新(c的父节点颜色不影响规则)

操作示例2(p为g的右子节点,c为p的左子节点):

  1. 以p为旋转点进行右单旋
  2. 以g为旋转点进行左单旋
  3. 将c变为黑色,g变为红色 最终效果同上,c成为新根且满足所有平衡条件。

2.2.4 情况2:双旋+变色

条件:

  • c为红色,p为红色,g为黑色
  • u不存在,或u存在且为黑色
    • 若u不存在,则c必定是新增节点
    • 若u存在且为黑色,则c原本为黑色(因子树插入符合情况1,通过变色将c从黑色变为红色)

分析: 必须将p变为黑色以解决连续红色节点问题。由于u不存在或为黑色,单纯变色无法解决问题,需要执行旋转+变色操作。

操作示例1(p为g的左子节点,c为p的右子节点):

  1. 以p为旋转点进行左单旋
  2. 以g为旋转点进行右单旋
  3. 将c变为黑色,g变为红色 最终c成为子树的新根,保持:
  • 子树黑色节点数量不变
  • 消除连续红色节点
  • 无需向上更新(c的父节点颜色不影响规则)

操作示例2(p为g的右子节点,c为p的左子节点):

  1. 以p为旋转点进行右单旋
  2. 以g为旋转点进行左单旋
  3. 将c变为黑色,g变为红色 最终效果同上,c成为新根且满足所有平衡条件。

2.3.5 代码实现和梳理

第一步:初始化与空树处理

cpp 复制代码
if (_root == nullptr) {
    _root = new Node(kv); // 创建根节点(默认颜色为红色)
    _root->_col = BLACK;  // 后续强制修正根为黑色
    return true;
}
  • 作用:若树为空,直接创建根节点并设为黑色(虽然构造函数默认红色,但最终会强制修正)。

第二步:标准BST插入

  1. 查找插入位置

    cpp 复制代码
    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. 创建新节点并挂载

    cpp 复制代码
    cur = new Node(kv); // 新节点默认为红色
    if (parent->_kv.first < kv.first) {
        parent->_right = cur; // 挂载到右子树
    } else {
        parent->_left = cur;  // 挂载到左子树
    }
    cur->_parent = parent;    // 维护三叉链

第三步:颜色修正(核心逻辑)

循环条件:父节点存在且为红色(若父节点为黑色,无需调整)。

cpp 复制代码
while (parent && parent->_col == RED) {
    Node* grandfather = parent->_parent; // 祖父节点必存在(因父为红,不可能是根)
    // 分父节点是祖父的左/右孩子两种情况处理
}

情况1:父节点是祖父的左孩子
  1. 获取叔节点

    cpp 复制代码
    Node* uncle = grandfather->_right;
  2. Case 1:叔节点存在且为红(变色处理):

    cpp 复制代码
    if (uncle && uncle->_col == RED) {
        parent->_col = uncle->_col = BLACK; // 父、叔变黑
        grandfather->_col = RED;            // 祖父变红
        cur = grandfather;                  // 向上回溯
        parent = cur->_parent;              // 检查新的父节点
    }
    • 结果:红色上移至祖父节点,可能递归处理。
  3. Case 2/3:叔节点为黑或不存在(旋转+变色):

    • Case 2(LL型):当前节点是父的左孩子(右单旋):

      cpp 复制代码
      if (parent->_left == cur) {
          RotateR(grandfather);     // 右旋祖父
          grandfather->_col = RED;  // 祖父变红
          parent->_col = BLACK;     // 父变黑
      }
    • Case 3(LR型):当前节点是父的右孩子(左右双旋):

      cpp 复制代码
      else {
          RotateL(parent);          // 先左旋父节点
          RotateR(grandfather);     // 再右旋祖父
          grandfather->_col = RED;  // 祖父变红
          cur->_col = BLACK;        // 当前节点变黑
      }
    • 结果:旋转后子树根节点变为黑色,结束调整。


对称情况:父节点是祖父的右孩子

处理逻辑与上述对称:

  1. 获取叔节点grandfather->_left

  2. Case 1:叔节点为红,变色后向上回溯。

  3. Case 2/3

    • Case 2(RR型):当前节点是父的右孩子(左单旋)。

    • Case 3(RL型):当前节点是父的左孩子(右左双旋)。


第四步:强制根节点为黑色

cpp 复制代码
_root->_col = BLACK; // 确保根节点始终为黑
  • 必要性:修正过程中祖父可能变为根且为红色,需强制修正。

示例流程(Case 3:LR型)

  1. 初始状态

    cpp 复制代码
        g(B) 
       /      
      p(R) 
       \  
        c(R) 
  2. 左旋父节点

    cpp 复制代码
      g(B) 
       /     
      c(R)   
     /      
    p(R)  
  3. 右旋祖父节点

    cpp 复制代码
      c(B)
       /   \
      p(R) g(R)
  4. 颜色调整g 变红,c 变黑,结束调整。

具体代码:

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);
	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->_left == parent)
		{
			//		g(B)
			//	p(R)	   u(分情况)
			Node* uncle = grandfather->_right;
			if (uncle && uncle->_col == RED) // 情况1:变色处理
			{
				//		g(B)
				//	p(R)	   u(存在且为红色)
				//c(R)
				// 变色
				parent->_col = uncle->_col = BLACK;
				grandfather->_col = RED;

				// 继续向上处理
				cur = grandfather;
				parent = cur->_parent;
			}
			else // 情况2,3:旋转 + 变色
			{
				if (parent->_left == cur) // 右单旋 + 变色
				{
					//		g(B)
					//	p(R)	   u(不存在或为黑色)
					//c(R)
					RotateR(grandfather);
					grandfather->_col = RED;
					parent->_col = BLACK;
				}
				else // 左右双旋 + 变色
				{
					//		g(B)
					//	p(R)	   u(不存在或为黑色)
					//    c(R)
					RotateL(parent);
					RotateR(grandfather);
					grandfather->_col = RED;
					cur->_col = BLACK;
				}
				//此时旋转之后的子树根节点为parent或cur,但都变为黑色,更新到黑结束
				break;
			}
		}
		else
		{
			//		      g(B)
			//	u(分情况)		 p(R)
			Node* uncle = grandfather->_left;
			if (uncle && uncle->_col == RED) // 情况1:变色处理
			{
				//			  g(B)
				//	u(存在且为红色)		p(R)
				//							c(R)
				// 变色
				parent->_col = uncle->_col = BLACK;
				grandfather->_col = RED;

				// 继续向上处理
				cur = grandfather;
				parent = cur->_parent;
			}
			else // 情况2,3:旋转 + 变色
			{
				if (parent->_left == cur) // 左单旋 + 变色
				{
					//			  g(B)
					//	u(不存在或为黑色)		p(R)
					//							c(R)
					RotateL(grandfather);
					grandfather->_col = RED;
					parent->_col = BLACK;
				}
				else // 右左双旋 + 变色
				{
					//			  g(R)
					//	u(不存在或为黑色)		p(R)
					//					c(R)
					RotateR(parent);
					RotateL(grandfather);
					grandfather->_col = RED;
					cur->_col = BLACK;
				}
				//此时旋转之后的子树根节点为parent或cur,但都变为黑色,更新到黑结束
				break;
			}
		}
	}

	// 暴力处理:无论是否处理到根节点,都直接把根节点变为黑色(符合根节点为黑色规则)
	_root->_col = BLACK;
	return true;
}

2.4 红黑树的查找

按二叉搜索树逻辑实现即可,搜索效率为 O(logN)

cpp 复制代码
Node* Find(const K& key)
{
	Node* cur = _root;
	while (cur)
	{
		if (cur->_kv.first < key)
		{
			cur = cur->_right;
		}
		else if (cur->_kv.first > key)
		{
			cur = cur->_left;
		}
		else
		{
			return cur;
		}
	}
	return nullptr;
}

2.5 红黑树的验证

单纯通过比较最长路径和最短路径的长度(检查最长路径不超过最短路径的2倍)并不能完全验证红黑树的合法性,因为即使满足这个条件,仍可能出现颜色规则不符的情况。当前状态可能暂时没有问题,但后续插入操作仍会导致错误。因此,我们采取直接检查红黑树四大规则的方法,只要满足这四点规则,自然就能保证最长路径不超过最短路径的2倍。

验证方法如下:

  1. 规则1:通过枚举颜色类型确保所有节点非红即黑
  2. 规则2:直接检查根节点是否为黑色即可
  3. 规则3:采用前序遍历时,反向检查父节点颜色比检查子节点颜色更方便(因为红色节点的两个子节点可能存在空值情况)
  4. 规则4:在前序遍历过程中,通过形参记录从根到当前节点的黑节点数量(blackNum),遇到黑节点时递增计数。遍历到空节点时即可得到该路径的黑节点数。取任意一条路径的黑节点数作为基准值,与其他路径进行比较验证即可。

1. 入口函数 _IsBalance()

cpp 复制代码
bool _IsBalance() 
{
    if (_root == nullptr) return true;  // 空树视为平衡
    if (_root->_col == RED) return false; // 根必须为黑

    // 计算参考黑节点数(取最左路径)
    int refNum = 0;
    Node* cur = _root;
    while (cur) {
        if (cur->_col == BLACK) refNum++;
        cur = cur->_left; // 沿最左路径向下
    }

    // 递归检查所有路径
    return Check(_root, 0, refNum);
}
  • 步骤

    1. 空树处理:直接返回平衡。

    2. 根节点颜色检查:根不为黑则立即失败。

    3. 计算参考黑节点数 :沿最左路径统计黑节点数 refNum(红黑树要求所有路径黑节点数相同)。

    4. 启动递归检查 :调用 Check 函数遍历所有路径。


2. 递归检查函数 Check()

cpp 复制代码
bool Check(Node* root, int blackNum, const int refNum) 
{
    if (root == nullptr) { 
        if (blackNum != refNum) { // 路径黑节点数不匹配
            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);
}
  • 步骤

    1. 叶子节点检查 :到达 nullptr 时,比较当前路径黑节点数 blackNum 与参考值 refNum

    2. 连续红节点检查:若当前节点和父节点均为红色,则违规。

    3. 黑节点计数:遇到黑色节点时累计数量。

    4. 递归遍历子树:深度优先遍历左右子树。

其他高度检测,节点数量,中序遍历等直接复用AVL树的代码。

具体代码:

cpp 复制代码
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
	int Height()
	{
		return _Height(_root);
	}

	bool IsBalance()
	{
		return _IsBalance();
	}

	int Size()
	{
		return _Size(_root);
	}
private:
	int _Size(Node* root)
	{
		if (root == nullptr) return 0;

		int leftSize = _Size(root->_left);
		int rightSize = _Size(root->_right);
		return leftSize + rightSize + 1;
	}

	int _Height(Node* root)
	{
		if (root == nullptr) return 0;

		int leftHigh = _Height(root->_left);
		int rightHigh = _Height(root->_right);
		return leftHigh > rightHigh ? leftHigh + 1 : rightHigh + 1;
	}

	bool Check(Node* root, int blackNum, const int refNum)
	{
		if (root == nullptr)
		{
			// 前序遍历走到空时,意味着一条路径走完了
			//cout << blackNum << endl;
			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);
	}
	bool _IsBalance()
	{
		if (_root == nullptr)
			return true;
		if (_root->_col == RED)
			return false;
		// 参考值
		int refNum = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK)
			{
				++refNum;
			}
			cur = cur->_left;
		}
		return Check(_root, 0, refNum);
	}
	

	void _InOrder(Node* root)
	{
		if (root == nullptr) return;

		_InOrder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << endl;
		_InOrder(root->_right);
	}

检测红黑树:

普通测试:

cpp 复制代码
void TestRBTTree1()
{
	RBTree<int, int> t;
	// 常规的测试用例
	//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	// 特殊的带有双旋场景的测试用例
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	for (auto e : a)
	{
		t.Insert({ e, e });
	}
	t.InOrder();
	cout << t.IsBalance() << endl;
}

常规测试用例:

带双旋的测试用例:

生成随机数插入测试:

cpp 复制代码
// 插入一堆随机值,测试平衡,顺便测试一下高度和性能等
void TestRBTTree2()
{
	const int N = 100000;
	vector<int> v;
	v.reserve(N);
	srand((unsigned int)time(0));
	for (int i = 0; i < N; i++)
	{
		v.push_back(rand() + i);
	}
	size_t begin2 = clock();
	RBTree<int, int> t;
	for (auto e : v)
	{
		t.Insert(make_pair(e, e));
	}
	size_t end2 = clock();
	cout << "Insert:" << end2 - begin2 << endl;
	cout << t.IsBalance() << endl;
	cout << "Height:" << t.Height() << endl;
	cout << "Size:" << t.Size() << endl;
	size_t begin1 = clock();
	// 确定在的值
	for (auto e : v)
	{
		t.Find(e);
	}
	// 随机值
	/*for (int i = 0; i < N; i++)
	{
		t.Find((rand() + i));
	}*/
	size_t end1 = clock();
	cout << "Find:" << end1 - begin1 << endl;
}

查找确定在的值:

查找随机值:

全部代码

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


enum Colour
{
	RED,
	BLACK
};

template<class K, class V>
struct RBTreeNode
{
    RBTreeNode(const pair<K, V>& kv)
        :_kv(kv)
        , _left(nullptr)
        , _right(nullptr)
        , _parent(nullptr)
        , _col(RED)          // 新节点默认红色(符合红黑树插入规则)
    {}

    pair<K, V> _kv;          // 键值对
    RBTreeNode<K, V>* _left; // 左子节点
    RBTreeNode<K, V>* _right;// 右子节点
    RBTreeNode<K, V>* _parent; // 父节点
    Colour _col;             // 节点颜色
};

template<class K, class V>
class RBTree
{
	using Node = RBTreeNode<K, V>;
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);
		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->_left == parent)
			{
				//		g(B)
				//	p(R)	   u(分情况)
				Node* uncle = grandfather->_right;
				if (uncle && uncle->_col == RED) // 情况1:变色处理
				{
					//		g(B)
					//	p(R)	   u(存在且为红色)
					//c(R)
					// 变色
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					// 继续向上处理
					cur = grandfather;
					parent = cur->_parent;
				}
				else // 情况2,3:旋转 + 变色
				{
					if (parent->_left == cur) // 右单旋 + 变色
					{
						//		g(B)
						//	p(R)	   u(不存在或为黑色)
						//c(R)
						RotateR(grandfather);
						grandfather->_col = RED;
						parent->_col = BLACK;
					}
					else // 左右双旋 + 变色
					{
						//		g(B)
						//	p(R)	   u(不存在或为黑色)
						//    c(R)
						RotateL(parent);
						RotateR(grandfather);
						grandfather->_col = RED;
						cur->_col = BLACK;
					}
					//此时旋转之后的子树根节点为parent或cur,但都变为黑色,更新到黑结束
					break;
				}
			}
			else
			{
				//		      g(B)
				//	u(分情况)		 p(R)
				Node* uncle = grandfather->_left;
				if (uncle && uncle->_col == RED) // 情况1:变色处理
				{
					//			  g(B)
					//	u(存在且为红色)		p(R)
					//							c(R)
					// 变色
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					// 继续向上处理
					cur = grandfather;
					parent = cur->_parent;
				}
				else // 情况2,3:旋转 + 变色
				{
					if (parent->_right == cur) // 左单旋 + 变色
					{
						//			  g(B)
						//	u(不存在或为黑色)		p(R)
						//							c(R)
						RotateL(grandfather);
						grandfather->_col = RED;
						parent->_col = BLACK;
					}
					else // 右左双旋 + 变色
					{
						//			  g(R)
						//	u(不存在或为黑色)		p(R)
						//					c(R)
						RotateR(parent);
						RotateL(grandfather);
						grandfather->_col = RED;
						cur->_col = BLACK;
					}
					//此时旋转之后的子树根节点为parent或cur,但都变为黑色,更新到黑结束
					break;
				}
			}
		}

		// 暴力处理:无论是否处理到根节点,都直接把根节点变为黑色(符合根节点为黑色规则)
		_root->_col = BLACK;
		return true;
	}

	// 右单旋
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;// parent的左子节点(旋转后的新根)
		Node* subLR = subL->_right;// subL的右子节点(可能为空)

		// 旋转节点
		subL->_right = parent;
		parent->_left = subLR;

		// 维护父指针
		Node* pParent = parent->_parent;
		parent->_parent = subL;
		if (subLR) //若subLR存在,更新其父指针,避免堆空指针解引用
		{
			subLR->_parent = parent;
		}
		subL->_parent = pParent;

		// 维护parent的父节点
		if (parent == _root) // parent为根节点的情况
		{
			_root = subL;
		}
		else // parent是一棵局部子树的情况
		{
			if (pParent->_left == parent)
			{
				pParent->_left = subL;
			}
			else
			{
				pParent->_right = subL;
			}
		}
	}

	// 左单旋
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;// parent的右子节点(旋转后的新根)
		Node* subRL = subR->_left;// subR的左子节点(可能为空)

		// 旋转节点
		subR->_left = parent;
		parent->_right = subRL;

		// 维护父指针
		Node* pParent = parent->_parent;
		parent->_parent = subR;
		if (subRL) //若subRL存在,更新其父指针,避免堆空指针解引用
		{
			subRL->_parent = parent;
		}
		subR->_parent = pParent;

		// 维护parent的父节点
		if (parent == _root) // parent为根节点的情况
		{
			_root = subR;
		}
		else // parent是一棵局部子树的情况
		{
			if (pParent->_left == parent)
			{
				pParent->_left = subR;
			}
			else
			{
				pParent->_right = subR;
			}
		}
	}

	Node* Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_kv.first < key)
			{
				cur = cur->_right;
			}
			else if (cur->_kv.first > key)
			{
				cur = cur->_left;
			}
			else
			{
				return cur;
			}
		}
		return nullptr;
	}

	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
	int Height()
	{
		return _Height(_root);
	}

	bool IsBalance()
	{
		return _IsBalance();
	}

	int Size()
	{
		return _Size(_root);
	}
private:
	int _Size(Node* root)
	{
		if (root == nullptr) return 0;

		int leftSize = _Size(root->_left);
		int rightSize = _Size(root->_right);
		return leftSize + rightSize + 1;
	}

	int _Height(Node* root)
	{
		if (root == nullptr) return 0;

		int leftHigh = _Height(root->_left);
		int rightHigh = _Height(root->_right);
		return leftHigh > rightHigh ? leftHigh + 1 : rightHigh + 1;
	}

	bool Check(Node* root, int blackNum, const int refNum)
	{
		if (root == nullptr)
		{
			// 前序遍历走到空时,意味着一条路径走完了
			//cout << blackNum << endl;
			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);
	}
	bool _IsBalance()
	{
		if (_root == nullptr)
			return true;
		if (_root->_col == RED)
			return false;
		// 参考值
		int refNum = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK)
			{
				++refNum;
			}
			cur = cur->_left;
		}
		return Check(_root, 0, refNum);
	}
	

	void _InOrder(Node* root)
	{
		if (root == nullptr) return;

		_InOrder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << endl;
		_InOrder(root->_right);
	}
private:
	Node* _root = nullptr;
};

测试代码:

cpp 复制代码
// 测试代码
void TestRBTTree1()
{
	RBTree<int, int> t;
	// 常规的测试用例
	//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	// 特殊的带有双旋场景的测试用例
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	for (auto e : a)
	{
		t.Insert({ e, e });
	}
	t.InOrder();
	cout << t.IsBalance() << endl;
}

// 插入一堆随机值,测试平衡,顺便测试一下高度和性能等
void TestRBTTree2()
{
	const int N = 100000;
	vector<int> v;
	v.reserve(N);
	srand((unsigned int)time(0));
	for (int i = 0; i < N; i++)
	{
		v.push_back(rand() + i);
	}
	size_t begin2 = clock();
	RBTree<int, int> t;
	for (auto e : v)
	{
		t.Insert(make_pair(e, e));
	}
	size_t end2 = clock();
	cout << "Insert:" << end2 - begin2 << endl;
	cout << t.IsBalance() << endl;
	cout << "Height:" << t.Height() << endl;
	cout << "Size:" << t.Size() << endl;
	size_t begin1 = clock();
	// 确定在的值
	/*for (auto e : v)
	{
		t.Find(e);
	}*/
	// 随机值
	for (int i = 0; i < N; i++)
	{
		t.Find((rand() + i));
	}
	size_t end1 = clock();
	cout << "Find:" << end1 - begin1 << endl;
}
相关推荐
YY&DS6 分钟前
Qt Designer 自定义控件已提升后,如何修改提升类
开发语言·qt
Brilliantwxx7 分钟前
【C++】 深入理解红黑树:实现与原理全解
数据结构·c++·笔记·算法·青少年编程·红黑树
右耳朵猫AI15 分钟前
Rust技术周刊 2026年第19周
开发语言·后端·rust
Leweslyh26 分钟前
基于 Confucius 架构的无人集群网络控制原语解析
开发语言·网络·php
月落归舟38 分钟前
Java线程小记
java·开发语言
摇滚侠1 小时前
01 基础语法 JavaScript 入门到精通全套教程
开发语言·javascript·ecmascript
sleven fung1 小时前
Milvus 向量数据库
开发语言·数据库·python·langchain·milvus
大大杰哥1 小时前
Java 日志框架详解:SLF4J + Logback 从入门到实战
java·开发语言·logback
ylscode1 小时前
黑客利用 GHOSTYNETWORKS 和 OMEGATECH 托管 JS 恶意软件基础设施
开发语言·安全·php·安全威胁分析
莫等闲-1 小时前
leetcode42. 接雨水 leetcode84.柱状图中最大的矩形
数据结构·c++·算法·leetcode