⭐上篇文章:35.C++二叉树进阶4(平衡二叉搜索树 - AVL树及其旋转操作图解)-CSDN博客
⭐本篇代码:c++学习/19.map和set的使用用与模拟 · 橘子真甜/c++-learning-of-yzc - 码云 - 开源中国 (gitee.com)
⭐标⭐是比较重要的部分
一. 什么是红黑树?
红黑树也是一种二叉搜索树,红黑树中的节点有红色与黑色,通过控制节点的颜色来保证最长路径节点数不会超过最短路径节点数的2倍。 从而保证该二叉搜索树是非常接近 平衡的。
4.1 红黑树的性质
1 根节点是黑色的
2 每一条路径上的黑色节点数量相同
3 红色节点的孩子只能为黑色节点(无连续的红色节点)
4 叶子节点是黑色的(这里的叶子指的是最后的空节点)
通过上面的性质可以总结出来:
每条路径黑色节点数量相同
最短路径为:全部是黑色节点
最长路径为:一红一黑
这样就能保证最长路径不会最短路径的二倍,从而保证红黑树是近似平衡的。

二. 有AVL树为何还要红黑树?
AVL树通过平衡因子和旋转操作来保证整棵树是高度平衡的,可以解决二叉搜索树退化为链表的问题。保证其增删查改效率为O(log N)。
而红黑树通过控制节点颜色来保证整棵树是近似平衡的(最长路径不超过最短路径二倍),能够保证其增删查改的效率为O(log N) ~ 2*O(log N)
既然如此,为何还要红黑树呢?
1 红黑树的平均效率是比AVL较低,但是现代计算机CPU效率非常高,logN和2 *log N的差异不是很大
2 AVL树严格的平衡是需要不断的旋转操作得到的,旋转操作是比较耗时的。而红黑树的旋转操作比AVL树少得多。
所以在很多情况下,红黑树的效率更高,综合性能更好,且实现简单。
三. 红黑树的代码框架
3.1 红黑树的节点
红黑树节点只需增加一个表示颜色的变量即可
cpp
enum Color
{
RED,
Block
};
template<class K,class V>
struct rbTreeNode
{
rbTreeNode<K, V>* _left;
rbTreeNode<K, V>* _right;
rbTreeNode<K, V>* _parent;
pair<K, V> _kv;
Color col;
rbTreeNode(const pair<K, V>& _kv = pair<K, V>())
:_left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _col(RED)
{}
};
这里默认将节点设置为红色,因为红色节点更温和,插入新节点容易调整红黑树。如果默认节点是黑色,每一次插入数据都要调整各个路径的值,操作复杂。
3.2 红黑树框架
cpp
template<class K,class V>
class RBTree
{
using Node = rbTreeNode<K, V>;
public:
bool Insert(const pair<K, V>& kv)
{
}
bool Find(const pair<K, V>& kv)
{
}
private:
Node* _root;
};
如果想要增加其他功能,可以直接在类内部增加。本文主要说明红黑树的节点的插入。
四. 红黑树节点的插入操作⭐
首先安装二叉搜索树的性质进行插入数据,插入数据后再根据红黑树的性质进行调整
3.1 插入节点父亲为黑色(情况一)
如果插入节点的父亲为黑色,插入节点为红色。直接插入即可,无论是其左孩子还是右孩子。这样不会破坏红黑树的任意一个条件。
3.2 插入节点父亲为红色
由于父亲为红色,插入节点也为红色。破坏了红黑树的条件(红色节点的孩子必须为黑色)。此时就需要进行调整。这里有很多种情况,读者可以根据图解快速理解。
注意:
1 如果父亲为红色,那么爷爷一定是黑色
2 所以红黑树的调整主要是:叔叔节点的颜色,位置(爷爷的左右孩子),是否存在
a 叔叔存在且颜色为红色(情况二与三)
如果叔叔存在且为红色,则会有下面两种情况。

上面这两种情况,**我们只需要将父亲p和叔叔u变为黑色,然后将爷爷变为黑色。**这样就能保证从爷爷开始的整颗树满足红黑树
如下图:

调整完之后,还要继续向上调整。
如果爷爷是根节点,则直接将爷爷变为黑色,调整结束。否则需要继续向上调整,因为爷爷是红色节点,如果爷爷的父亲是红色则需要重新调整(不能有连续的红节点)。
此时爷爷g就是新的cur。
就像下图:

b 叔叔不存在或者叔叔是黑色节点(情况三与四)
图解如下:解释在图解中

所以这种只需要对g进行右旋转然后再让p变黑色,g变红色即可
如果cur此时是父亲的右侧,则需要先对p进行左旋。如下图:

上面展示的是cur父亲是cur爷爷左孩子的情况,对于右孩子的情况也是一样。只是旋转的方向改一改就行。
c 插入操作的代码
cpp
bool Insert(const pair<K, V>& kv)
{
//1.按照搜索树的规则进行插入
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;
}
}
cur = new Node(kv);
if (cur->_kv.first > parent->_kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;
//2.调整颜色
//新增结点是红色的(因为红色节更温和,影响红黑树的结构小)
cur->_col = RED;
while (parent && parent->_col == RED)
{
Node* grandfather = parent->_parent;
//父亲在祖父左边
if (parent == grandfather->_left)
{
//红黑树的调节关键看叔叔
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
//继续向上调整
cur = grandfather;
parent = grandfather->_parent;
}
else
{
//情况二三,叔叔是黑色或者不存在
//情况三,cur在parent右边(折线),此时对parent左旋,再对grandfather右旋
if (parent->_right == cur)
{
RotateLeft(parent);
swap(parent, cur); //只是交换了指针,kv节点并没有交换(更换了父亲和孩子的位置)
}
//情况二,cur在parent左边(直线),此时对grandfather右旋即可
//再让parent变黑,grandfather变红
RotateRight(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
break;
}
}
else //说明父亲在祖父的右边
{
Node* uncle = grandfather->_left;
//情况一
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = grandfather->_parent;
}
else
{
//情况二三
//情况三(折线),此时父亲在右边,cur在父亲左边,对parent右旋,再对grandfath左旋
if (cur == parent->_left)
{
RotateRight(parent);
swap(cur, parent);
}
//情况二(直线)
RotateLeft(grandfather);
grandfather->_col = RED;
parent->_col = BLACK;
break; //此时颜色已经调整好了,不需要继续调整
}
}
}
_root->_col = BLACK; //无论如何变色,最后让根为黑色。就不会出错
return true;
}
旋转操作的代码:
cpp
void RotateLeft(Node* parent)
{
if (!parent)
return;
Node* ppNode = parent->_parent;//要旋转节点的父亲
Node* SubR = parent->_right;//要旋转节点的右孩子
Node* SubRLeft = SubR->_left;//要旋转节点右孩子的左孩子
//一:调整节点
parent->_right = SubRLeft;
if (SubRLeft)
SubRLeft->_parent = parent;
SubR->_left = parent;
parent->_parent = SubR;
//1.parent是根,现在SubR是根
//2.parent是整棵树的子树,找到其父亲,旋转完成后,让subR与其父亲相连接
if (_root == parent)
{
_root = SubR;
SubR->_parent = nullptr;
}
else
{
if (ppNode->_left == parent)
{
ppNode->_left = SubR;
}
else
{
ppNode->_right = SubR;
}
SubR->_parent = ppNode;
}
}
//左左,右单旋
void RotateRight(Node* parent)
{
if (!parent)
return;
Node* ppNode = parent->_parent;
Node* SubL = parent->_left;
Node* SubLRight = SubL->_right;
//1.旋转,调整节点
parent->_left = SubLRight;
if (SubLRight)
SubLRight->_parent = parent;
SubL->_right = parent;
parent->_parent = SubL;
if (_root == parent)
{
_root = SubL;
SubL->_parent = nullptr;
}
else
{
if (ppNode->_left == parent)
{
ppNode->_left = SubL;
}
else
{
ppNode->_right = SubL;
}
SubL->_parent = ppNode;
}
}
其他操作的代码在代码仓库中,读者可自行阅读。
五. 红黑树的应用
很多软件,代码库都有着红黑树的身影。
1 C++STL中的map/set使用的就是红黑树
2 在某些代码库中,哈希表的链表过长时候会使用红黑树替代
3 某些数据库系统使用红黑树作为索引
4 Linux内核调度器,虚拟内存管理使用红黑树
......