
🌈 say-fall:个人主页 🚀 专栏:《手把手教你学会C++》 | 《系统深入Linux操作系统》 | 《数据结构与算法》 | 《小游戏与项目》 💪 格言:做好你自己,才能吸引更多人,与他们共赢,这才是最好的成长方式。
前言
在前面我们学习了二叉搜索树(BST),它能在 O(logN) 的平均时间复杂度下完成查找、插入和删除操作。但二叉搜索树有一个致命的缺陷------它可能退化成链表!
试想一下,如果插入的数据是 1, 2, 3, 4, 5, 6... 这样的有序序列,二叉搜索树就会变成一条"长蛇",每次操作都变成了 O(N) 的时间复杂度。
为了解决这个问题,计算机科学家们设计了两棵"自平衡"的二叉搜索树:
| 树 | 高度平衡条件 | 查找复杂度 | 调整代价 |
|---|---|---|---|
| AVL树 | 任意节点左右子树高度差 ≤ 1 | O(logN) | 高(可能多次旋转) |
| 红黑树 | 用颜色约束近似平衡 | O(logN) | 低(最多2次旋转) |
红黑树是 STL 中 map、set、multimap、multiset 的底层结构,也是 Linux 内核中进程调度、内存管理等核心模块的数据结构。
文章目录
-
- 前言
- 一、红黑树的5大性质
- 二、红黑树节点结构
- 三、旋转操作------左旋与右旋
-
- [3.1 右旋(RotateR)](#3.1 右旋(RotateR))
- [3.2 左旋(RotateL)](#3.2 左旋(RotateL))
- 四、插入操作------4种情况详解
-
- 插入的2个阶段
- [4.1 调整的4种情况](#4.1 调整的4种情况)
-
- [🔹 情况一:父叔均为红(最简单)](#🔹 情况一:父叔均为红(最简单))
- [🔹 情况二:cur为红,uncle不存在或为黑 + cur是p的左孩子 → 单右旋](#🔹 情况二:cur为红,uncle不存在或为黑 + cur是p的左孩子 → 单右旋)
- [🔹 情况三:cur为红,uncle不存在或为黑 + cur是p的右孩子 → 先左旋后右旋](#🔹 情况三:cur为红,uncle不存在或为黑 + cur是p的右孩子 → 先左旋后右旋)
- [🔹 情况四:父在祖父右侧(镜像对称)](#🔹 情况四:父在祖父右侧(镜像对称))
- [4.2 插入完整代码](#4.2 插入完整代码)
- 五、平衡性检验
- 六、完整测试代码
- [七、红黑树 vs AVL树](#七、红黑树 vs AVL树)
- 八、红黑树在STL中的应用
- 本节完
一、红黑树的5大性质
红黑树通过给节点标记"颜色"(红色或黑色)来约束树的高度,保证它始终近似平衡:
性质1: 每个节点要么是 红色,要么是 🟫黑色
性质2: 根节点必须是 🟫黑色
性质3: 每个 红色 节点的父节点必须是 🟫黑色(即不能有两个连续的红色节点)
性质4: 所有路径上的 🟫黑色节点数量必须相等****
性质5: 每个 空节点(NIL) 默认视为 🟫黑色
cpp
enum Colour
{
Red,
Black
};
▲ 一棵合法的红黑树:每条路径上黑色节点数相同,无连续红节点
二、红黑树节点结构
红黑树的节点比普通二叉搜索树多了一个颜色字段:
cpp
template<class K, class V>
struct RBTreeNode
{
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)
{
// 新节点默认为红色(如果设为黑色会影响黑色高度平衡)
}
};
为什么新插入的节点默认为红色?
- 如果插入 红色 节点:只会可能违反"不能有连续红节点"这一条,调整代价较小
- 如果插入 🟫黑色节点:必定会违反"路径黑色高度相等"这一条,影响更大****
所以插入 红色 节点,调整代价更小!
三、旋转操作------左旋与右旋
红黑树的平衡调整依赖两个基本操作------左旋和右旋。
3.1 右旋(RotateR)
将"左重"的树调整为平衡状态
cpp
void RotateR(Node* parent)
{
// g subL
// subL → p g
// p subLR
Node* cur = parent->_left; // cur = p
Node* subLR = cur->_right; // subLR = p的右孩子
Node* Pparent = parent->_parent; // 记下g的父节点
// 1. 建立 p → g 的链接
cur->_right = parent;
parent->_parent = cur;
// 2. 将 subLR 挂到 g 的左边
parent->_left = subLR;
if (subLR) subLR->_parent = parent;
// 3. 处理 g 与其父节点的关系
if (parent == _root)
{
_root = cur;
cur->_parent = nullptr;
}
else
{
cur->_parent = Pparent;
if (Pparent->_left == parent)
Pparent->_left = cur;
else
Pparent->_right = cur;
}
}
3.2 左旋(RotateL)
将"右重"的树调整为平衡状态(与右旋对称)
cpp
void RotateL(Node* parent)
{
// g subR
// subR → g p
// p subRL
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* Pparent = parent->_parent;
subR->_left = parent;
parent->_parent = subR;
parent->_right = subRL;
if (subRL) subRL->_parent = parent;
if (parent == _root)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
subR->_parent = Pparent;
if (Pparent->_left == parent)
Pparent->_left = subR;
else
Pparent->_right = subR;
}
}
💡 Tip: 旋转操作只是改变指针链接,不破坏二叉搜索树的性质(中序遍历有序)
四、插入操作------4种情况详解
红黑树的插入是整个实现中最复杂的部分。我们需要 边插入边调整,保证每一步都满足5条性质。
插入的2个阶段
阶段1: 按照BST规则找到插入位置(与普通BST完全一样)
阶段2: 将新节点着色为 红色,然后从下往上检查和调整
4.1 调整的4种情况
设 cur = 新插入节点,parent = 父节点,grandfather = 祖父节点,uncle = 叔叔节点
🔹 情况一:父叔均为红(最简单)
调整前: 调整后:
g(黑) g(红)
p(红) u(红) → p(黑) u(黑)
c(红) c(红) ← 继续向上
策略: 父、叔变黑,祖父变红,cur 指向祖父继续向上检查
🔹 情况二:cur为红,uncle不存在或为黑 + cur是p的左孩子 → 单右旋
调整前: 调整后:
g(黑) p(黑)
p(红) u(黑/空) → c(红) g(红)
c(红) u(黑/空)
策略: 对祖父右旋,然后父变黑、祖父变红,调整完成(break)
🔹 情况三:cur为红,uncle不存在或为黑 + cur是p的右孩子 → 先左旋后右旋
调整前: 先左旋(p): 再右旋(g):
g(黑) g(黑) c(黑)
p(红) u(黑/空) → c(红) u(黑/空) → p(红) g(红)
c(红) p(红) u(黑/空)
策略: 先对父左旋(变成情况二),再对祖父右旋,cur变黑、祖父变红
🔹 情况四:父在祖父右侧(镜像对称)
镜像情况一:父叔均为红 → 父叔变黑,祖父变红,cur上移
镜像情况二:cur是p的右孩子 → 对祖父左旋,父变黑、祖父变红
镜像情况三:cur是p的左孩子 → 先对父右旋,再对祖父左旋
4.2 插入完整代码
cpp
bool Insert(pair<K,V> kv)
{
// ========== 阶段1:按BST规则找到插入位置 ==========
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; // 键已存在,插入失败
}
}
// ========== 阶段2:插入节点并调整 ==========
cur = new Node(kv);
cur->_col = Red; // 新节点默认为红色
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 (parent == grandfather->_left)
{
Node* uncle = grandfather->_right;
// ✅ 情况一:父叔均为红 → 变色 + 上移
if (uncle && uncle->_col == Red)
{
parent->_col = Black;
grandfather->_col = Red;
uncle->_col = Black;
cur = grandfather;
parent = cur->_parent;
}
// ❌ 情况二/三:uncle不存在或为黑
else
{
// 情况二:cur是左孩子 → 单右旋
if (cur == parent->_left)
{
RotateR(grandfather);
parent->_col = Black;
grandfather->_col = Red;
}
// 情况三:cur是右孩子 → 先左旋再右旋
else
{
RotateL(parent);
RotateR(grandfather);
cur->_col = Black;
grandfather->_col = Red;
}
break; // 旋转后cur变黑,调整完成
}
}
// ========== 父在祖父右侧(镜像处理)==========
else
{
Node* uncle = grandfather->_left;
if (uncle && uncle->_col == Red) // 镜像情况一
{
parent->_col = Black;
grandfather->_col = Red;
uncle->_col = Black;
cur = grandfather;
parent = cur->_parent;
}
else
{
if (cur == parent->_right) // 镜像情况二
{
RotateL(grandfather);
parent->_col = Black;
grandfather->_col = Red;
}
else // 镜像情况三
{
RotateR(parent);
RotateL(grandfather);
cur->_col = Black;
grandfather->_col = Red;
}
break;
}
}
}
_root->_col = Black; // 预防根节点变红
return true;
}
五、平衡性检验
红黑树的平衡性检验分三步:
cpp
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);
}
bool Check(Node* root, int blackNum, const int refNum)
{
if (root == nullptr)
{
// 到达叶子(空节点),检查黑色节点数是否匹配
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);
}
六、完整测试代码
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include"RBTree.h"
void TestRBTree1()
{
RBTree<int, int> t;
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; // 1 = 平衡,0 = 不平衡
}
int main()
{
TestRBTree1();
return 0;
}
七、红黑树 vs AVL树
| 对比维度 | 红黑树 | AVL树 |
|---|---|---|
| 平衡标准 | 近似平衡(最长 ≤ 最短2倍) | 严格平衡(左右高度差 ≤ 1) |
| 插入调整 | 最多2次旋转 | 可能多次旋转 |
| 查找效率 | O(logN)(实际略低) | O(logN)(更接近logN) |
| 适用场景 | map/set(查询/插入混合) | 数据库索引(查询多) |
| 调整代价 | 低 | 高 |
总结: 红黑树牺牲了部分平衡性,换取了更低的调整代价,因此在实际工程中被广泛采用!
八、红黑树在STL中的应用
红黑树是 C++ STL 中关联式容器的底层结构:
| STL容器 | 底层结构 | 是否允许键重复 |
|---|---|---|
| map | 红黑树 | 否 |
| set | 红黑树 | 否 |
| multimap | 红黑树 | 是 |
| multiset | 红黑树 | 是 |
正是因为红黑树的插入和删除调整代价低,所以 STL 选择了它作为底层结构!
本节完
作者:say-fall | 编辑:say-fall | 原创不易,如果对你有帮助,记得 点赞 + 收藏 哦!