【彻底吃透C++红黑树 | 五大性质、变色旋转原理、完整手写实现、逐行注释、考点精讲
定位:适配高校期末考核、考研数据结构、互联网秋招底层手撕代码标准
核心特点:内容无删减、逻辑无跳步、严格遵循学术定义,实现原理与底层代码一一对应。精准贴合考情:完整实现秋招必考的旋转、插入、变色底层源码,删除模块仅讲解核心思路,不实现底层代码。本文为纯手写红黑树底层教程,与STL map上层调用完全区分,是吃透C++ map底层内核的核心资料。
前置关键区分(必看):此前的Map教程为【STL容器上层API调用】,本文档为【从零手写红黑树底层数据结构】,完整实现节点定义、染色机制、旋转平衡、插入修复核心逻辑,是Map/Set容器的底层核心原理。
一、红黑树前置认知 & 为什么替代AVL树
1.1 平衡树家族对比
AVL树是严格平衡二叉树,要求左右子树高度差严格不超过1,平衡精度极高,但插入、删除操作极易触发失衡,需要频繁旋转调整,数据修改效率较低。
红黑树是弱平衡二叉搜索树,不追求绝对高度平衡,通过一套严格的颜色约束规则,限制树的最长路径与最短路径比例,大幅减少旋转次数,插入、删除的整体效率显著优于AVL树。
工业级应用结论(面试常考):频繁增删场景优先使用红黑树,频繁查询场景优先使用AVL树。C++ map/set、Java TreeMap、Linux内核进程调度均采用红黑树作为底层数据结构。
1.2 红黑树核心定位
红黑树本质是基于颜色约束实现自平衡的二叉搜索树
所有子树依然满足BST规则:左子树值 < 根 < 右子树值
其平衡机制不依赖节点高度差,而是依靠五大核心性质强制约束树结构,保证最长路径长度不超过最短路径的2倍,稳定维持O(logn)时间复杂度。
二、红黑树五大硬性性质
以下五条为标准学术定义,是期末考试、作业作答、面试口述的标准答案,需精准记忆。
-
性质1:每个节点,只能是黑色 或 红色。
-
性质2:根节点必须是黑色。
-
性质3:所有叶子节点(空节点NIL)均为黑色。
-
性质4:不存在连续两个红色节点(红节点的孩子一定是黑节点)。
-
性质5:从任意节点出发,到达其所有叶子节点的路径上,黑色节点数量相等(黑高一致)。
2.1 五大性质核心推论(
-
最短路径:全黑色节点
-
最长路径:红黑交替节点
-
最长路径长度 ≤ 2 × 最短路径长度
-
整树高度稳定在 O(logn),不会退化斜链
三、红黑树核心基础概念
3.1 节点颜色定义
标准枚举定义:RED=红色、BLACK=黑色
3.2 NIL 虚拟叶子节点(重点难点)
红黑树摒弃普通BST的空指针判定,将所有空叶子统一替换为黑色NIL虚拟节点。
核心作用:统一所有边界判断逻辑,严格保证黑高一致的性质成立
3.3 失衡修复两大手段
红黑树不依赖高度平衡,仅靠两种操作修复所有违规:
-
变色:修改节点红/黑颜色,修复红节点连续、黑高不一致问题
-
旋转:左旋、右旋,调整树结构,不破坏BST规则
四、基础工具模块(完整手写+逐行注释)
本节包含:颜色枚举、节点结构体、全局NIL节点、构造函数、析构函数、高度获取、基础旋转,全部为底层核心依赖,无任何上层封装。
#include <iostream>
using namespace std;
// 红黑树节点颜色枚举 严格标准定义
enum Color
{
RED, // 红色节点
BLACK // 黑色节点
};
/**
* @brief 红黑树节点结构体
* @param val 节点存储数据
* @param color 节点颜色
* @param left/right 左右孩子指针
* @param parent 父节点指针(红黑树必备,用于回溯祖父、叔叔节点)
*/
struct RBNode
{
int val;
Color color;
RBNode* left;
RBNode* right;
RBNode* parent;
// 节点构造函数:新节点默认红色
RBNode(int v) : val(v), color(RED), left(nullptr), right(nullptr), parent(nullptr) {}
};
// 全局唯一NIL黑色虚拟叶子节点(统一边界、保证黑高规则成立)
RBNode* NIL = new RBNode(0);
/**
* @brief 红黑树类
* @note 仅实现【面试必考】插入全套底层逻辑
* @note 删除仅讲原理,无底层代码
*/
class RBTree
{
private:
RBNode* root; // 整树根节点
/**
* @brief 获取节点颜色,兼容NIL空节点
* @param node 目标节点
* @return 节点颜色,空节点统一返回黑色
*/
Color getColor(RBNode* node)
{
return node == NIL ? BLACK : node->color;
}
/**
* @brief 修改节点颜色,过滤NIL空节点
* @param node 目标节点
* @param c 目标颜色
*/
void setColor(RBNode* node, Color c)
{
if (node != NIL)
node->color = c;
}
/**
* @brief 红黑树左旋操作
* @param x 旋转基准节点
* @note 仅调整树结构,不修改颜色、不破坏BST性质
*/
void leftRotate(RBNode* x)
{
// y为x的右孩子
RBNode* y = x->right;
// y的左子树挂载到x的右子树
x->right = y->left;
// 非空子树更新父节点
if (y->left != NIL)
y->left->parent = x;
// y继承x的父节点关系
y->parent = x->parent;
// 若x是根节点,y升级为新根
if (x->parent == nullptr)
root = y;
// 若x是父节点左孩子
else if (x == x->parent->left)
x->parent->left = y;
// 若x是父节点右孩子
else
x->parent->right = y;
// x成为y的左孩子
y->left = x;
x->parent = y;
}
/**
* @brief 红黑树右旋操作
* @param y 旋转基准节点
* @note 仅调整树结构,不修改颜色、不破坏BST性质
*/
void rightRotate(RBNode* y)
{
// x为y的左孩子
RBNode* x = y->left;
// x的右子树挂载到y的左子树
y->left = x->right;
// 非空子树更新父节点
if (x->right != NIL)
x->right->parent = y;
// x继承y的父节点关系
x->parent = y->parent;
// 若y是根节点,x升级为新根
if (y->parent == nullptr)
root = x;
else if (y == y->parent->left)
y->parent->left = x;
else
y->parent->right = x;
// y成为x的右孩子
x->right = y;
y->parent = x;
}
/**
* @brief 插入后红黑树平衡与颜色修复函数
* @param cur 新插入的节点
* @note 唯一违规场景:父节点为红色,出现连续红节点
* @note 修复手段:变色、左旋、右旋、向上回溯
*/
void insertFix(RBNode* cur)
{
// 父节点为红色才存在违规
while (getColor(cur->parent) == RED)
{
// 情况一:父节点是祖父节点的左孩子
if (cur->parent == cur->parent->parent->left)
{
RBNode* p = cur->parent; // 父节点
RBNode* g = p->parent; // 祖父节点
RBNode* u = g->right; // 叔叔节点
// 场景1:叔叔为红色 —— 纯变色修复
if (getColor(u) == RED)
{
setColor(p, BLACK); // 父变黑
setColor(u, BLACK); // 叔叔变黑
setColor(g, RED); // 祖父变红
cur = g; // 向上回溯继续修复
}
else
{
// 场景2:叔叔黑、cur是右孩子(LR交叉失衡)
// 先左旋转为LL直线失衡
if (cur == p->right)
{
cur = p;
leftRotate(cur);
}
// 场景3:叔叔黑、cur是左孩子(LL直线失衡)
setColor(p, BLACK);
setColor(g, RED);
rightRotate(g);
}
}
// 情况二:父节点是祖父节点的右孩子(镜像对称)
else
{
RBNode* p = cur->parent;
RBNode* g = p->parent;
RBNode* u = g->left;
// 场景1镜像:叔叔红色
if (getColor(u) == RED)
{
setColor(p, BLACK);
setColor(u, BLACK);
setColor(g, RED);
cur = g;
}
else
{
// 场景2镜像:RL交叉失衡,先右旋转RR
if (cur == p->left)
{
cur = p;
rightRotate(cur);
}
// 场景3镜像:RR直线失衡
setColor(p, BLACK);
setColor(g, RED);
leftRotate(g);
}
}
}
// 强制根节点为黑色,满足性质2
setColor(root, BLACK);
}
/**
* @brief BST底层插入逻辑
* @param val 待插入数值
* @note 新节点默认红色插入,最小化平衡代价
*/
void insert(int val)
{
// 新建节点,默认红色,绑定NIL虚拟叶子
RBNode* newNode = new RBNode(val);
newNode->left = NIL;
newNode->right = NIL;
// 空树直接初始化根节点
if (root == nullptr)
{
root = newNode;
root->color = BLACK;
return;
}
// 非空树遍历寻找插入位置
RBNode* cur = root;
RBNode* pre = nullptr;
while (cur != NIL)
{
pre = cur;
if (val < cur->val)
cur = cur->left;
else
cur = cur->right;
}
// 挂载新节点,建立父子关系
newNode->parent = pre;
if (val < pre->val)
pre->left = newNode;
else
pre->right = newNode;
// 插入完成,启动平衡修复
insertFix(newNode);
}
/**
* @brief 中序遍历 BST升序输出
* @param node 遍历起始节点
*/
void inOrder(RBNode* node)
{
if (node == NIL) return;
inOrder(node->left);
cout << node->val << " ";
inOrder(node->right);
}
/**
* @brief 后序递归销毁整树,防止内存泄漏
* @param node 销毁起始节点
*/
void destroy(RBNode* node)
{
if (node == NIL) return;
destroy(node->left);
destroy(node->right);
delete node;
}
public:
// 构造函数:初始化空树
RBTree()
{
root = nullptr;
}
// 析构函数:释放所有节点内存
~RBTree()
{
destroy(root);
}
// 对外公开插入接口
void insertVal(int val)
{
insert(val);
}
// 对外公开遍历接口:打印有序序列
void printTree()
{
inOrder(root);
cout << endl;
}
};
五、红黑树插入机制全解析
红黑树插入是秋招、期末唯一要求手撕的核心代码,旋转、变色、回溯逻辑必须完整掌握。本节补齐全套完整插入源码,包含BST插入底层逻辑、insertFix平衡修复逻辑,逐行超详细注释,零省略、零跳转。
5.1 插入核心前置原理
新节点默认红色插入。核心原因:
若插入黑色节点,会直接破坏整树黑高统一性质(性质5),需要全局修复,代价极大;
若插入红色节点,仅可能破坏无连续红节点(性质4),仅需局部变色+旋转修复,是最优插入策略。
插入失衡四大核心角色:cur(当前失衡节点)、p(父节点)、g(祖父节点)、u(叔叔节点)。
5.2 插入失衡三大标准场景
统一前置条件:插入后父节点p为红色,触发连续红色违规,根据红黑树合法规则,祖父节点g一定为黑色。
场景一:叔叔节点为红色(纯变色修复,无旋转)
结构特征:父红、叔叔红、祖父黑
修复逻辑:父节点、叔叔节点染黑,祖父节点染红;当前节点回溯至祖父节点,向上迭代修复上层失衡。
特点:无需旋转,仅变色即可修复局部失衡。
场景二:叔叔黑色、交叉失衡(LR / RL 先旋转矫正结构)
LR交叉:父为祖父左孩子、cur为父右孩子、叔叔黑 → 左旋父节点,转为LL直线失衡
RL交叉:父为祖父右孩子、cur为父左孩子、叔叔黑 → 右旋父节点,转为RR直线失衡
场景三:叔叔黑色、直线失衡(LL / RR 旋转+变色终结修复)
LL直线:父左、cur左、叔叔黑 → 祖父染红、父染黑、右旋祖父,修复完成无回溯
RR直线:父右、cur右、叔叔黑 → 祖父染红、父染黑、左旋祖父,修复完成无回溯
5.3 完整插入全套底层源码(含BST插入 + 平衡修复)
以下为可直接编译的完整插入核心代码,已整合进红黑树类,注释全覆盖、逻辑无省略。
/**
* @brief 底层BST插入逻辑
* @param val 待插入节点数值
* @note 1. 新节点默认红色,左右孩子绑定全局NIL黑节点
* @note 2. 单独处理空树场景,根节点强制黑色
* @note 3. 遍历寻找合法插入位置,维护父子指针关系
* @note 4. 插入完成后调用平衡修复函数,恢复红黑树五大性质
*/
void insert(int val)
{
// 实例化新节点:默认红色,初始绑定NIL虚拟叶子节点
RBNode* newNode = new RBNode(val);
newNode->left = NIL;
newNode->right = NIL;
// 特殊情况:空树,新节点直接作为根节点,根必须为黑色
if (root == nullptr)
{
root = newNode;
root->color = BLACK;
return;
}
// 非空树:按照BST规则遍历寻找插入位置
RBNode* cur = root;
RBNode* pre = nullptr;
while (cur != NIL)
{
pre = cur; // 记录当前节点为父节点
if (val < cur->val) // 小于当前节点,往左子树走
cur = cur->left;
else // 大于等于当前节点,往右子树走
cur = cur->right;
}
// 挂载新节点,建立父子双向指针关系
newNode->parent = pre;
if (val < pre->val)
pre->left = newNode;
else
pre->right = newNode;
// 插入完成,启动红黑树颜色与结构平衡修复
insertFix(newNode);
}
/**
* @brief 插入后红黑树自平衡修复函数
* @param cur 新插入的失衡节点
* @note 仅解决【连续红节点】违规问题
* @note 修复手段:变色、左旋、右旋、向上回溯迭代
*/
void insertFix(RBNode* cur)
{
// 循环条件:父节点为红色,出现连续红节点,触发违规
while (getColor(cur->parent) == RED)
{
// 分支1:父节点是祖父节点的左孩子(左侧失衡体系)
if (cur->parent == cur->parent->parent->left)
{
RBNode* p = cur->parent; // 定义父节点
RBNode* g = p->parent; // 定义祖父节点
RBNode* u = g->right; // 定义叔叔节点
// 场景1:叔叔节点为红色 —— 纯变色修复
if (getColor(u) == RED)
{
setColor(p, BLACK); // 父节点变黑,消除连续红
setColor(u, BLACK); // 叔叔节点变黑
setColor(g, RED); // 祖父节点变红,向上传递失衡
cur = g; // 指针回溯至祖父,继续向上校验
}
else
{
// 场景2:叔叔黑、当前节点为右孩子(LR交叉失衡)
// 先左旋父节点,将交叉结构转为LL直线结构
if (cur == p->right)
{
cur = p;
leftRotate(cur);
}
// 场景3:叔叔黑、当前节点为左孩子(LL直线失衡)
// 变色+右旋,一次性终结修复
setColor(p, BLACK);
setColor(g, RED);
rightRotate(g);
}
}
// 分支2:父节点是祖父节点的右孩子(右侧镜像失衡体系)
else
{
RBNode* p = cur->parent;
RBNode* g = p->parent;
RBNode* u = g->left;
// 场景1镜像:叔叔红色,纯变色修复
if (getColor(u) == RED)
{
setColor(p, BLACK);
setColor(u, BLACK);
setColor(g, RED);
cur = g;
}
else
{
// 场景2镜像:RL交叉失衡,先右旋转为RR直线
if (cur == p->left)
{
cur = p;
rightRotate(cur);
}
// 场景3镜像:RR直线失衡,左旋+变色终结修复
setColor(p, BLACK);
setColor(g, RED);
leftRotate(g);
}
}
}
// 强制根节点为黑色,严格遵守红黑树性质2
setColor(root, BLACK);
}
5.4 插入代码核心逻辑总结
1、先按照标准BST规则完成节点插入,新节点默认红色;
2、判断是否出现连续红节点违规,若父节点为黑色则无需修复;
3、根据叔叔节点颜色、当前节点位置,区分三大场景;
4、纯变色场景向上回溯,旋转场景直接终结修复;
5、最后强制根节点变黑,保证五大性质全部成立。
秋招/面试核心结论(重点记忆):红黑树删除底层源码极其复杂,包含双重黑色消解、四套镜像场景、多层回溯修复,常规秋招面试、期末考试均不要求手写删除底层代码,仅需掌握核心思路即可。本文档不提供任何删除底层代码,完全贴合面试考情。
6.1 删除模块核心认知
红黑树插入仅破坏「无连续红节点」规则,修复简单;而删除操作会直接破坏黑高一致性(五大核心性质5),产生「双重黑色节点」,是红黑树最复杂的逻辑。
删除整体执行流程(仅理解思路,无需写代码):
-
按照标准BST规则找到待删除节点,区分叶子节点、单孩子节点、双孩子节点三种情况;
-
双孩子节点采用「后继节点替换法」,用右子树最小节点覆盖待删除节点值,再删除后继节点;
-
判断被删除节点颜色:删除红色节点无任何影响,无需修复;删除黑色节点会产生双重黑色,破坏整树黑高平衡;
-
通过兄弟节点、兄弟节点左右孩子的颜色组合,匹配4种修复场景,逐层向上消解双重黑色,最终恢复红黑树五大性质。
6.2 面试必背问答
场景一:叔叔节点为红色(纯变色修复,无需旋转)
结构特征:父节点红、叔叔节点红、祖父节点黑
修复逻辑:父、叔叔节点染黑,祖父节点染红;随后将当前节点回溯至祖父节点,向上继续校验平衡,直到根节点。
核心特点:无需旋转,仅变色即可修复局部失衡,是最简单的插入失衡场景。
场景二:叔叔节点黑色、交叉失衡(LR / RL,先旋转再修复)
以左侧LR为例:父节点是祖父左孩子、当前节点是父节点右孩子、叔叔节点黑色
修复逻辑:先对父节点左旋,将交叉结构转换为直线结构(LL场景),再进入场景三修复。
右侧RL镜像:先对父节点右旋,转换为RR直线结构。
场景三:叔叔节点黑色、直线失衡(LL / RR,旋转+变色修复)
以左侧LL为例:父节点是祖父左孩子、当前节点是父节点左孩子、叔叔节点黑色
修复逻辑:祖父节点染红,父节点染黑,对祖父节点右旋,一次性修复所有失衡,无需继续回溯。
右侧RR镜像:祖父染红、父节点染黑,对祖父节点左旋修复。
七、完整测试主函数
int main()
{
// 实例化红黑树对象
RBTree tree;
// 乱序测试数据(验证自动排序、自动变色、自动旋转平衡)
int arr[] = {10, 20, 30, 15, 25, 5};
int n = sizeof(arr) / sizeof(arr[0]);
// 批量插入节点
for (int i = 0; i < n; i++)
{
tree.insertVal(arr[i]);
}
// 中序遍历一定升序,验证BST特性与平衡正确性
cout << "红黑树插入完成 · 中序升序遍历结果:" << endl;
tree.printTree();
return 0;
}
六、工具函数、测试代码与重难点总结
8.1 插入核心考点必背
-
根黑、叶黑、节点红黑二色、无连续红、任意路径黑高相等
-
新节点默认红色插入,代价最小
8.2 插入修复三大场景答题模板
-
叔叔红:仅变色,向上回溯
-
叔叔黑、LR:先左旋转LL,再右旋+变色
-
叔叔黑、LL:直接右旋+变色
8.3 删除考点总结
-
删除核心难点:删除黑色节点产生双重黑色,破坏全局黑高一致性
-
修复逻辑:依靠兄弟节点、兄弟子节点颜色组合,4种场景逐层消解双重黑色
-
应试结论:秋招面试、基础期末考核,无需掌握删除底层代码,仅需理解原理
8.4 红黑树 vs AVL树
-
AVL树:严格平衡,查询效率高,增删旋转多、效率低
-
红黑树:弱平衡,允许局部不平衡,旋转少、增删效率高,工业级首选
更多推荐
所有评论(0)