【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;
}
相关推荐
虾球xz6 小时前
CppCon 2016 学习:GAME ENGINE USING C++11
大数据·开发语言·c++·学习
Jet45056 小时前
第100+42步 ChatGPT学习:R语言实现阈值调整
开发语言·学习·chatgpt·r语言
虾球xz6 小时前
CppCon 2016 学习:fixed_point Library
开发语言·c++·学习
希希不嘻嘻~傻希希6 小时前
CSS 字体与文本样式笔记
开发语言·前端·javascript·css·ecmascript
HaiQinyanAN6 小时前
【学习笔记】nlohmannjson&&cjson
c++·笔记·学习·json
C语言小火车6 小时前
【C语言】银行账户管理系统丨源码+解析
c语言·c++·算法·课程设计
寄思~7 小时前
Python学习笔记:错误和异常处理
开发语言·笔记·python·学习
clmm1237 小时前
Java动态生成Nginx服务配置
java·开发语言·nginx
东方芷兰7 小时前
Leetcode 刷题记录 17 —— 堆
java·c++·b树·算法·leetcode·职场和发展
lzb_kkk7 小时前
【MFC】编辑框、下拉框、列表控件
c语言·开发语言·c++·mfc·1024程序员节