目录
- 一、红黑树的基本概念
-
- [1.1 红黑树是什么?](#1.1 红黑树是什么?)
- [1.2 红黑树的五大规则](#1.2 红黑树的五大规则)
- [1.3 红黑树的效率](#1.3 红黑树的效率)
- [1.4 红黑树的底层节点结构](#1.4 红黑树的底层节点结构)
- 二、红黑树的实现
-
- [2.1 红黑树的 insert 操作](#2.1 红黑树的 insert 操作)
-
- [2.1.1 情况一: 变色](#2.1.1 情况一: 变色)
- [2.1.2 情况二: 单旋 + 变色](#2.1.2 情况二: 单旋 + 变色)
- [2.1.3 情况三: 双旋 + 变色](#2.1.3 情况三: 双旋 + 变色)
- [2.2 中序遍历](#2.2 中序遍历)
- [2.3 其它接口的实现](#2.3 其它接口的实现)
-
- [2.3.1 红黑树的验证](#2.3.1 红黑树的验证)
- [2.3.2 Size 、 Height 函数、查找函数](#2.3.2 Size 、 Height 函数、查找函数)
一、红黑树的基本概念
1.1 红黑树是什么?
红黑树是一种自平衡的二叉查找树 ,红黑树本质上是一棵二叉搜索树 ,它拥有二叉搜索树的所有基本特性。普通的二叉搜索树在插入有序数据时,会退化成链表,查找效率从 O(log n) 降为 O(n)。为了解决这个问题,红黑树在二叉搜索树的基础上增加了以下五个核心规则,通过这些规则来约束树的生长,使其尽可能地保持平衡。
1.2 红黑树的五大规则
- 每个结点不是黑色就是红色。
- 根结点是黑色的。
- 所有叶子结点都是黑色的。(不是普通的叶子结点,而是叶子结点的左右孩子即空结点
NIL 或 NULL) - 红色结点的两个子结点必须是黑色,即任意一条路径上不能有两个连续的红色结点。这条规则是保证平衡的关键,它限制了任何路径上红色节点的数量。
- 对于任意一个结点,从该结点到其所有
NULL结点的简单路径上,均包含相同数量的黑色结点。

注意:最后两条规则共同确保了没有一条路径会比其他路径长出两倍,从而近似平衡。
1.3 红黑树的效率
假设N是红黑树树中结点数量,h最短路径的长度,那么 (2h - 1) <= N <= (22h - 1), 由此推出h ≈ logN,也就是意味着红黑树增删查改最坏也就是走最长路径 2 ∗ logN,那么时间复杂度还是O(logN)。
红黑树 的表达相对AVL树要抽象一些,AVL树通过高度差直观的控制了平衡。红黑树通过规则对颜色的约束,间接的实现了近似平衡,它们效率都是同一档次,但是相对而言,插入相同数量的结点,红黑树的旋转次数是更少的,因为它对平衡的控制没那么严格。
1.4 红黑树的底层节点结构
cpp
// 枚举值表示颜色
enum Color
{
RED,
BLACK
};
// 这里我们默认按key/value结构实现
template<class K, class V>
struct RBTreeNode
{
// 这里更新控制平衡也要加入父指针
pair<K, V> _kv;
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
Color _col;
RBTreeNode(const pair<K, V>& kv)
: _kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _col(RED)
{ }
};
每个结点都是有颜色区分的,所以我们可以枚举RED、BLACK两种状态。与AVL树的节点结构相比将平衡因子换成了节点颜色。
二、红黑树的实现
2.1 红黑树的 insert 操作
细节:
- 如果是空树插入,新增结点是黑色结点 。如果是非空树插入,新增结点必须红色结点 ,因为非空树插入,新增黑色结点就破坏了规则
5,规则5是很难维护的。 - 非空树插入时,新增结点必须红色结点,如果父结点为黑色,则没有违反规则,插入结束,如果父结点为红色,违反了规则
4,此时就需要分情况变色处理。
cpp
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)
{
parent = cur;
if (kv.first > cur->_kv.first)
{
cur = cur->_right;
}
else if (kv.first < cur->_kv.first)
{
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(kv);
cur->_col = RED;
if (cur->_kv.first > parent->_kv.first) parent->_right = cur;
else parent->_left = cur;
cur->_parent = parent;
while (parent && parent->_col == RED)
{
// 处理违反规则的情况
}
return true;
}
那么如何处理违反规则的情况呢?违反规则的原因主要是产生了连续的红色结点,我们可以分析一下情况,我们把新插入的结点叫做c(cur) ,把新插入结点的父结点叫做p(parent) ,把父结点的父结点叫做g(grandpa) ,把g结点的另一个孩子结点叫做u(uncle)。
其中我们新插入的结点是红色的,而p结点一定是红色 ,那么g结点一定是黑色 ,它们都是固定的,所以唯一的变量就是u结点。
u结点可以分为三种情况,1、u结点不存在;2、u结点是红色结点;3、u结点是黑色结点。
2.1.1 情况一: 变色
当叔叔存在且为红色结点,则将p和u变黑,g变红。在把g当做新的c,继续往上更新 。所以c结点不一定是新增结点,可能是由g结点变红之后再次违反规则产生的新的c。
这里我们给出一个实际的示例图:

上图中刚好变色后g结点的父结点为黑结点,所以就不用向上处理了。
c为新增结点时:

c不是新增结点时:

这里有一个细节,就是当c持续往上更新时,如上图,当10就是根节点时,那就不需要向上更新了,但是此时10这个结点变成了红色,循环已经结束了,那怎么处理呢? 很简单,我们在循环外面加上一个_root->_col = BLACK;语句就好了。
搞清楚这些,我们就可以写这种情况在插入部分的代码了。
cpp
while (parent && parent->_col == RED)
{
// 处理违反规则的情况
Node* grandpa = parent->_parent;
// g 结点存在, 且左边是 p 结点
if (grandpa && grandpa->_left == parent)
{
Node* uncle = grandpa->_right;
// 叔叔存在且为红,变色处理
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandpa->_col = RED;
// 继续向上处理
cur = grandpa;
parent = grandpa->_parent;
}
}
else if (grandpa && grandpa->_right == parent)
{
Node* uncle = grandpa->_left;
// 叔叔存在且为红,变色处理
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandpa->_col = RED;
// 继续向上处理
cur = grandpa;
parent = grandpa->_parent;
}
}
}
// 将处理过程中变色的根结点恢复成黑色结点
_root->_col = BLACK;
2.1.2 情况二: 单旋 + 变色
u不存在,或者u存在且为黑色结点。
g p u c如果
p是g的左,c是p的左,那么以g为旋转点进行右单旋,再把p变黑,g变红即可。p变成这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为p的父结点是黑色还是红色或者为空都不违反规则。
g u p c如果
p是g的右,c是p的右,那么以g为旋转点进行左单旋,再把p变黑,g变红即可。p变成课这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为p的父结点是黑色还是红色或者为空都不违反规则。

当u结点不存在时,c必须是新增结点 。因为u结点这条路径上只有一个黑色结点,如果c是由g变色而来,那么说明左边这条路径上的黑色结点大于1,这绝对不可能,所以当u不存在时,c就是新增结点。
注意:下图中标注的hb是子树中黑色结点的数量 。

当u结点存在且为黑色时,c结点肯定不是新增结点 。因为u结点这条路径上有至少两个黑色结点,如果c为新增结点,那么这条路径上才有一个黑色结点,绝对不可能,所以此时c结点不是新增结点。
了解这些之后就可以实现这种情况的插入部分代码了。
cpp
while (parent && parent->_col == RED)
{
// 处理违反规则的情况
Node* grandpa = parent->_parent;
// g 结点存在, 且左边是 p 结点
if (grandpa && grandpa->_left == parent)
{
Node* uncle = grandpa->_right;
// 叔叔存在且为红,变色处理
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandpa->_col = RED;
// 继续向上处理
cur = grandpa;
parent = grandpa->_parent;
}
else // 叔叔不存在 或者 存在且为黑
{
if (cur == parent->_left)
{
// g
// p u
//c
//右单旋 + 变色
RotateR(grandpa);
parent->_col = BLACK;
grandpa->_col = RED;
}
break;
}
}
else if (grandpa && grandpa->_right == parent)
{
Node* uncle = grandpa->_left;
// 叔叔存在且为红,变色处理
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandpa->_col = RED;
// 继续向上处理
cur = grandpa;
parent = grandpa->_parent;
}
else // 叔叔不存在 或者 存在且为黑
{
if (cur == parent->_right)
{
// g
// u p
// c
//左单旋 + 变色
RotateL(grandpa);
parent->_col = BLACK;
grandpa->_col = RED;
}
break;
}
}
}
// 将处理过程中变色的根结点恢复成黑色结点
_root->_col = BLACK;
return true;
}
private:
void RotateR(Node* parent)
{
//parent 是旋转点, 不是轴心
Node* subL = parent->_left; //相当于插入函数部分的 cur
Node* subLR = subL->_right;
//进行旋转操作
parent->_left = subLR;
if (subLR)//维护 subLR 的父指针
{
subLR->_parent = parent;
}
Node* pparent = parent->_parent; //subL 之后要更改的父指针指向
subL->_right = parent;
parent->_parent = subL;
//判断之前 parent 是什么角色, 便于更改 subL 的父指针
if (parent == _root)
{
_root = subL;
_root->_parent = nullptr;
}
else
{
if (pparent->_left == parent)
{
pparent->_left = subL;
}
else pparent->_right = subL;
subL->_parent = pparent;
}
}
void RotateL(Node* parent)
{
//parent 是旋转点, 不是轴心
Node* subR = parent->_right; //相当于插入函数部分的 cur
Node* subRL = subR->_left;
//进行旋转操作
parent->_right = subRL;
if (subRL)//维护 subRL 的父指针
{
subRL->_parent = parent;
}
Node* pparent = parent->_parent; //subR 之后要更改的父指针指向
subR->_left = parent;
parent->_parent = subR;
//判断之前 parent 是什么角色, 便于更改 subR 的父指针
if (parent == _root)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (pparent->_left == parent)
{
pparent->_left = subR;
}
else pparent->_right = subR;
subR->_parent = pparent;
}
}
其中左单旋、右单旋部分在上期的AVL树的模拟实现部分讲解了,如果感兴趣可以点此查看:详细操作。
2.1.3 情况三: 双旋 + 变色
这种情况也是u结点不存在或者存在且为黑色 ,只不过c结点相对p结点的位置发生了变化。
g p u c如果
p是g的左,c是p的右,那么先以p为旋转点进行左单旋,再以g为旋转点进行右单旋,再把c变黑,g变红即可。c变成这棵树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为c的父结点是黑色还是红色或者为空都不违反规则。
g u p c如果
p是g的右,c是p的左,那么先以p为旋转点进行右单旋,再以g为旋转点进行左单旋,再把c变黑,g变红即可。c变成这棵树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为c的父结点是黑色还是红色或者为空都不违反规则。


现在我们可以补充这种情况在插入部分的代码了。
插入操作的完整代码:
cpp
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)
{
parent = cur;
if (kv.first > cur->_kv.first)
{
cur = cur->_right;
}
else if (kv.first < cur->_kv.first)
{
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(kv);
cur->_col = RED;
if (cur->_kv.first > parent->_kv.first) parent->_right = cur;
else parent->_left = cur;
cur->_parent = parent;
while (parent && parent->_col == RED)
{
// 处理违反规则的情况
Node* grandpa = parent->_parent;
// g 结点存在, 且左边是 p 结点
if (grandpa && grandpa->_left == parent)
{
Node* uncle = grandpa->_right;
// 叔叔存在且为红,变色处理
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandpa->_col = RED;
// 继续向上处理
cur = grandpa;
parent = grandpa->_parent;
}
else // 叔叔不存在 或者 存在且为黑
{
if (cur == parent->_left)
{
// g
// p u
//c
//右单旋 + 变色
RotateR(grandpa);
parent->_col = BLACK;
grandpa->_col = RED;
}
else
{
// g
// p u
// c
//左右双旋 + 变色
RotateL(parent);
RotateR(grandpa);
grandpa->_col = RED;
cur->_col = BLACK;
}
break;
}
}
else if (grandpa && grandpa->_right == parent)
{
Node* uncle = grandpa->_left;
// 叔叔存在且为红,变色处理
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandpa->_col = RED;
// 继续向上处理
cur = grandpa;
parent = grandpa->_parent;
}
else // 叔叔不存在 或者 存在且为黑
{
if (cur == parent->_right)
{
// g
// u p
// c
//左单旋 + 变色
RotateL(grandpa);
parent->_col = BLACK;
grandpa->_col = RED;
}
else
{
// g
// u p
// c
//右左双旋 + 变色
RotateR(parent);
RotateL(grandpa);
cur->_col = BLACK;
grandpa->_col = RED;
}
break;
}
}
}
// 将处理过程中变色的根结点恢复成黑色结点
_root->_col = BLACK;
return true;
}
2.2 中序遍历
cpp
public:
void InOrder()
{
_InOrder(_root);
cout << endl;
}
private:
void _InOrder(Node* root)
{
if (root == nullptr) return;
_InOrder(root->_left);
cout << root->_kv.first << " ";
_InOrder(root->_right);
}
测试代码:
cpp
void TestRBTree1()
{
RBTree<int, int> t;
// 特殊的带有双旋场景的测试用例
int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
for (auto e : a)
{
if (e == 14)
{
int x = 0;
}
t.insert({ e, e });
}
t.InOrder();
}
测试结果 :

2.3 其它接口的实现
2.3.1 红黑树的验证
检查这棵树是不是红黑树,就是检查这棵树符不符合红黑树的规则。
cpp
public:
bool IsRBTree()
{
if (_root == nullptr) return true;
if (_root->_col == RED) return false;
// 检查黑色结点数量的一致性
//计算最左边的路径作为参考值
Node* leftcur = _root;
int blackref = 0;
while (leftcur)
{
if (leftcur->_col == BLACK) blackref++;
leftcur = leftcur->_left;
}
return Check(_root, 0, blackref);
}
private:
bool Check(Node* cur, int numb, int blackref)
{
if (cur == nullptr)
{
// 此时本条路径的黑色结点计算完了
if (numb != blackref)
{
cout << "黑色结点的数量不相等" << endl;
return false;
}
return true;
}
if (cur->_col == RED && cur->_parent && cur->_parent->_col == RED)
{
cout << cur->_kv.first << "->" << "连续的红色节点" << endl;
return false;
}
// 统计路径上的黑色结点数量
if (cur->_col == BLACK) numb++;
return Check(cur->_left, numb, blackref)
&& Check(cur->_right, numb, blackref);
}
测试代码:
cpp
void TestRBTree1()
{
RBTree<int, int> t;
// 特殊的带有双旋场景的测试用例
int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
for (auto e : a)
{
if (e == 14)
{
int x = 0;
}
t.insert({ e, e });
}
t.InOrder();
cout << t.IsRBTree() << endl;
}
测试结果 :

2.3.2 Size 、 Height 函数、查找函数
cpp
public:
int Size()
{
return _Size(_root);
}
int Height()
{
return _Height(_root);
}
private:
int _Size(Node* root)
{
return root == nullptr ? 0 : _Size(root->_left) + _Size(root->_right) + 1;
}
int _Height(Node* root)
{
if (root == nullptr)
{
return 0;
}
int leftH = _Height(root->_left);
int rightH = _Height(root->_right);
return leftH > rightH ? leftH + 1 : rightH + 1;
}
cpp
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (key > cur->_kv.first)
cur = cur->_right;
else if (key < cur->_kv.first)
cur = cur->_left;
else
return cur;
}
return nullptr;
}
测试代码:
cpp
void TestRBTree1()
{
RBTree<int, int> t;
// 特殊的带有双旋场景的测试用例
int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14, 36, 27, 19, 8 };
for (auto e : a)
{
if (e == 14)
{
int x = 0;
}
t.insert({ e, e });
cout << "insert:" << e << "->" << t.IsRBTree() << endl;
}
t.InOrder();
cout << t.IsRBTree() << endl;
cout << t.Size() << " " << t.Height() << endl;
cout << t.Find(2) << " " << t.Find(17) << endl;
}
测试结果 :

总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~
