目录

红黑树简介
为了保持AVL树的平衡性,在插入和删除操作后,会非常频繁地调整全树的整体拓扑结构,代价比较大,为此在AVL树的基础上放宽一些条件,则引入了红黑树结构。
红黑树是一棵二叉搜索树 ,它和AVL树类似,也需要通过旋转来调整树的高度,避免在极端情况下造成单边树。但它的旋转又不像AVL树那么严格,它是通过颜色来约束的 。
红黑树中,它的每个节点都增加一个存储位来表示节点颜色,可以是红色也可以是黑色。有了颜色,以及旋转的条件,使得红黑树也差不多能达到平衡的效果,相比AVL树不会太差。
即:
AVL树------严格平衡
红黑树------近似平衡
红黑树的性质
红黑树的性质有以下几点:
- 每个节点不是黑色就是红色
- 根节点是黑色的
- 一条路径中,没有连续的红色节点
- 每条路径黑色节点数量相等
- 叶子节点(即虚构的外部节点、NULL节点)都是黑色的。
满足以上性质,红黑树就能保证:最长路径中节点个数不会超过最短路径节点数的二倍 。即最短路径*2≥最长路径。
王道上有一句小口诀:左根右,根叶黑,不红红,黑路同
下面就是一棵标准的红黑树。

下面解答一下为什么红黑树能保证:最长路径中节点个数不会超过最短路径节点数的二倍。
红黑树中最短路径:该路径中是全黑,个数设其为n
红黑树中最长路径:一红一黑相间排列,则红节点数≤黑节点数,则个数最多为2n。
所以则有:最短路径*2≥最长路径。
注意:在数路径时,不能数有多少个叶子节点作为路径,而应该以空节点为终点来数。
则使用红黑树来查找时,查找最短路径上的时间复杂度是logn,而对于最长路径上查找时是2logn。但就算n非常大,就算是一亿,此时也只需要最长查找60次,对cpu来说,这些次数不算什么,因此在查找上红黑树和AVL树是差不多的。
但在插入和删除时红黑树更具有优势。
插入节点的探讨
对于红黑树,我们就要考量插入的时候到底插入红色节点还是黑色节点。
首先给出结论:默认插入红色节点。
有连续的红节点就有可能最长路径超过最短路径的二倍。但此时我们可以通过改变颜色和旋转来解决。而如果增加的黑色,无论怎么旋转变颜色,就不能保证每条路径黑色节点数量都相同。
红黑树的插入
红黑树从头到尾只看是否满足它所需的性质,并没有看它的高度。
红黑树本来就不想过多的旋转,它会先通过变颜色来调整。
首先红黑树的插入可定为以下规则:
- 先查找,确定插入位置(类似AVL树),插入新节点
- 新节点为根------则染为黑色
- 新节点为非根------则染为红色

红黑树如何调整,则需要看叔叔的脸色,也就是父亲节点的兄弟节点的颜色。
对于红黑树的调整和AVL树的调整差不多,AVL树是看平衡因子,哪个不符合规则就调整,并一直往上调整。红黑树也是一样,主要是看新增节点,父亲节点,叔叔节点,爷爷节点之间的颜色关系 ,然后来进行调整,并要连续向上调整。
只需要我每次调整它都是红黑树的状态,我就能保证它到最后一定是一棵红黑树,有点类似递归。
红黑树插入节点会保证每条路径的黑色节点数相同,若出现两个连续的红节点,则会进行调色或旋转来处理。
此时我们可以大致分为以下两种情况:
- 新增节点为红,父亲节点为红,爷爷节点为黑,叔叔节点存在且为红
即父亲红,叔叔红。
此时是最简单的一种,只需要调整颜色即可:
只需要把父亲和叔叔都变为黑色,把爷爷变为红色,此时再把爷爷节点当做新插入的节点,继续向上调整。
- 新增节点为红,父亲节点为红,爷爷节点为黑,叔叔节点不存在或存在且为黑
即父亲红,叔叔黑,此时则要考虑旋转。
类似于AVL树,同样有左单旋,右单旋,左右双旋,右左双旋这四种,只是还会加上颜色变换。
叔叔都是黑色或不存在,此时的旋转规则如下:
-
LL型:右单旋,父换爷+染色

-
RR型:左单旋,父换爷+染色

-
LR型:左右双旋,儿(cur)换爷+染色

-
RL型:右左双旋,儿(cur)换爷+染色

下面是插入的完整代码:
cpp
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
Node* k = new Node(kv);
_root = k;
_root->_col = BLACK;//根节点为黑节点
return true;
}
//找到要插入的位置
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (cur->_kv.first > kv.first)//往左走
{
parent = cur;
cur = cur->_left;
}
else if (cur->_kv.first < kv.first)//往右走
{
parent = cur;
cur = cur->_right;
}
else
return false;//找到重复的,直接退出,不允许插入一样的值
}
//找到要插入的位置了
cur = new Node(kv);
//新增节点的颜色定为红色
cur->_col = RED;
cur->_parent = parent;
if (parent->_kv.first > kv.first)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
while (parent && parent->_col == RED)//父亲节点为红,则一定有爷爷节点,因为根节点为黑
{
Node* grandfather = parent->_parent;
if (parent == grandfather->_left)
{
Node* uncle = grandfather->_right;
//uncle存在且uncle为红色------>单纯变色再继续往上处理
if (uncle && uncle->_col == RED)
{
uncle->_col = parent->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
//uncle存在且为黑,或者不存在------>旋转+变色,再继续向上处理
else
{
if (cur == parent->_left)//右单旋,父和爷染色
{
// g
// f u
//c
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else//双旋,左右双旋,儿爷染色
{
// grandfather
// father uncle
// cur
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
//当旋转变色完成之后,已达到平衡,可直接跳出
break;
}
}
else//parent == grandfather->_right
{
Node* uncle = grandfather->_left;
//uncle存在且uncle为红色------>单纯变色再继续往上处理
if (uncle && uncle->_col == RED)
{
uncle->_col = parent->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
//uncle存在且为黑,或者不存在------>旋转+变色,再继续向上处理
else
{
if (cur == parent->_right)//左单旋,父和爷染色
{
// g
// u p
// c
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else//双旋,右左双旋,儿爷染色
{
// grandfather
// uncle parent
// cur
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
//当旋转变色完成之后,已达到平衡,可直接跳出
break;
}
}
}
_root->_col = BLACK;//简单粗暴的把根变为黑
return true;
}
下面是往一棵空的红黑树依次插入节点的过程图:
从一棵空的红黑树开始依次插入:20,10,5,30,40



判断是否是红黑树
- 首先空树一定是红黑树,如果根不是黑色,则直接断定不是红黑树。
- 要看是否有连续的红节点
- 看每条路径上的黑色节点数量是否相同。
对于是否有连续的红节点,只需要看当前节点和它的父节点的颜色是否都是红色的即可。
对于每条路径上的黑色节点数量是否相同,比较难想到:
- 我们先算出最左侧路径的黑色节点数量,将其作为参考值。
- 给每个节点设置一个值,用来记录从根节点到该节点一共有多少个黑色节点。
- 调用递归,当递归到null节点时,判断黑色节点数和参考值是否相同,如果相同则正确。
代码如下:
cpp
public:
bool IsBalance()
{
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 Check(_root, 0, refNum);
}
private:
bool Check(Node* root, int blackNum, const int refNum)
{
if (root == nullptr)
{
// 前序遍历走到空时,意味着一条路径走完了
//cout << blackNum << endl;
if (refNum != blackNum)
{
cout << "存在黑色节点的数量不相等的路径" << endl;
return false;
}
return true;
}
// 检查孩子不太方便,因为孩子有两个,且不一定存在,反过来检查父亲就方便多了
if (root->_col == RED && root->_parent->_col == RED)
{
cout << root->_kv.first << "存在连续的红色节点" << endl;
return false;
}
if (root->_col == BLACK)
{
blackNum++;
}
return Check(root->_left, blackNum, refNum)
&& Check(root->_right, blackNum, refNum);
}
感谢大家观看,如果大家喜欢,希望大家一键三连支持一下,如有表述不正确,也欢迎大家批评指正。本篇还涵盖了王道的内容与图片。
