【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 红黑树的规则:
- 每个节点必须是红色或黑色
- 根节点必须是黑色
- 红色节点的两个子节点必须都是黑色(即不允许出现连续红色节点)
- 从任意节点到其所有NULL节点的简单路径上,黑色节点数量必须相同
以上这些都是红黑树
注:《算法导论》等书籍补充了一条规则:所有叶节点(NIL)必须是黑色。这里的叶节点并非传统意义上的叶节点,而是指空节点(也被称为外部节点)。NIL节点的引入是为了准确标识所有路径,但在实际实现细节中《算法导论》也忽略了NIL节点,了解这个概念即可。
1.2 思考:红黑树如何保证最长路径不超过最短路径的两倍?
红黑树通过以下规则保证其平衡性:
-
根据规则4(每个节点到其所有后代NULL节点的路径包含相同数量的黑色节点),我们可以推导出:
- 设从根节点到NULL节点的最短路径为全黑色节点路径,其黑色高度为bh
- 示例:在一个bh=3的红黑树中,最短路径形式为:黑→黑→黑→NULL
-
结合规则2(根节点是黑色)和规则3(红色节点的子节点必须是黑色):
- 任何路径上都不会出现连续的红色节点
- 最长路径必须由黑色和红色节点严格交替组成
- 最长路径的理论最大长度为:黑→红→黑→红→黑→红→黑→NULL(即2*bh-1)
- 但实际上,由于根节点必须是黑色,典型最长路径为:黑→红→黑→红→黑→NULL(即2*bh)
-
路径长度的数学约束:
- 对于任意路径x,满足:bh ≤ x ≤ 2*bh
- 示例场景:
- 当bh=2时:
- 最短路径:黑→黑→NULL(长度2)
- 最长路径:黑→红→黑→红→NULL(长度4)
- 当bh=4时:
- 最短路径:黑→黑→黑→黑→NULL(长度4)
- 最长路径:黑→红→黑→红→黑→红→黑→红→NULL(长度8)
- 当bh=2时:
-
实际应用中的表现形式:
- 并非所有红黑树都会达到理论极值
- 插入/删除操作时,通过旋转和重新着色来维持这个比例关系
- 在数据库索引等实际应用中,这种约束确保了查询效率的稳定性
1.3 红黑树的效率分析:
设N为红黑树中的节点总数,h为红黑树的最短路径长度(即黑色高度)。根据红黑树的以下关键性质:
- 每个节点非红即黑
- 根节点是黑色
- 红色节点的子节点必须是黑色
- 从任一节点到其每个叶子节点的路径包含相同数量的黑色节点
可以得出数学关系式: 2^h - 1 ≤ N < 2^(2h) - 1
这个不等式右边2^(2h)是因为红黑树的最长路径可能达到2h(红黑交替)。通过数学推导可得树的近似高度范围: h ≈ log₂N → 2log₂N
这意味着红黑树的增删查改操作在最坏情况下只需遍历最长路径(高度不超过2log₂N),时间复杂度保持为O(logN)。例如:
- 包含100万个节点的红黑树,查找操作最多只需40次比较
- 包含10亿个节点时,最多只需60次比较
与AVL树的对比分析:
-
平衡机制差异:
- AVL树:严格平衡,左右子树高度差不超过1
- 红黑树:通过颜色规则实现近似平衡,最长路径不超过最短路径的两倍
-
操作效率比较:
- 查找:AVL树略优(更平衡)
- 插入/删除:红黑树更优(旋转次数更少)
- 综合性能:红黑树更适合频繁修改的场景
-
旋转操作统计:
- 实验数据显示,插入相同数量节点时,红黑树的旋转次数约为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; // 节点颜色
};
关键点:
-
键值对存储:使用
pair<K, V>
存储键值。 -
三叉链结构:每个节点包含
_left
、_right
、_parent
指针,便于回溯和调整树结构。 -
颜色初始化:新节点默认红色(插入后可能触发颜色调整)。
3. 红黑树类 RBTree
template<class K, class V>
class RBTree
{using Node = RBTreeNode<K, V>; // 类型别名简化代码
public:private:Node* _root = nullptr; // 根节点初始化为空
};
设计分析:
-
模板化设计:支持泛型键值类型(
K
和V
),类似 STL 的map
。 -
根节点管理:私有成员
_root
表示树的根,初始为空。 -
待实现方法:
-
插入(
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 红黑树插入值的基本流程
红黑树的插入操作分为两个主要阶段:首先执行标准的二叉搜索树插入,然后进行颜色调整和旋转操作以维持红黑树性质。具体步骤如下:
-
标准BST插入阶段:
- 从根节点开始,比较待插入值与当前节点值
- 根据比较结果向左或向右子树递归查找插入位置
- 在适当位置创建新节点并建立父子关系
-
颜色修正阶段:
- 新插入节点初始着色规则:
- 空树插入时,新增节点作为根节点必须为黑色(满足规则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存在且为红色
处理步骤:
- 将p和u变为黑色
- 将g变为红色
- 将g作为新的c节点继续向上更新
原理分析:
- 由于p和u都是红色,g是黑色,将p和u变黑会增加左侧子树的黑色节点数
- g变红后,整个子树的黑色节点数保持不变
- 这样既解决了c和p连续红色的问题,又保持了平衡
- 需要继续向上更新是因为g变为红色后,如果其父节点也是红色就需要进一步处理
特殊情况:
- 若g的父节点是黑色,处理结束
- 若g是根节点,最后将其变回黑色
说明:
- 情况1仅涉及变色操作,不进行旋转
- 无论c是p的左/右子节点,p是g的左/右子节点,处理方式相同
- 类似AVL树,图0展示了一个具体实例,但实际存在多种类似情况
- 图1进行了抽象表示:
- d/e/f表示每条路径含hb个黑色节点的子树
- a/b表示每条路径含hb-1个黑色节点的红色根子树(hb≥0)
- 图2/3/4分别展示了hb=0/1/2的具体组合情况
- 当hb=2时,组合情况可达上百亿种
- 这些示例说明无论情况多么复杂,处理方式都相同:变色后继续向上处理
- 因此只需关注抽象图即可理解核心原理
图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的左孩子时:
- 以g为旋转点进行右单旋
- 将p变为黑色,g变为红色 结果:
- p成为新的子树根结点
- 子树黑色结点数量保持不变
- 消除连续红色结点问题
- 无需继续向上调整
情况2(RR型): 当p是g的右孩子且c是p的右孩子时:
- 以g为旋转点进行左单旋
- 将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的右子节点):
- 以p为旋转点进行左单旋
- 以g为旋转点进行右单旋
- 将c变为黑色,g变为红色 最终c成为子树的新根,保持:
- 子树黑色节点数量不变
- 消除连续红色节点
- 无需向上更新(c的父节点颜色不影响规则)
操作示例2(p为g的右子节点,c为p的左子节点):
- 以p为旋转点进行右单旋
- 以g为旋转点进行左单旋
- 将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的右子节点):
- 以p为旋转点进行左单旋
- 以g为旋转点进行右单旋
- 将c变为黑色,g变为红色 最终c成为子树的新根,保持:
- 子树黑色节点数量不变
- 消除连续红色节点
- 无需向上更新(c的父节点颜色不影响规则)
操作示例2(p为g的右子节点,c为p的左子节点):
- 以p为旋转点进行右单旋
- 以g为旋转点进行左单旋
- 将c变为黑色,g变为红色 最终效果同上,c成为新根且满足所有平衡条件。
2.3.5 代码实现和梳理
第一步:初始化与空树处理
if (_root == nullptr) {_root = new Node(kv); // 创建根节点(默认颜色为红色)_root->_col = BLACK; // 后续强制修正根为黑色return true;
}
-
作用:若树为空,直接创建根节点并设为黑色(虽然构造函数默认红色,但最终会强制修正)。
第二步:标准BST插入
-
查找插入位置:
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; // 祖父节点必存在(因父为红,不可能是根)// 分父节点是祖父的左/右孩子两种情况处理
}
情况1:父节点是祖父的左孩子
-
获取叔节点:
Node* uncle = grandfather->_right;
-
Case 1:叔节点存在且为红(变色处理):
if (uncle && uncle->_col == RED) {parent->_col = uncle->_col = BLACK; // 父、叔变黑grandfather->_col = RED; // 祖父变红cur = grandfather; // 向上回溯parent = cur->_parent; // 检查新的父节点 }
-
结果:红色上移至祖父节点,可能递归处理。
-
-
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; // 当前节点变黑 }
-
结果:旋转后子树根节点变为黑色,结束调整。
-
对称情况:父节点是祖父的右孩子
处理逻辑与上述对称:
-
获取叔节点:
grandfather->_left
。 -
Case 1:叔节点为红,变色后向上回溯。
-
Case 2/3:
-
Case 2(RR型):当前节点是父的右孩子(左单旋)。
-
Case 3(RL型):当前节点是父的左孩子(右左双旋)。
-
第四步:强制根节点为黑色
_root->_col = BLACK; // 确保根节点始终为黑
-
必要性:修正过程中祖父可能变为根且为红色,需强制修正。
示例流程(Case 3:LR型)
-
初始状态:
g(B) / p(R) \ c(R)
-
左旋父节点:
g(B) / c(R) / p(R)
-
右旋祖父节点:
c(B)/ \p(R) g(R)
-
颜色调整:
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:通过枚举颜色类型确保所有节点非红即黑
- 规则2:直接检查根节点是否为黑色即可
- 规则3:采用前序遍历时,反向检查父节点颜色比检查子节点颜色更方便(因为红色节点的两个子节点可能存在空值情况)
- 规则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);
}
-
步骤:
-
空树处理:直接返回平衡。
-
根节点颜色检查:根不为黑则立即失败。
-
计算参考黑节点数:沿最左路径统计黑节点数
refNum
(红黑树要求所有路径黑节点数相同)。 -
启动递归检查:调用
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);
}
-
步骤:
-
叶子节点检查:到达
nullptr
时,比较当前路径黑节点数blackNum
与参考值refNum
。 -
连续红节点检查:若当前节点和父节点均为红色,则违规。
-
黑节点计数:遇到黑色节点时累计数量。
-
递归遍历子树:深度优先遍历左右子树。
-
其他高度检测,节点数量,中序遍历等直接复用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;
}