当前位置: 首页 > news >正文

【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

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

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


2. 节点结构体 RBTreeNode

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

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

设计分析

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

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

  3. 待实现方法

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

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

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

具体代码:

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树的实现中查看

旋转代码:

// 右单旋
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 代码实现和梳理

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

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


第二步:标准BST插入

  1. 查找插入位置

    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;    // 维护三叉链

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

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

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

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

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

    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型):当前节点是父的左孩子(右单旋):

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

      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型):当前节点是父的左孩子(右左双旋)。


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

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


示例流程(Case 3:LR型)

  1. 初始状态

        g(B) /      p(R) \  c(R) 
  2. 左旋父节点

      g(B) /     c(R)   /      
    p(R)  
  3. 右旋祖父节点

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

具体代码:

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)

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()

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()

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树的代码。

具体代码:

	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);}

检测红黑树:

普通测试:

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;
}

查找确定在的值:

查找随机值:

全部代码

#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;
};

测试代码:

// 测试代码
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;
}

相关文章:

  • JWT与布隆过滤器结合使用指南
  • C++编程单例模式详细解释---模拟一个网络配置管理器,负责管理和分发网络连接参数
  • 分布式缓存:三万字详解Redis
  • 华为OD机试真题—— 矩阵匹配(2025B卷:200分)Java/python/JavaScript/C/C++/GO最佳实现
  • Redis数据安全分析
  • 上海医日健集团物联网专利技术领跑智慧药房赛道
  • Lua 脚本在 Redis 中的运用-24 (使用 Lua 脚本实现原子计数器)
  • (27)运动目标检测 之 分类(如YOLO) 数据集自动划分
  • 大语言模型在软件工程中的应用、影响与展望
  • 什么是 Spring MVC 的异步请求处理?
  • ZLG USBCANFD python UDS刷写脚本
  • 【HarmonyOS5】DevEco Studio 使用指南:代码阅读与编辑功能详解
  • 【寻找Linux的奥秘】第八章:进程控制
  • string的使用和模拟实现
  • LeetCode --- 450周赛
  • 【图像大模型】ControlNet:深度条件控制的生成模型架构解析
  • 项目阅读:Instruction Defense
  • 前端vue3实现图片懒加载
  • 漫谈英伟达GPU架构进化史:从Celsius到Blackwell
  • 《仿盒马》app开发技术分享-- 原生地图展示(端云一体)
  • 网站建设服务器端软件/下载一个百度时事新闻
  • 使用php做的网站/有效获客的六大渠道
  • 低代码开发会废了程序员吗/sem优化托管
  • 广州的兼职网站建设/长沙官网seo技巧
  • 网站怎么做交易/网站的排名优化怎么做
  • 婚庆公司网站建设总结/二级域名查询入口