红黑树的核心是自平衡二叉查找树:给每个节点标记红 / 黑颜色,通过 5 条颜色规则约束路径长度,再用 "左旋 / 右旋 + 变色" 修复平衡,保证任意节点到叶子的最长路径≤最短路径的 2 倍,操作效率稳定在 O (logn)。
下面用 C/C++ 代码实现核心逻辑,结合 "书架" 比喻拆解每一步,同时标注新手易踩的坑。
一、先定义红黑树的 "基础构件"(节点结构)
把红黑树节点想象成 "书架的层板",每个层板包含:
- 键值(层板上的书编号);
- 颜色(红 / 黑配重);
- 左 / 右孩子(下层书架);
- 父节点(上层书架);
- 哨兵节点(NIL):统一表示 "空层板",避免频繁判断
NULL(红黑树实现的经典技巧)。
cpp
#include <iostream>
using namespace std;
// 颜色枚举(C++11及以上支持,C语言可改用#define)
enum Color { RED, BLACK };
// 红黑树节点结构体
struct RBNode {
int key; // 节点键值(书的编号)
Color color; // 节点颜色(配重)
RBNode* left; // 左孩子(左下层书架)
RBNode* right; // 右孩子(右下层书架)
RBNode* parent; // 父节点(上层书架)
// 构造函数(C++),C语言可改用初始化函数
RBNode(int k = 0) : key(k), color(RED), left(nullptr), right(nullptr), parent(nullptr) {}
};
// 红黑树结构(包含根节点和哨兵节点)
struct RBTree {
RBNode* root; // 根节点(书架主干)
RBNode* nil; // 哨兵节点(空层板)
RBTree() {
nil = new RBNode(); // 哨兵节点默认黑色(空层板都是黑色配重)
nil->color = BLACK;
root = nil; // 初始时树为空,根指向哨兵
}
};
新手误区 1:不用哨兵节点,直接用 NULL
❌ 错误做法:插入 / 删除时频繁判断node->left == NULL,容易漏判且代码冗余;✅ 正确做法:用全局哨兵nil代替所有空节点,所有空指针都指向nil,简化逻辑。
二、核心操作 1:旋转(调整书架 "重心")
旋转是红黑树修复平衡的 "掰书架" 动作,分左旋 (右重左掰)和右旋 (左重右掰),核心是:掰完后仍保持二叉查找树 "左小右大" 的规则。
1. 左旋(解决 "右子树过重")
比喻:书架右侧层板太多,把右侧的 "主层板" 往左掰,成为新的上层。
cpp
// 左旋:以x为轴,把右子树往左掰(tree是树的指针,x是旋转轴节点)
void leftRotate(RBTree* tree, RBNode* x) {
RBNode* y = x->right; // y是x的右孩子(要掰上来的层板)
// 第一步:把y的左子树(y的左下层书架)挂到x的右位置(保证BST规则)
x->right = y->left;
if (y->left != tree->nil) { // 非空才更新父节点(哨兵节点无需父节点)
y->left->parent = x;
}
// 第二步:把x的父节点转给y(y上位)
y->parent = x->parent;
if (x->parent == tree->nil) { // x是根节点,旋转后y成新根
tree->root = y;
} else if (x == x->parent->left) { // x是父节点的左孩子
x->parent->left = y;
} else { // x是父节点的右孩子
x->parent->right = y;
}
// 第三步:把x挂到y的左位置(y成为x的新父节点)
y->left = x;
x->parent = y;
}
2. 右旋(解决 "左子树过重")
比喻:书架左侧层板太多,把左侧的 "主层板" 往右掰,成为新的上层。
cpp
// 右旋:以y为轴,把左子树往右掰
void rightRotate(RBTree* tree, RBNode* y) {
RBNode* x = y->left; // x是y的左孩子(要掰上来的层板)
// 第一步:把x的右子树挂到y的左位置
y->left = x->right;
if (x->right != tree->nil) {
x->right->parent = y;
}
// 第二步:把y的父节点转给x
x->parent = y->parent;
if (y->parent == tree->nil) {
tree->root = x;
} else if (y == y->parent->right) {
y->parent->right = x;
} else {
y->parent->left = x;
}
// 第三步:把y挂到x的右位置
x->right = y;
y->parent = x;
}
新手误区 2:旋转后漏更父节点
❌ 常见错误:只更新孩子节点,忘记更新parent指针(比如y->parent = x->parent);✅ 关键:旋转的核心是 "节点位置互换",必须同步维护parent、left、right三个指针,否则树结构会断裂。
三、核心操作 2:插入(新增层板 + 修复平衡)
红黑树插入分两步:
- 像普通二叉查找树(BST)一样插入节点(找位置、挂节点);
- 把新节点设为红色(轻配重,尽量不破坏平衡),然后调用
insertFix修复颜色规则。
1. 插入核心函数
cpp
// 插入节点(对外接口)
void insert(RBTree* tree, int key) {
RBNode* z = new RBNode(key); // 新节点默认红色(规则5)
z->left = tree->nil;
z->right = tree->nil;
RBNode* y = tree->nil; // 记录父节点(遍历用)
RBNode* x = tree->root; // 从根开始找插入位置
// 第一步:像BST一样找插入位置(左小右大)
while (x != tree->nil) {
y = x;
if (z->key < x->key) {
x = x->left;
} else if (z->key > x->key) {
x = x->right;
} else {
delete z; // 键值已存在,直接返回(避免重复)
return;
}
}
z->parent = y; // 挂到父节点上
if (y == tree->nil) {
tree->root = z; // 树为空,新节点成根
} else if (z->key < y->key) {
y->left = z;
} else {
y->right = z;
}
// 第二步:修复红黑树规则(新节点可能违反"红节点不能相邻")
insertFix(tree, z);
}
2. 插入后修复平衡(核心中的核心)
插入后可能违反 "红色节点的子节点必须是黑色"(规则 3),需分 3 种情况修复(本质是 "变色 + 旋转"):
cpp
// 插入后修复红黑树规则
void insertFix(RBTree* tree, RBNode* z) {
// 只要父节点是红色,就需要修复(红节点相邻,违反规则3)
while (z->parent->color == RED) {
if (z->parent == z->parent->parent->left) { // 父节点是祖父的左孩子
RBNode* uncle = z->parent->parent->right; // 叔父节点(祖父的右孩子)
// 情况1:叔父是红色 → 只需变色(父、叔父变黑,祖父变红)
if (uncle->color == RED) {
z->parent->color = BLACK;
uncle->color = BLACK;
z->parent->parent->color = RED;
z = z->parent->parent; // 祖父成为新的检查节点
} else {
// 情况2:叔父是黑色,且z是父节点的右孩子 → 先左旋转成情况3
if (z == z->parent->right) {
z = z->parent;
leftRotate(tree, z);
}
// 情况3:叔父是黑色,且z是父节点的左孩子 → 变色+右旋
z->parent->color = BLACK;
z->parent->parent->color = RED;
rightRotate(tree, z->parent->parent);
}
} else { // 父节点是祖父的右孩子(对称逻辑,镜像处理)
RBNode* uncle = z->parent->parent->left;
if (uncle->color == RED) {
z->parent->color = BLACK;
uncle->color = BLACK;
z->parent->parent->color = RED;
z = z->parent->parent;
} else {
if (z == z->parent->left) {
z = z->parent;
rightRotate(tree, z);
}
z->parent->color = BLACK;
z->parent->parent->color = RED;
leftRotate(tree, z->parent->parent);
}
}
}
tree->root->color = BLACK; // 根节点强制设为黑色(规则1)
}
新手误区 3:修复逻辑只记 "代码" 不记 "规则"
❌ 错误做法:死记 3 种修复情况的代码;✅ 核心逻辑:修复的本质是 "消除红节点相邻",优先通过 "变色" 解决(情况 1),变色解决不了再通过 "旋转 + 变色" 调整(情况 2/3),所有操作都围绕 5 条规则展开。
四、辅助函数:中序遍历(验证 BST 规则)
红黑树本质还是 BST,中序遍历应输出 "升序序列",可用于验证代码正确性:
cpp
// 中序遍历(左→根→右,输出升序键值)
void inorder(RBTree* tree, RBNode* node) {
if (node != tree->nil) {
inorder(tree, node->left);
cout << "键值:" << node->key << ",颜色:" << (node->color == RED ? "红" : "黑") << endl;
inorder(tree, node->right);
}
}
// 对外遍历接口
void inorderTraverse(RBTree* tree) {
inorder(tree, tree->root);
}
五、测试代码(完整可运行)
cpp
int main() {
RBTree tree;
// 插入一组数据,模拟"有序插入"(普通BST会歪成链表,红黑树会自平衡)
int keys[] = {10, 20, 30, 15, 25, 5};
for (int key : keys) {
insert(&tree, key);
}
// 中序遍历验证(应输出5,10,15,20,25,30,且红节点不相邻)
cout << "红黑树中序遍历(键值+颜色):" << endl;
inorderTraverse(&tree);
return 0;
}
输出示例(颜色会因平衡调整略有不同,但键值必升序):
bash
红黑树中序遍历(键值+颜色):
键值:5,颜色:红
键值:10,颜色:黑
键值:15,颜色:黑
键值:20,颜色:红
键值:25,颜色:黑
键值:30,颜色:红
六、红黑树的本质(C/C++ 实现视角)
- 颜色是 "平衡约束的载体":5 条颜色规则(根黑、红节点子黑、路径黑节点数相同等)本质是 "量化路径长度上限",避免 BST 退化成链表;
- 旋转是 "结构调整工具":左旋 / 右旋不破坏 BST 的 "左小右大" 规则,只调整节点位置,配合变色修复颜色规则;
- 工程价值 :C/C++ 标准库中
std::map/std::set底层就是红黑树,相比 AVL 树(严格平衡),红黑树插入 / 删除时旋转次数更少,效率更高。
新手终极误区:追求 "绝对平衡"
❌ 错!红黑树是 "近似平衡"(最长路径≤最短路径 2 倍),AVL 树才是 "严格平衡"(左右子树高度差≤1);✅ 记住:红黑树牺牲 "绝对对称",换来了更少的旋转操作,这也是它成为工程首选的原因。
总结
红黑树的 C/C++ 实现核心是:
- 用
nil哨兵节点简化空指针处理; - 旋转维护结构、变色维护规则;
- 插入 / 删除后通过
fix函数修复平衡。理解它的关键不是死记代码,而是把 "颜色规则" 和 "旋转逻辑" 对应到 "平衡书架" 的比喻上 ------ 所有操作都是为了让书架 "不歪得太离谱",保证找书效率稳定。