目录
[(一) 前言](#(一) 前言)
[(二) 正文](#(二) 正文)
[(1) 红黑树的概念和性质](#(1) 红黑树的概念和性质)
[(2) 红黑树的模拟](#(2) 红黑树的模拟)
[2.1 红黑树的插入](#2.1 红黑树的插入)
[(3) 红黑树的验证](#(3) 红黑树的验证)
[(4) AVL树和红黑树的比较](#(4) AVL树和红黑树的比较)
(一) 前言
在上一篇文章中,我们学习了 AVL 树,本次将重点学习红黑树。在正式开始前,我们先通过对比两种树的核心特性,理解红黑树的应用价值。
AVL 树是严格平衡的二叉搜索树,要求任意节点的左右子树高度差不超过 1;而红黑树是接近平衡的二叉搜索树,仅要求 "最长路径的节点数不超过最短路径的 2 倍"。
从高度数据来看:
- 当数据量为 1000 时,AVL 树的高度约为 logN(≈10),红黑树的高度约为 2logN(≈20);
- 当数据量达到 10 亿时,AVL 树的高度约为 30,红黑树的高度约为 60。
显然红黑树的高度更高,但实际应用中红黑树反而更优,原因是什么呢?
对 CPU 而言,"30 与 60""10 与 20" 的计算开销差异极小,二者性能处于同一量级;但 AVL 树的 "严格平衡" 是有代价的 ------ 插入和删除操作中,为了维持高度差限制,需要进行大量的旋转调整。相比之下,红黑树的平衡要求更宽松,旋转操作更少,整体效率更高。因此,深入学习红黑树具有重要的实用意义。
(二) 正文
(1) 红黑树的概念和性质
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或 Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。
红黑树图像:

红黑树的性质(需注意路径要从根节点走到空节点)
- 每个结点不是红色就是黑色
- 根节点是黑色的
- 如果一个节点是红色的,则它的两个孩子结点必须是黑色的 (任何路径都不会存在连续的红色结点,但是可以存在连续的黑色节点)
- 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均 包含相同数目的黑色结点
- 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)(即所有的NIL叶子节点为黑色)
小思考:
为什么满足上面的性质,红黑树就能保证:其最长路径中节点个数不会超过最短路径节点 个数的两倍?(首先,最短路径的节点必然全为黑色)
- 设最短路径的黑色节点数为 n(即该路径的黑高为 n),根据性质 4,所有路径的黑高均为 n;
- 最长路径的结构必然是 "黑红交替" ------ 因为性质 3 禁止连续的红色节点,要让路径最长,需在黑色节点之间尽可能插入红色节点;
- 对于 "黑红交替" 的最长路径,黑色节点数为 n(与黑高一致),红色节点数最多也为 n(每两个黑色节点间插一个红色节点),因此最长路径的总节点数为 "黑色节点数 + 红色节点数"=n+n=2n;
- 最短路径的节点数为 n(全黑),最长路径节点数为 2n,因此 "最长路径节点数不超过最短路径的 2 倍"。
同时可推出:最长路径中黑色节点的占比≥1/2,进一步说明红黑树的结构始终接近平衡。
(2) 红黑树的模拟
依旧得先定义树和树的节点
// 枚举值表示颜色
enum Colour
{
RED,
BLACK
};
template<class K, class V>
struct RBTreeNode
{
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
pair<K, V> _kv;
Colour _col;
RBTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, parent(nullptr)
, _kv
, _col(BLACK)
{}
};
template<class K,class V>
class RBTree
{
typedef RBTreeNode<k, V> Node;
public:
RBTree()
:_root(nullptr)
{}
//内容
private:
Node* _root;
};
小知识点:enum的介绍
enum的基本使用方法:
enum 枚举名 { 常量1, 常量2, ... };
- 枚举名 是自定义的类型名(如你代码中的 Colour);
- 大括号内是枚举常量(如 RED、BLACK),它们本质是整数,默认从 0 开始依次递增(RED 默认为 0,BLACK 默认为 1);
也可以手动指定枚举常量的值
enum Colour { RED = 2, // 手动指定为2 BLACK = 5 // 手动指定为5 };简单说,枚举就是给一组整数 "起名字",让代码更易懂、更规范。
2.1 红黑树的插入
新插入的节点为什么颜色,为什么了?
**红色节点,**因为将新节点设为红色能最小化对红黑树性质的破坏。红色节点不会改变路径的黑高,而插入黑色节点会直接增加该路径的黑节点数量,违反"所有路径黑节点数相同"的性质,并且会影响整棵树的所有路径,需要更多调整操作。相比之下,插入红色节点只会影响当前路径,所需的调整操作更少。
红黑树是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树的插入可分为两步:
1. 按照二叉搜索的树规则插入新节点
2. 检测新节点插入后,红黑树的性质是否造到破坏
由于新节点默认设置为红色,会出现以下两种情况:
- 若其父节点为黑色,则不违反任何红黑树性质,无需调整;
- 若父节点为红色,则违反了"不允许相邻两个节点均为红色"的性质(性质三)。
面对二的情况需根据具体情况进行调整,具体如下:(先约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点)
情况一: cur为红,p为红,g为黑,u存在且为红
操作步骤:
- 将 p 和 u 改为黑色
- 将 g 改为红色
- 将 g 设为新的当前节点(cur),继续向上调整
注意:
- 若 g 是根节点,调整后需将其重新改为黑色;
- 若 g 不是根节点,则其必有父节点。若 g 的父节点为红色,则需继续向上调整。
具体图像:

抽象图片解释:

情况二: cur 为红, p 为红, g 为黑, u 不存在或为黑(且 cur 与 p 、 p 与 g 呈直线关系)
判断条件:
- 若 p 是 g 的左孩子,且 cur 是 p 的左孩子(直线型),则为左直线情况。
- 若 p 是 g 的右孩子,且 cur 是 p 的右孩子(直线型),则为右直线情况。
解决方法:
- 左直线情况: 执行右单旋转
- 右直线情况: 执行左单旋转
- 旋转后,将 p 设为黑色,g 设为红色
关于 u 的两种情况说明:
- 若 u 节点不存在,则 cur 必定是新插入节点。因为如果 cur 不是新插入节点,根据性质4(每条路径黑色节点数量相同),cur 或 p 中至少有一个应为黑色节点。
具体图:

- 若 u 节点存在,则其颜色必为黑色。此时 cur 节点原本应为黑色,其呈现红色的原因是在子树调整过程中将 cur 节点的颜色由黑色改为红色。
具体图;

两种情况的抽象图

情况三: cur 为红, p 为红, g 为黑, u 不存在或为黑(且 cur 与 p 、 p 与 g 呈折线关系)
处理方式:
- 若 p 是 g 的左子节点且 cur 是 p 的右子节点(折线型),则先对 p 执行左旋操作,将其转换为情况二的左直线形态;
- 若 p 是 g 的右子节点且 cur 是 p 的左子节点(折线型),则先对 p 执行右旋操作,将其转换为情况二的右直线形态。
具体图:

抽象图:

具体代码:
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 != nullptr)
{
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);
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;
//情况一: cur为红,p为红,g为黑,u存在且为红
if (uncle != nullptr && uncle->_col == RED)
{
//将p,u改为黑,g改为红,然后把g当成cur,继续向上调整。
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
//情况二: cur为红,p为红,g为黑,u不存在/u存在且为黑
else
{
if (cur == parent->_left)
{
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
else //parent == grandfather->_right
{
Node* uncle = grandfather->_left;
//情况一: cur为红,p为红,g为黑,u存在且为红
if (uncle != nullptr && uncle->_col == RED)
{
//将p,u改为黑,g改为红,然后把g当成cur,继续向上调整。
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
//情况二: cur为红,p为红,g为黑,u不存在/u存在且为黑
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;
}
(3) 红黑树的验证
红黑树的验证过程包含两个主要步骤:
- 检查是否符合二叉搜索树特性(通过中序遍历验证是否为有序序列)
- 验证是否满足红黑树的所有性质
我们的实现重点在于第二项的验证,主要包括以下三个方面:
- 根节点颜色必须为黑色
- 不能出现连续的红色节点
- 所有路径的黑色节点数量必须相同
具体实现详见以下代码:
bool CheckColour(Node* root, int blacknum, int benchmark)
{
if (root == nullptr)
{
if (blacknum != benchmark)
return false;
return true;
}
if (root->_col == BLACK)
++blacknum;
if (root->_col == RED && root->_parent && root->_parent->_col == RED)
{
cout << root->_kv.first << "出现连续红色节点" << endl;
return false;
}
return CheckColour(root->_left, blacknum, benchmark)
&& CheckColour(root->_right, blacknum, benchmark);
}
bool IsBalance()
{
return _IsBalance(_root);
}
bool _IsBalance(Node* root)
{
if (root == nullptr)
return true;
if (root->_col != BLACK)
{
cout << "根节点颜色不是黑色" << endl;
return false;
}
int benchmark = 0;
Node* cur = root;
while (cur != nullptr)
{
if (cur->_col == BLACK)
++benchmark;
cur = cur->_left;
}
return CheckColour(root, 0, benchmark);
}
测试用例代码为:
void test1()
{
//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
RBTree<int, int> t;
for (auto e : a)
{
t.Insert(make_pair(e, e));
cout << "Insert:" << e << "->" << t.IsBalance() << endl;
}
}
void test2()
{
const int N = 10000;
vector<int> v;
v.reserve(N);
srand(time(0));
for (size_t i = 0; i < N; i++)
{
v.push_back(i);
}
RBTree<int, int> rbt;
for (auto e : v)
{
rbt.Insert(make_pair(e, e));
//cout << "Insert:" << e << "->" << t.IsBalance() << endl;
}
cout << rbt.IsBalance() << endl;
}
运行结果:
test1的
Insert:16->1
Insert:3->1
Insert:7->1
Insert:11->1
Insert:9->1
Insert:26->1
Insert:18->1
Insert:14->1
Insert:15->1
Insert:4->1
Insert:2->1
Insert:6->1
Insert:1->1
Insert:3->1
Insert:5->1
Insert:15->1
Insert:7->1
Insert:16->1
Insert:14->1
test2的
1
完整代码------博主的gitee
(4) AVL树和红黑树的比较
红黑树和AVL树都是高效的平衡二叉搜索树,均能保证查找、插入、删除等操作的时间复杂度为O(logN)。两者的主要区别在于平衡策略:红黑树通过放宽平衡条件,仅要求最长路径不超过最短路径的两倍,从而减少了插入和删除时的旋转操作次数。这种设计使红黑树在频繁修改的场景中性能优于AVL树。此外,红黑树的实现相对简单,因此在工程实践中应用更为广泛。
测试方法:
我们可以再AVLTree和RBTRee的代码里面再加以一个测量高度和旋转次数的代码
测量高度的代码:
int Height()
{
return Height(_root);
}
int Height(Node* root)
{
if (root == nullptr)
return 0;
int leftHeight = Height(root->_left);
int rightHeight = Height(root->_right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
要实现旋转次数统计功能,可以在左旋和右旋操作中增加++_rotateCount计数语句,同时在类成员变量中声明一个公共的_rotateCount计数器。
最后测试案例变为
void test3()
{
const int N = 10000;
vector<int> v;
v.reserve(N);
srand(time(0));
for (size_t i = 0; i < N; i++)
{
v.push_back(i);
}
RBTree<int, int> rbt;
for (auto e : v)
{
rbt.Insert(make_pair(e, e));
//cout << "Insert:" << e << "->" << t.IsBalance() << endl;
}
cout << rbt.IsBalance() << endl;
cout << rbt.Height() << endl;
cout << rbt._rotateCount << endl;
AVLTree<int, int> avlt;
for (auto e : v)
{
avlt.Insert(make_pair(e, e));
//cout << "Insert:" << e << "->" << t.IsBalance() << endl;
}
cout << avlt.IsAVLTree() << endl;
cout << avlt.Height() << endl;
cout << avlt._rotateCount << endl;
return 0;
}
测试结果:
1
24
9976
1
14
9986
由结果我们可以发现:
1. 平衡性质:均保高效,规避退化
- AVL 树:严格保证 "节点左右子树高度差≤1"(绝对平衡);
- 红黑树:通过 "根黑、无连续红节点" 等规则,保证 "最长路径≤2× 最短路径"(近似平衡);
二者均将增删改查时间复杂度稳定在O(logN),避免普通 BST 退化为链表(O (N))。
2. 树高:AVL 更优,红黑够用
- AVL 树高 14:绝对平衡压缩树高,查询时比较次数少,理论查询性能更好;
- 红黑树高 24:虽高于 AVL,但仍属 O (logN) 级别,以略高树高换增删时更少的旋转开销。
3. 旋转次数:红黑开销更低
- AVL 树:需维持绝对平衡,高度失衡就旋转,易触发连锁旋转,开销高;
- 红黑树:仅 "连续红节点" 时调整,优先变色(不旋转),仅变色无效时旋转(最多 2 次 / 调整),频率更低,印证其增删性能更优。
总结:适用场景分化
- **AVL 树:**适合查询多、增删少的场景------ 绝对平衡最大化查询效率,增删开销可忽略;
- **红黑树:**适合增删频繁的场景------ 近似平衡保查询效率,低旋转开销 + 易实现,工程应用更广。
以上就是C++之红黑树的学习的学习。希望这些知识能为你带来帮助!如果觉得内容实用,欢迎点赞支持~ 若发现任何问题或有改进建议,也请随时与我交流。感谢你的阅读!