学习本博客之前建议看看我前面发布的AVL树代码,因为AVL树与红黑树有很多相似之处,并且本文对于旋转部分讲解将直接套用AVL旋转代码哦(>,<)
结尾有红黑树完整代码,可以结合来看
//下章将讲解红黑树平衡判断
在二叉搜索树(BST)的基础上,红黑树通过 "颜色标记 + 旋转平衡" 解决了 BST 在极端情况下退化为链表的问题,同时相比 AVL 树的 "严格平衡",红黑树的插入 / 删除效率更高(旋转次数更少),是c++ STL 中map
/set
的底层实现。本文结合完整 C++ 代码,从红黑树的核心规则入手,拆解节点设计、插入流程与平衡维护逻辑,帮你彻底搞懂红黑树的 "平衡密码"。
一、为什么需要红黑树?------ 从 BST 的缺陷说起
普通 BST 在插入有序数据(如1,2,3,4
)时,会退化为单链表,此时查找 / 插入的时间复杂度从O(log n)
骤降为O(n)
。为解决这个问题,需要一种 "自平衡二叉搜索树",常见的有 AVL 树和红黑树:
- AVL 树:要求左右子树高度差不超过 1(严格平衡),插入 / 删除时旋转次数多,适合查询密集场景;
- 红黑树:通过 "颜色规则" 维持 "近似平衡"(最长路径不超过最短路径的 2 倍),插入 / 删除时平均旋转次数仅 1~2 次,适合插入 / 删除频繁的场景。
红黑树的核心优势是 "平衡与效率的折中",这也是它成为工业级标准的关键原因。
二、红黑树的 4 条核心规则(必须牢记)
红黑树的所有平衡操作都围绕以下 4 条规则展开,任何插入 / 删除操作后,都需通过 "变色" 或 "旋转" 恢复这些规则:
- 颜色约束:每个节点只能是红色(RED)或黑色(BLACK);
- 根节点规则:根节点必须是黑色;
- 连续红节点禁止:红色节点的父节点不能是红色(即不允许出现连续两个红色节点);
- 黑高一致规则 :任意节点到其所有空子孙(
nullptr
)的路径上,黑色节点的数量必须相等(这个数量称为 "黑高")。
规则 4 是红黑树 "近似平衡" 的关键 ------ 它保证了 "最长路径(一黑一红交替)" 的长度不超过 "最短路径(全黑)" 的 2 倍,从而维持O(log n)
的时间复杂度。
三、红黑树的节点设计 ------ 数据结构基础
要实现红黑树,首先需要定义 "带颜色标记" 的节点结构。代码中使用模板类设计,支持任意键(K)值(V)类型,同时保留父节点指针(旋转和平衡维护必须依赖父节点关系)。
节点类代码解析
template<class K, class V>
class RBTreeNode
{
public:
pair<K, V> _kv; // 键值对(存储数据)
RBTreeNode<K, V>* _left; // 左子节点
RBTreeNode<K, V>* _right;// 右子节点
RBTreeNode<K, V>* _parent;// 父节点(旋转需维护父子关系)
Colour _col; // 节点颜色(RED/BLACK)
// 构造函数:初始化键值对,子节点和父节点默认为nullptr
RBTreeNode(const pair<K, V>& kv)
:_kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
{}
};
- 父节点指针:是红黑树与普通 BST 的核心区别之一 ------ 旋转时需要调整 "祖父 - 父 - 子" 三级节点的关系,必须通过父节点指针定位上层节点;
- 颜色成员 :使用
enum Colour
枚举(RED=0,BLACK=1),简化颜色判断逻辑
四、红黑树的插入流程 ------ 从 BST 插入到平衡维护
红黑树的插入分为两步:1. 按 BST 规则插入新节点;2. 检查并修复平衡(若违反规则)。其中第二步是核心,需要根据 "叔叔节点的颜色" 决定用 "变色" 还是 "旋转" 修复。
步骤 1:按 BST 规则插入新节点
插入逻辑与普通 BST 一致:从根节点出发,根据键值大小找到插入位置,创建新节点并链接到父节点。注意新节点默认设为红色------ 原因是:
- 若插入黑色节点,会直接导致 "插入路径的黑高 + 1",违反规则 4(黑高一致);
- 若插入红色节点,仅可能违反规则 3(连续红节点),修复成本更低。
代码解析(BST 插入部分)
bool Insert(const pair<K, V>& kv)
{
// 情况1:树为空,直接创建根节点(根节点必须为黑色)
if (_root == nullptr)
{
_root = new Node(kv);
_root->_col = BLACK; // 规则2:根节点为黑
return true;
}
// 情况2:树非空,按BST规则找插入位置
Node* parent = nullptr;
Node* pcur = _root;
while (pcur)
{
if (pcur->_kv.first < kv.first) // 键值比当前节点大,走右子树
{
parent = pcur;
pcur = pcur->_right;
}
else if (pcur->_kv.first > kv.first) // 键值比当前节点小,走左子树
{
parent = pcur;
pcur = pcur->_left;
}
else // 键值已存在,插入失败(红黑树不允许重复键)
{
return false;
}
}
// 创建新节点(默认红色),链接到父节点
pcur = new Node(kv);
pcur->_col = RED; // 新节点默认红色,降低修复成本
if (parent->_kv.first < kv.first)
{
parent->_right = pcur; // 新节点是父节点的右孩子
}
else
{
parent->_left = pcur; // 新节点是父节点的左孩子
}
pcur->_parent = parent; // 维护父节点指针
// 关键:插入后检查平衡(父节点为红时才可能违反规则3)
// ... 平衡维护逻辑见下文 ...
}
步骤 2:插入后的平衡维护 ------ 核心中的核心
平衡维护的触发条件是 "父节点为红色"(此时新节点 + 父节点为连续红节点,违反规则 3)。需根据 "祖父节点的另一个孩子(叔叔节点)的颜色",分 3 种场景处理:
场景 1:叔叔节点存在且为红色 ------ 仅需变色
适用条件:
- 父节点(parent)为红色;
- 祖父节点(grandfather)存在(父节点为红则祖父必为黑,否则插入前已违反规则 3);
- 叔叔节点(uncle,祖父的另一个孩子)存在且为红色。
变色逻辑:
- 父节点(parent)→ 黑色;
- 叔叔节点(uncle)→ 黑色;
- 祖父节点(grandfather)→ 红色;
- 继续向上检查祖父节点的父节点(可能仍存在连续红节点)。
示意图(以父节点是祖父左孩子为例):
// 变色前(违反规则3:parent和pcur均为红)
grandfather(BLACK)
/ \
parent(RED) uncle(RED) // 叔叔为红
/
pcur(RED) // 新插入节点
// 变色后(修复规则3,祖父变红需继续向上检查)
grandfather(RED) // 祖父变红,可能与它的父节点冲突
/ \
parent(BLACK) uncle(BLACK) // 父和叔变黑
/
pcur(RED)
代码解析:
while (parent && parent->_col == RED) // 父节点为红才需要处理
{
Node* grandfather = parent->_parent; // 祖父节点(必存在,否则父为根,根不能为红)
// 子场景A:父节点是祖父的左孩子(对称处理右孩子场景)
if (grandfather->_left == parent)
{
Node* uncle = grandfather->_right; // 叔叔节点(祖父的右孩子)
// 场景1:叔叔存在且为红 → 仅变色
if (uncle && uncle->_col == RED)
{
grandfather->_col = RED; // 祖父变红
parent->_col = BLACK; // 父变黑
uncle->_col = BLACK; // 叔变黑
// 继续向上检查祖父节点(祖父变红可能与它的父节点冲突)
pcur = grandfather;
parent = pcur->_parent;
}
// 场景2和3:叔叔不存在或为黑 → 需要旋转(见下文)
else
{
// ... 旋转逻辑 ...
}
}
// 子场景B:父节点是祖父的右孩子(与左孩子场景对称)
else
{
// ... 对称逻辑 ...
}
}
// 最终确保根节点为黑(防止祖父节点变红后根为红)
_root->_col = BLACK;
场景 2:叔叔不存在或为黑色,且新节点在父节点同侧 ------ 单旋 + 变色
适用条件:
- 父节点为红,叔叔不存在或为黑;
- 新节点(pcur)与父节点(parent)在祖父节点的同侧(如父是祖父左孩子,新节点是父的左孩子 → 左左 LL 场景;父是祖父右孩子,新节点是父的右孩子 → 右右 RR 场景)。
处理逻辑:
- 对祖父节点做 "单旋"(LL 场景做右单旋,RR 场景做左单旋);
- 旋转后调整颜色:父节点→黑色,祖父节点→红色(修复规则 3)。
以 LL 场景(右单旋)为例,示意图:
// 旋转前(违反规则3:parent和pcur均为红,叔叔为黑/不存在)
grandfather(BLACK)
/
parent(RED) uncle(BLACK/不存在)
/
pcur(RED) // 新节点与父同侧(左)
// 步骤1:对祖父做右单旋(调整节点关系)
parent(RED)
/ \
pcur(RED) grandfather(BLACK)
\
uncle(BLACK/不存在)
// 步骤2:变色(父变黑,祖父变红,修复规则3)
parent(BLACK) // 父节点变黑,消除连续红
/ \
pcur(RED) grandfather(RED)
\
uncle(BLACK/不存在)
单旋函数实现(右单旋 RotateR):单旋的核心是 "调整三级节点的指针关系",避免出现悬空指针。以右单旋为例(处理 LL 场景):
// 右单旋:针对"左子树过高"的情况(如LL场景)
void RotateR(Node* parent)
{
Node* subL = parent->_left; // 父节点的左子树(要成为新的父节点)
Node* subLR = subL->_right; // 左子树的右子树(旋转后要挂到原父节点的左子树)
Node* pparent = parent->_parent; // 原父节点的父节点(祖父节点)
// 1. 把subLR挂到parent的左子树
parent->_left = subLR;
if (subLR) // 若subLR非空,更新其父亲为parent
subLR->_parent = parent;
// 2. 把parent挂到subL的右子树
subL->_right = parent;
parent->_parent = subL;
// 3. 把subL挂到原祖父节点(pparent)的对应位置
if (parent == _root) // 原parent是根节点,更新根为subL
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (pparent->_left == parent) // 原parent是祖父的左孩子
pparent->_left = subL;
else // 原parent是祖父的右孩子
pparent->_right = subL;
subL->_parent = pparent;
}
}
场景 2 代码解析(LL 场景右单旋 + 变色):
else // 叔叔不存在或为黑
{
// 场景2:新节点与父节点同侧(LL场景)
if (pcur == parent->_left)
{
RotateR(grandfather); // 对祖父做右单旋
// 变色:父节点变黑,祖父节点变红
parent->_col = BLACK;
grandfather->_col = RED;
}
// 场景3:新节点与父节点异侧(LR场景,见下文)
else
{
// ... 双旋逻辑 ...
}
}
场景 3:叔叔不存在或为黑色,且新节点在父节点异侧 ------ 双旋 + 变色
适用条件:
- 父节点为红,叔叔不存在或为黑;
- 新节点(pcur)与父节点(parent)在祖父节点的异侧(如父是祖父左孩子,新节点是父的右孩子 → 左右 LR 场景;父是祖父右孩子,新节点是父的左孩子 → 右左 RL 场景)。
处理逻辑:
- 先对父节点做 "单旋"(LR 场景做左单旋,RL 场景做右单旋),将异侧场景转化为同侧场景;
- 再对祖父节点做 "单旋"(LR 场景做右单旋,RL 场景做左单旋);
- 旋转后调整颜色:新节点→黑色,祖父节点→红色(修复规则 3)。
以 LR 场景(先左旋父,再右旋祖父)为例,示意图:
// 旋转前(违反规则3:parent和pcur均为红,叔叔为黑/不存在)
grandfather(BLACK)
/
parent(RED) uncle(BLACK/不存在)
\
pcur(RED) // 新节点与父异侧(右)
// 步骤1:对父节点做左单旋(转化为LL场景)
grandfather(BLACK)
/
pcur(RED) uncle(BLACK/不存在)
/
parent(RED)
// 步骤2:对祖父节点做右单旋
pcur(RED)
/ \
parent(RED) grandfather(BLACK)
\
uncle(BLACK/不存在)
// 步骤3:变色(新节点变黑,祖父变红,修复规则3)
pcur(BLACK) // 新节点变黑,消除连续红
/ \
parent(RED) grandfather(RED)
\
uncle(BLACK/不存在)
场景 3 代码解析(LR 场景双旋 + 变色):
else // 新节点与父节点异侧(LR场景)
{
RotateL(parent); // 第一步:对父节点做左单旋
RotateR(grandfather); // 第二步:对祖父节点做右单旋
// 变色:新节点(pcur)变黑,祖父节点变红
pcur->_col = BLACK;
grandfather->_col = RED;
}
五、上篇总结:红黑树插入的核心逻辑
红黑树的插入平衡本质是 "根据叔叔节点颜色选择修复方式":
- 叔叔为红 → 仅变色,继续向上检查;
- 叔叔为黑 / 不存在 → 按新节点与父节点的位置关系,选择单旋或双旋 + 变色。
通过这三种场景的处理,最终能恢复红黑树的 4 条规则,维持 "近似平衡"。下篇将重点讲解红黑树的 "平衡验证逻辑"(如何判断插入后的树是否合法)、测试用例设计与常见问题调试技巧,帮你实现 "从代码到验证" 的完整闭环
对了,这里附上我自己写的完整红黑树代码,里面有很多注释哦,大家也可以结合代码看文章:
//可以复制到编译器,这样子看起来更舒服
#include <iostream>
using namespace std;
enum Colour
{
RED,
BLACK
};
//RBTree的实现
//红黑树规则
//1, 只能由红黑两个色
//2,根节点必须是黑色
//3,红色节点的两个孩子不可以是红色,即不可以同时出现连续两个红色节点
//4,任意一个节点到左右nullptr的最简路径出现的黑色节点必须相等
//假如一条路径有x个黑节点
//4说明了最短路径为x(即都是黑节点)
//234共同说明最长路径为2x(即一黑一红这样排列直到有x个黑节点)
namespace ym
{
template<class K, class V>
class RBTreeNode
{
public:
pair<K, V> _kv;
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
Colour _col;
RBTreeNode(const pair<K, V>& kv)
:_kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
{
}
};
//K->key(比较一般使用关键词key)
template<class K, class V>
class RBTree
{
using Node = RBTreeNode<K, V>;
public:
RBTree() = default; //强制生成默认构造
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
_root->_col = BLACK;
return true;
}
else
{
Node* parent = nullptr;
Node* pcur = _root;
while (pcur)
{
if (pcur->_kv.first < kv.first)
{
parent = pcur;
pcur = pcur->_right;
}
else if (pcur->_kv.first > kv.first)
{
parent = pcur;
pcur = pcur->_left;
}
else
{
return false;
}
}
pcur = new Node(kv);
pcur->_col = RED;
if (parent->_kv.first < kv.first)
{
parent->_right = pcur;
}
else
{
parent->_left = pcur;
}
// 链接父亲
pcur->_parent = parent;
while (parent && parent->_col == RED) //如果父亲是红色,说明了出现了连续的两个红色节点,需要继续处理
{
Node* grandfather = parent->_parent;
//各个字母的含义
//g -> grandfather (父亲节点的父亲)
//p -> parent
//c -> children (父亲节点的孩子, 即插入的节点)
//u -> uncle (父亲的父亲节点的另一个孩子, 即和parent同高度的另一个节点)
//不管怎么插入什么,必须插入红色节点,因为插入黑色节点,就必然违反规则4,红色节点可能违反3,但也可能
//不违反,所以必须插入红色节点,并且parent节点在旋转后可能变为黑色,不然可能违反规则三
//简单表述就是插入必定是红色节点, parent可能会变色
//分三种大情况去讨论
if (grandfather->_left == parent)
{
// g
// p u
//c
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED)
{
//第一种大情况(只变色)
//g为黑, p为红, c为红, u为红且存在
//-->g变为红, p变为黑, c变为红, u变为黑
//但是还没完,因为g的parent可能也是红色,所以继续上述过程(前提是满足g为黑, p为红, c为红, u为红且存在,不然就是别的情况了)
//直到g的parent为黑或者g到达了根节点(此时直接把g变为黑色,满足规则1)才可以退出
//变色
grandfather->_col = RED;
parent->_col = BLACK;
uncle->_col = BLACK;
//继续往上处理
pcur = grandfather;
parent = pcur->_parent;
}
else
{
//下面简绍要旋转的两种情况,区别是pcur插入的位置
// 双旋 单旋
//pcur插入在parent异侧还是同侧
if (pcur == parent->_left)
{
//第二种大情况(单旋+变色)
//g为黑, p为红, c为红且插入在parent同侧, u为黑或者不存在 这个情况也暗示了c是插入到中间了,因为为了保持规则四,说明p下面肯定还有黑色节点,不然左边肯定比右边多
//假如pcur在parent的左边,就使用右单旋,反之使用左单旋
//然后改变颜色
//下面演示右单旋
// (黑)g (红)p (黑)p
// (红)p u(黑) -> (红)c g(黑) -> (红)c g(红)
//(红)c u(黑) u(黑)
//单旋
RotateR(grandfather);
//变色
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
//第三种大情况(双旋+变色)
//g为黑, p为红, c为红且插入在parent异侧, u为黑或者不存在 这个情况也暗示了c是插入到中间了,因为为了保持规则四,说明p下面肯定还有黑色节点,不然左边肯定比右边多
//假如pcur在parent的右边(前提是parent在grandfather的左边),就先使用左单旋,再使用右单旋
//然后改变颜色
//双旋
RotateL(parent);
RotateR(grandfather);
//变色
pcur->_col = BLACK;
grandfather->_col = RED;
}
}
}
else
{
//与上面都相反
// g
//u p
Node* uncle = grandfather->_left;
// 叔叔存在且为红,变⾊即可
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
// 继续往上处理
pcur = grandfather;
parent = pcur->_parent;
}
else // 叔叔不存在,或者存在且为⿊
{
// 旋转+变⾊
if (pcur == parent->_right)
{
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
RotateR(parent);
RotateL(grandfather);
pcur->_col = BLACK;
grandfather->_col = RED;
}
}
}
}
_root->_col = BLACK;
return true;
}
}
// 单旋是纯粹的一边高
void RotateR(Node* parent) //左边多,右单旋
{
Node* subL = parent->_left; //左边
Node* subLR = subL->_right; //左边的右边
Node* pparent = parent->_parent; //父亲的父亲节点
// 6(p)
// 4(L) 7
// 3 5(LR)
//(插入)
parent->_left = subLR;
if (subLR) //非空就可以修改
subLR->_parent = parent;
parent->_parent = subL;
subL->_right = parent;
if (parent == _root)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (pparent->_left == parent)
{
pparent->_left = subL;
}
else
{
pparent->_right = subL;
}
subL->_parent = pparent;
}
}
void RotateL(Node* parent) //右边多,左单旋
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* pparent = parent->_parent;
if (subRL)
subRL->_parent = parent;
parent->_right = subRL;
subR->_left = parent;
parent->_parent = subR;
if (_root == parent)
{
subR->_parent = nullptr;
_root = subR;
}
else
{
if (pparent->_left == parent)
{
pparent->_left = subR;
}
else
{
pparent->_right = subR;
}
subR->_parent = pparent;
}
}
bool Find(const K& key)
{
Node* pcur = _root;
while (pcur)
{
if (pcur->_kv.first < key)
{
pcur = pcur->_right;
}
else if (pcur->_kv.first > key)
{
pcur = pcur->_left;
}
else
{
return true;
}
}
return false;
}
bool IsBalence()
{
return _IsBalence(_root);
}
void InOrder()
{
_InOrder(_root);
}
private:
void _InOrder(const Node* root)
{
if (root == nullptr)
{
//cout << "Nullptr ";
return;
}
_InOrder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << endl;
_InOrder(root->_right);
}
bool check(Node* root, int blackNum, const int refNum)
{
if (root == nullptr)
{
if (refNum != blackNum)
{
cout << "任意节点到REF(nullptr节点)最简路径上的black节点数量不相等,不满足规则4" << endl;
return false;
}
return true;
}
if (root->_col != RED && root->_col != BLACK)
{
cout << "出现了其他颜色的节点,不满足规则1" << endl;
return false;
}
if (root != _root && root->_col == RED && root->_parent->_col == RED)
{
cout << "出现了两个连续的红色节点,不满足规则3" << endl;
return false;
}
if (root->_col == BLACK)
{
blackNum++;
}
return check(root->_left, blackNum, refNum) && check(root->_right, blackNum, refNum);
}
bool _IsBalence(Node* _root)
{
if (_root == nullptr)
{
return true;
}
if (_root->_col == RED)
{
cout << "根节点颜色错误,不符合规则2" << endl;
return false;
}
int refNum = 0; //这里使用refNum记录根节点到REF(nullptr节点)的路径上black的个数来判断是否任意节点到REF节点都满足规则4
Node* pcur = _root;
while (pcur)
{
if (pcur->_col == BLACK)
{
refNum++;
}
pcur = pcur->_left;
}
return check(_root, 0, refNum);
}
Node* _root = nullptr;
};
}
void TestRBTree1()
{
ym::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 << endl << t.IsBalence() << endl;
}
int main()
{
TestRBTree1();
return 0;
}