C++ STL篇(十一) ------ 红黑树详解
上篇文章中我们从零开始,深入学习了 AVL树的底层逻辑,本篇将由浅入深,带你循序渐进地掌握红黑树,全程干货,坐稳发车~ ദ്ദി˶ー̀֊ー́ )✧
文章目录
- [C++ STL篇(十一) ------ 红黑树详解](#C++ STL篇(十一) —— 红黑树详解)
-
- 一、红黑树的概念
-
- [1.1 红黑树的四条铁律](#1.1 红黑树的四条铁律)
- [1.2 为什么最长路径不会超过最短路径的两倍?](#1.2 为什么最长路径不会超过最短路径的两倍?)
- [1.3 效率怎么样?](#1.3 效率怎么样?)
- 二、红黑树的节点结构
- 三、红黑树插入的总体流程
- 四、插入修正的三种情况(图文拆解)
-
- [4.1 情况1:叔叔为红色 ------ 变色 + 向上回溯](#4.1 情况1:叔叔为红色 —— 变色 + 向上回溯)
-
- [4.1.1 cur 是新增节点](#4.1.1 cur 是新增节点)
- [4.1.2 cur 不是新增节点](#4.1.2 cur 不是新增节点)
- [4.2 情况2:叔叔为黑/不存在且呈直线 ------ 单旋 + 变色](#4.2 情况2:叔叔为黑/不存在且呈直线 —— 单旋 + 变色)
-
- [4.2.1 cur 为新增节点 (此时uncle一定不存在)](#4.2.1 cur 为新增节点 (此时uncle一定不存在))
- [4.2.2 cur 不是新增节点(此时uncle一定存在且为黑色)](#4.2.2 cur 不是新增节点(此时uncle一定存在且为黑色))
- [4.3 情况3:叔叔为黑/不存在且呈折线 ------ 双旋 + 变色](#4.3 情况3:叔叔为黑/不存在且呈折线 —— 双旋 + 变色)
-
- [4.3.1 cur是新增节点](#4.3.1 cur是新增节点)
- [4.3.2 cur不是新增节点](#4.3.2 cur不是新增节点)
- 五、插入代码
- 六、旋转操作的实现
-
- [6.1 右单旋(RotateR)](#6.1 右单旋(RotateR))
- [6.2 左单旋(RotateL)](#6.2 左单旋(RotateL))
- 七、红黑树的查找
- 八、如何验证一棵树是不是红黑树
- 九、完整代码
- 十、红黑树的删除(简要说明)
- 结语:
一、红黑树的概念
首先,红黑树是一棵二叉搜索树 (BST),它拥有二叉搜索树的全部特性:左子树所有节点的值 < 根节点的值 < 右子树所有节点的值。
但它比普通二叉搜索树多出来的一样东西是:每个节点多了一个存储位,用来表示该节点的"颜色"------非红即黑。正是通过对红黑颜色的严格约束,红黑树才保证了树的形状大致平衡。
1.1 红黑树的四条铁律
红黑树必须同时满足以下 4 条规则,缺一不可:
- 节点颜色非红即黑:每个节点要么是红色,要么是黑色。
- 根节点必须是黑色。
- 红色节点的孩子必须是黑色 :也就是说,从根到叶子的任何一条简单路径上,不允许出现两个相邻的红色节点。
- 每条路径上的黑色节点数量相等:从任意一个节点出发,到其所有后代空节点(NULL / NIL)的简单路径上,包含的黑色节点数目必须相同。
这里看几个红黑树的图帮助理解:


补充:关于"叶子节点"
有些教材(比如《算法导论》)还会额外加一条规则:"每个叶子节点(NIL)都是黑色的"。这里的"叶子"并不是我们平时说的没有孩子的那个节点,而是指空节点 ,也就是 NULL 指针所在的位置,有时也叫"外部节点"。
引入这个 NIL 黑色叶子可以让"路径"的定义更加统一(所有路径都终结于一个黑色 NIL),方便理论分析。《算法导论》在后续讲解实现的细节中也忽略了NIL结点,所以我们知道一下这个概念即可。

这里我们还要注意一个点 ,你觉得下面这个红黑树中一共有几条路径?

很多人可能会把路径数当成叶子节点的个数,认为有4条路径,这样就大错特错了!
正确答案是9条路径,因为路径是从根节点走到空节点,并不是到叶子节点。

1.2 为什么最长路径不会超过最短路径的两倍?
这可能是很多人最好奇的问题:不用记录高度,只靠颜色规则,怎么就能保证树不"退化"呢?
我们做两个极端假设,推理一下:
- 规则 4 告诉我们:每条从根到 NULL 的路径上,黑色节点的数量是相等的。我们把这个数量记作
bh(black height,黑高)。 - 那么,一条路径上最短的情况是什么呢?全是黑色节点 ------因为规则不禁止黑色连续出现,而且这样节点数最少。此时最短路径长度就是
bh。 - 最长的情况呢?在保持黑节点数为
bh的前提下,我们尽量塞入红色节点。规则 3 规定红节点不能相邻,所以只能在一对黑节点之间"插空"放一个红节点,形成黑-红-黑-红 交替的样子。这样一来,路径上的红色节点最多也只能是bh个(每个黑节点后面跟一个红节点,或者相反)。因此最长路径的长度就是bh + bh = 2*bh。
比较一下:最短路径长度为 bh,最长路径长度最多为 2*bh。所以,最长路径永远不会超过最短路径的 2 倍。

注意,这只是理论上的极限情况。实际的红黑树很少长成全黑或完美红黑交替,大多数时候路径长度介于
bh和2*bh之间。但有了这个上界保证,树的高度就被限制在了 (O(log N)) 级别。
1.3 效率怎么样?
设节点总数为 (N),树的高度为 (h)。根据二叉树的节点数下限:

所以高度 (h) 最坏也是 (O(log N))。那么插入、删除、查找时顺着路径走到头,时间复杂度自然就是 (O(log N)) 了。
和 AVL 树相比,两者效率在同一量级。但红黑树对平衡的要求更"宽松"(AVL 要求高度差不超过 1),所以在插入、删除时红黑树需要的旋转次数往往更少,综合性能更优。
二、红黑树的节点结构
先看看节点是如何定义的:
cpp
enum Colour
{
RED,
BLACK
};
template<class K, class V>
struct RBTreeNode
{
pair<K, V> _kv; // 键值对
RBTreeNode* _left; // 左孩子
RBTreeNode* _right; // 右孩子
RBTreeNode* _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是局部性的,只涉及祖孙三代,调整起来相对容易。两害相权取其轻,所以新节点一律标红。
三、红黑树插入的总体流程
插入一个键值对 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.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
此时子树可能的情况:

图解:

d/e/f 为hb==1 的红黑树:
c之前是黑色节点,在a和b中插入引发c变色为红色- d/e/f为x/y/z/m中任意一种,组合为 4x4x4
a和b为红色节点,在a和b的四个孩子的任意位置插入,都会让a和b变成黑色,c变成红色,继续往上更新,插入位置有4个位置。- 所有情况组合起来合计:4x4x4x4 = 256
场景三 : hb == 2
此时子树可能的情况:


图解(这里a/b/c/d/e/f 内部可能是各种组合,我们只展示了其中一种):

d/e/f为hb == 2的红黑树,a和b是 hb == 1的根为红色的树:
- d/e/f的组合为:(256+16)x(256+16)x(256+16) = 20123648
- a和b为根节点为红色节点的hb==1的树,这里可以看到a和b插入组合也不少
- a或者b插入至少要经历两次变色和向上处理才能得到这里的情况,这里的组合情况至少是百亿以上了。
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)染成 红色。完成插入修正。
五、插入代码
有了上面的理论铺垫,我们再来读代码,就会觉得每一行都落在预料之中。下面是 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;
}
六、旋转操作的实现
红黑树的旋转和 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;
}
}
七、红黑树的查找
查找操作完全就是标准二叉搜索树的查找,不用关心颜色。时间复杂度 (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;
}
八、如何验证一棵树是不是红黑树
写完代码后,我们必须有手段来检查树是否满足所有红黑树性质。有些人可能会想:先算一下最长路径和最短路径,看看最长是不是小于等于最短的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);
}
这里有个细节:我们取参考黑高时走的是最左路径,但实际上取任意一条路径都可以,因为只要树满足规则4,所有路径的黑高都相同。走最左路径只是图个方便。
九、完整代码
将上面的所有代码整合,加上一些辅助函数(中序遍历 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* _left;
RBTreeNode* _right;
RBTreeNode* _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;
};
十、红黑树的删除(简要说明)
红黑树的删除比插入更复杂。基本流程是:
- 按二叉搜索树规则删除节点。
- 如果删除的是红色节点,不破坏任何规则,直接结束。
- 如果删除的是黑色节点,会导致某些路径的黑高减少,需要进行复杂的重新平衡操作(变色、旋转),甚至可能需要从兄弟节点那里"借"一个黑色节点过来。
删除的情况分析多达六、七种,这里不展开。有兴趣深入的同学可以阅读《算法导论》或《STL源码剖析》中的相关章节。对于大多数应用场景,理解插入修正已经能帮你掌握红黑树的核心思想了。
结语:
今天的内容到这里就结束了,希望你能有所收获~
干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _