1. 红⿊树的概念
红⿊树是⼀棵⼆叉搜索树,他的每个结点增加⼀个存储位来表⽰结点的颜⾊,可以是红⾊或者⿊⾊。
通过对任何⼀条从根到叶⼦的路径上各个结点的颜⾊进⾏约束,红⿊树确保没有⼀条路径会⽐其他路径⻓出2倍,因⽽是接近平衡的。
1.1 红⿊树的规则:
- 每个结点不是红⾊就是⿊⾊
- 根结点是⿊⾊的
- 如果⼀个结点是红⾊的,则它的两个孩⼦结点必须是⿊⾊的,也就是说任意⼀条路径不会有连续的红⾊结点。
- 对于任意⼀个结点,从该结点到其所有NULL结点的简单路径上,均包含相同数量的⿊⾊结点
- 说明:《算法导论》等书籍上补充了⼀条每个叶⼦结点(NIL)都是⿊⾊的规则。他这⾥所指的叶⼦结点
不是传统的意义上的叶⼦结点,⽽是我们说的空结点,有些书籍上也把NIL叫做外部结点。NIL是为了⽅便准确的标识出所有路径,《算法导论》在后续讲解实现的细节中也忽略了NIL结点,所以我们知道⼀下这个概念即可。

1.2 思考⼀下,红⿊树如何确保最⻓路径不超过最短路径的2倍的?
• 由规则4可知,从根到NULL结点的每条路径都有相同数量的⿊⾊结点,所以极端场景下,最短路径就就是全是⿊⾊结点的路径,假设最短路径⻓度为bh(black height)。
• 由规则2和规则3可知,任意⼀条路径不会有连续的红⾊结点,所以极端场景下,最⻓的路径就是⼀⿊⼀红间隔组成,那么最⻓路径的⻓度为2bh。
• 综合红⿊树的4点规则⽽⾔,理论上的全⿊最短路径和⼀⿊⼀红的最⻓路径并不是在每棵红⿊树都存在的。假设任意⼀条从根到NULL结点路径的⻓度为x,那么bh<=h<=2bh。
1.3 红⿊树的效率:
假设N是红⿊树树中结点数量,h最短路径的⻓度,由此推出h ≈ logN ,也就是意味着红⿊树增删查改最坏也就是⾛最⻓路径 2 ∗ logN,那么时间复杂度还是O(logN)
红⿊树的表达相对AVL树要抽象⼀些,AVL树通过⾼度差直观的控制了平衡。红⿊树通过4条规则的颜⾊约束,间接的实现了近似平衡,他们效率都是同⼀档次,但是相对⽽⾔,插⼊相同数量的结点,红⿊树的旋转次数是更少的,因为他对平衡的控制没那么严格。
2. 红⿊树的实现
2.1 红⿊树的结构
节点定义
cpp
enum Colour
{
RED,
BLACK
};
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)
,_col(RED) // 新节点默认红色,原因马上揭晓
{ }
};
这里有几个关键点:
①带 _parent 指针的三叉链结构:比起普通二叉搜索树只存左右孩子,这里多了一个指向父节点的指针。这是为了方便在插入后向上回溯检查规则,以及在进行旋转操作时能快速调整父节点的指向,不用单独维护栈来记录路径。
②颜色枚举 Colour:只有 RED 和 BLACK 两种,规则1在代码层面直接由类型保证。
③新节点的默认颜色是 RED:这是红黑树插入策略中非常重要的一环。为什么不是黑色?
如果新插入一个黑色节点,它会立刻增加这条路径上的黑色节点数量,导致它所在路径的黑高比其他路径多 1,直接违反规则4。规则4是全局性的,破坏后需要大范围调整,非常难修复。
如果新插入一个红色节点,最坏情况是它的父节点也是红色,此时违反规则3(连续红色)。规则3是局部性的,只涉及祖孙三代,调整起来相对容易。两害相权取其轻,所以新节点一律标红
3红黑树插入的总体流程
插入一个键值对 kv 的过程分为两步:
按二叉搜索树的规则找到位置,插入新节点。
如果新节点的父节点是黑色,则插入结束,无需调整。
如果新节点的父节点是红色,就触发了"红红冲突",需要根据叔叔节点(父节点的兄弟)的情况进行分类修正,直到整棵树重新满足红黑树规则。
在接下来的讨论中,我们统一用以下代号称呼相关节点:
cur(或 c):当前关注的节点。一开始就是刚插入的红色新节点,调整过程中可能会向上移动。
parent(或 p):cur 的父节点。
grandfather(或 g):parent 的父节点,cur 的祖父。
uncle(或 u):parent 的兄弟节点,也就是 grandfather 的另一个孩子。
因为红黑树的规则对称,我们这里重点讨论 parent 是 grandfather 左孩子的情况,另一侧(parent 是右孩子)镜像对称即可。
当 cur 和 parent 都是红色时,grandfather 必定是黑色(不然插入前就不满足规则3了)。现在 cur 红、parent 红、grandfather 黑这三个颜色已经定死,问题的突破口就在 uncle 身上。根据 uncle 的颜色和是否存在,可以分为以下三种情况。
4插入修正的三种情况(图文拆解)
4.1 情况1:叔叔为红色 ------ 变色 + 向上回溯
条件: cur 为红,parent 为红,grandfather 为黑,并且 uncle 存在且为红色。
4.1.1 cur 是新增节点
情形示意(parent 是左孩子):

(c 是 p 的左右孩子均可,情况1的处理不关心方向)
分析:
• p 和 u 是红色,g 是黑色。
• 现在 c 和 p 连续红,破坏了规则3。
• 同时我们也不想破坏规则4(黑高不变)。
解决策略:变色
• 将 p 和 u 由红变黑。
• 将 g 由黑变红。

为什么这样可行?
从 g 往下看,左右两条路径原本各有 1 个黑色节点(g 自己是黑,子树内可能还有更多黑节点,但这里只看与 p/u 直接相关的部分)。现在 p 和 u 变黑,它们的路径上多了一个黑色节点;g 变红,相当于 g 把自己的那个黑色分给了两个儿子。这样一来,左右路径上的黑色节点总数保持不变,规则4依然满足。
同时,c 和 p 的红红冲突也解除了,因为 p 变成了黑色。
但是,g 被染成了红色,如果 g 的父节点(即 c 的曾祖父)恰好也是红色,那么就会在更高层引发新的红红冲突。这就是为什么需要将 cur 指针指向 g,把 g 当作新的 cur 继续向上调整。
如果 g 就是整棵树的根节点,最后我们会强制把它恢复为黑色,满足规则2,调整结束。
如果 g 的父节点是黑色,则直接结束,万事大吉。
4.1.2 cur 不是新增节点
上面我们讨论了cur是新增节点时的一种情况,,但是实际中需要这样处理的有很多种情况。为了能更直观的把握,我们使用抽象图来表示子树。抽象图虽然看起来简单,但它能覆盖成百上千种具体的节点形态------这正是红黑树调整的精妙之处:无论子树多复杂,只要结构关系符合,修正操作完全一致。图中长方形 a、b、c、d、e、f 表示符合红黑树规则的子树,并且它们都带有一些黑色节点数(黑高)。
d/e/f代表每条路径拥有hb个黑色结点的子树,
a/b代表每条路径拥有hb-1个黑色结点的根为红色的子树

具体场景分析:
场景一: hb == 0
如果 hb == 0 ,此时a/b/c/d/e/f 都为空,这时的情况就是我们上面提到过的cur为新增节点时的情况。
场景二: hb == 1
此时子树可能的情况:

场景三 : hb == 2
此时子树可能的情况:

hb == 2 的情况已经达到百亿以上了,hb>=3 的情况肯定超级的多,这里我们就不展开分析了。
小结一下:
情况1只进行了变色,没有旋转。无论 c 是 p 的左孩子还是右孩子,p 是 g 的左孩子还是右孩子,处理手段都完全一样:p 和 u 变黑,g 变红,然后向上回溯。
4.2 情况2:叔叔为黑/不存在且呈直线 ------ 单旋 + 变色
(这里我们重点分析右单旋的情况,左单旋同理)
条件: cur 为红,parent 为红,grandfather 为黑,且 uncle 不存在或者存在但为黑色。
同时,cur、parent、grandfather 三者在一条直线上:即 cur 是 parent 的左孩子且 parent 是 grandfather 的左孩子(左左),或 cur 是 parent 的右孩子且 parent 是 grandfather 的右孩子(右右)。
4.2.1 cur 为新增节点 (此时uncle一定不存在)

分析:
c 和 p 连续红,必须解决。
u 是不存在,单纯把 p 变黑并不能平衡,因为如果把 p 变黑,左路径黑高增加1,而右路径那边黑高不变,就违反了规则4。我们需要通过旋转来改变树的结构,同时配合变色来维持黑高。
操作:右单旋 + 变色
以 grandfather(g)为支点进行右单旋。
旋转后,parent(p)成为新子树的根。
将 p 的颜色改为黑色,g 的颜色改为红色。
新的根 p 是黑色,如果它上面还有父节点,无论父节点是红还是黑都不会再引发冲突(因为规则3只要求红节点孩子为黑,黑节点的孩子可以是任意颜色)。因此,调整到此结束,不需要再向上回溯,可以直接 break。
为什么u不存在的时候,c一定是新增节点?
如果cur不是新增,那它在最初只能是黑色的,经过调整之后才变红的(在情况1的调整过程中,由黑色变成红色然后向上回溯上来的"老"节点),这样就不满足每条路径上的黑色节点相同这个条件了。
4.2.2 cur 不是新增节点(此时uncle一定存在且为黑色)

因为uncle为黑色,此时我们就不能使用情况一的调整规则,需要旋转+变色来进行调整。
为什么u存在且为黑的时候,c一定不是新增节点?
插入一个红色节点,如果叔叔是黑色,那说明在插入前,p 所在路径的黑高比 u 所在路径少,这不符合规则4!
4.3 情况3:叔叔为黑/不存在且呈折线 ------ 双旋 + 变色
(这里我们重点分析先左旋后右旋的情况,先右旋后左旋同理)
条件: 同样满足 cur 红、parent 红、grandfather 黑,uncle 不存在或为黑,但这次 cur、parent、grandfather 三者不在一条直线上,而是呈折线状:cur 是 parent 的右孩子,而 parent 是 grandfather 的左孩子(左右),或 cur 是 parent 的左孩子,而 parent 是 grandfather 的右孩子(右左)。
4.3.1 cur是新增节点

这种情况直接对 g 做右单旋是没有用的,旋转后 c 依然会是某个节点的红色孩子,仍然违反规则。正确的做法是进行两次旋转,把折线"掰直"。
操作:双旋 + 变色
- 先以 parent(p)为支点进行左单旋。此时结构变为 c 在上,p 在左下的直线形态。
- 再以 grandfather(g)为支点进行右单旋。
- 最后,将旋转后处于子树根位置的 cur 染成黑色,将 grandfather(g)染成红色。
- 新根是黑色,不再需要向上回溯,调整直接结束。
4.3.2 cur不是新增节点

这里同样需要进行两次旋转,第一次以p为旋转点进行左单旋,将结构拉直,第二次以g为旋转点进行右单旋,两次旋转结束后将处于子树根位置的 cur染成 黑色,将 grandfather(g)染成 红色。完成插入修正。
5插入代码
有了上面的理论铺垫,我们再来读代码,就会觉得每一行都落在预料之中。下面是 Insert 函数的完整实现。
cpp
bool Insert(const pair<K, V>& kv)
{
// 1. 空树处理
if (_root == nullptr)
{
_root = new Node(kv);
_root->_col = BLACK; // 根必须是黑色
return true;
}
// 2. 按照二叉搜索树规则查找插入位置
Node* cur = _root;
Node* parent = nullptr;
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; // 不允许重复键值
}
}
// 3. 创建新节点(默认红色)并链接到父节点
cur = new Node(kv);
if (parent->_kv.first < kv.first)
parent->_right = cur;
else
parent->_left = cur;
cur->_parent = parent;
// 4. 如果父节点是黑色,插入结束;如果父节点是红色,进入修正循环
while (parent && parent->_col == RED)
{
Node* grandfather = parent->_parent; // 父红则祖父必然存在
if (grandfather->_left == parent) // 父是祖父的左孩子
{
Node* uncle = grandfather->_right;
// 情况1:叔叔存在且为红
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
// 把祖父当作新的cur,继续向上调整
cur = grandfather;
parent = cur->_parent;
}
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 // 父是祖父的右孩子(对称处理)
{
Node* uncle = grandfather->_left;
if (uncle && uncle->_col == RED) // 情况1
{
uncle->_col = parent->_col = BLACK;
grandfather->_col = RED;
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;
}
}
}
// 5. 强制根为黑色,防止情况1把根染红
_root->_col = BLACK;
return true;
}
6旋转操作的实现
红黑树的旋转和 AVL 树的旋转完全一样。因为我们维护了父指针,旋转时需要小心地更新三个节点的父子关系。
6.1 右单旋(RotateR)
旋转点 parent 是要被拉下来的节点(图中的 g)。右旋后,它的左孩子 subL 会成为新的根。
cpp
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 = parent;
// 2. 处理 parent:把它挂到 subL 的右子
subL->_right = parent;
parent->_parent = subL;
// 3. 处理 subL 与原父节点的关系
if (parent == _root)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (pParent->_left == parent)
pParent->_left = subL;
else
pParent->_right = subL;
subL->_parent = pParent;
}
}
6.2 左单旋(RotateL)
与右单旋完全对称。
cpp
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* pParent = parent->_parent;
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
subR->_left = parent;
parent->_parent = subR;
if (_root == parent)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (pParent->_left == parent)
pParent->_left = subR;
else
pParent->_right = subR;
subR->_parent = pParent;
}
}
7红黑树的查找
查找操作完全就是标准二叉搜索树的查找,不用关心颜色。时间复杂度 (O(log N))。
cpp
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_kv.first < key)
cur = cur->_right;
else if (cur->_kv.first > key)
cur = cur->_left;
else
return cur;
}
return nullptr;
}
8如何验证一棵树是不是红黑树
写完代码后,我们必须有手段来检查树是否满足所有红黑树性质。有些人可能会想:先算一下最长路径和最短路径,看看最长是不是小于等于最短的2倍不就行了?
这是不够的! 满足"最长路径 <= 2×最短路径"的树不一定满足红黑树的颜色规则,它可能只是"歪打正着"的平衡。如果不按照颜色规则去维护,后续继续插入时迟早会崩。
因此,我们应该正面检查四条规则:
规则1(颜色只有红或黑):代码中用枚举类型 Colour 赋值,天然保证。
规则2(根节点是黑色):直接检查 _root->_col == BLACK。
规则3(没有连续红色节点):遍历过程中,如果当前节点是红色,检查其父节点是否为红色(注意根节点没有父节点,但它是黑色,不会进此判断)。
规则4(每条路径黑色节点数相等):先计算出一条"最左路径"上的黑色节点数目作为参考值 refNum,然后递归遍历所有路径,对于每条路径,一边走一边统计黑节点数,走到空节点时与参考值比较是否相等。
下面是验证函数 IsBalanceTree 及其辅助递归函数 RBCheck 的实现
cpp
bool IsBalanceTree()
{
if (_root == nullptr)
return true;
// 检查规则2:根是否为黑
if (_root->_col == RED)
return false;
// 计算参考黑高:沿着最左路径走到底,统计黑色节点数
int refNum = 0;
Node* cur = _root;
while (cur)
{
if (cur->_col == BLACK)
refNum++;
cur = cur->_left;
}
// 递归检查规则3和规则4
return RBCheck(_root, 0, refNum);
}
bool RBCheck(Node* root, int blackNum, int refNum)
{
// 走到空节点:一条路径结束,检查黑高是否等于参考值
if (root == nullptr)
{
if (blackNum != refNum)
{
cout << "存在黑色节点数量不相等的路径!" << endl;
return false;
}
return true;
}
// 检查规则3:当前节点为红时,父节点不能为红
if (root->_col == RED && root->_parent->_col == RED)
{
cout << "存在连续的红色节点!" << endl;
return false;
}
// 累加黑色节点
if (root->_col == BLACK)
blackNum++;
// 递归检查左右子树
return RBCheck(root->_left, blackNum, refNum)
&& RBCheck(root->_right, blackNum, refNum);
}
9完整代码
将上面的所有代码整合,加上一些辅助函数(中序遍历 Inorder、求节点数 Size、求高度 Height),就可以跑起来了。
cpp
#pragma once
#include<iostream>
using namespace std;
enum Colour
{
RED,
BLACK
};
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)
,_col(RED)
{ }
};
template<class K, class V>
class RBTree
{
typedef RBTreeNode<K, V> Node;
public:
bool Insert(const pair<K,V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
_root->_col = BLACK;
return true;
}
Node* cur = _root;
Node* parent = nullptr;
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;
}
}
cur = new Node(kv);
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 (grandfather->_left == parent)
{
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
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
{
Node* uncle = grandfather->_left;
if (uncle && uncle->_col == RED)
{
uncle->_col = parent->_col = BLACK;
grandfather->_col = RED;
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;
}
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
Node* pParent = parent->_parent;
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
subL->_right = parent;
parent->_parent = subL;
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;
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
subR->_left = parent;
parent->_parent = subR;
if (_root == parent)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (pParent->_left == parent)
pParent->_left = subR;
else
pParent->_right = subR;
subR->_parent = pParent;
}
}
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_kv.first < key)
cur = cur->_right;
else if (cur->_kv.first > key)
cur = cur->_left;
else
return cur;
}
return nullptr;
}
int Size()
{
return _Size(_root);
}
int Height()
{
return _Height(_root);
}
void Inorder()
{
_Inorder(_root);
}
bool IsBalanceTree()
{
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 RBCheck(_root, 0, refNum);
}
private:
void _Inorder(Node* root)
{
if (root == nullptr)
return;
_Inorder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << endl;
_Inorder(root->_right);
}
int _Size(Node* root)
{
if (root == nullptr)
return 0;
return _Size(root->_left) + _Size(root->_right) + 1;
}
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;
}
bool RBCheck(Node* root, int blackNum, int refNum)
{
if (root == nullptr)
{
if (blackNum != refNum)
{
cout << "存在黑色节点数量不相等的路径!" << endl;
return false;
}
return true;
}
if (root->_col == RED && root->_parent->_col == RED)
{
cout << "存在连续的红色节点!" << endl;
return false;
}
if (root->_col == BLACK)
{
blackNum++;
}
return RBCheck(root->_left, blackNum, refNum)
&& RBCheck(root->_right, blackNum, refNum);
}
private:
Node* _root = nullptr;
};