1. 红黑树的概念
红黑树是一棵二叉搜索树,它的每个结点增加一个存储位来表示结点的颜色,可以是红色或者黑色。通过对任何一条从根到叶子的路径上各个结点的颜色进行约束,**红黑树确保没有一条路径会比其他路径长出 2 倍,因而接近平衡。**也就是说,如果一棵树的最短路径是:h,那最长路径不会大于2h。这里所说的路径,指的是从根节点到nullptr节点的这段路程。
所以,可以说红黑树是一个近似二叉平衡搜索树的树。
2. 红黑树的规则
-
每个结点不是红色就是黑色。
-
根结点是黑色的。
-
如果一个结点是红色的,则它的两个孩子结点必须是黑色的,也就是说任意一条路径不会有连续的红色结点。
-
对于任意一个结点,从该结点到其所有 NULL 结点的简单路径上,均包含相同数量的黑色结点。
我们要思考一个问题:对于红黑树来说,最短路径和最长路径的特征是什么?
以这个树为例:

因为路径指的是从根节点到nullptr节点的这段路程,那么此图中最短路径就是最左侧的两个黑色节点所组成的路径,最长路径就是最右侧的两个路径。因此最短路径的特征是:全部由黑色节点组成的路径;最长路径的特征是:由红黑相间的节点组成的路径。
说明:《算法导论》等书籍上补充了一条每个叶子结点 (NIL) 都是黑色的规则。他这里所指的叶子结点不是传统的意义上的叶子结点,而是我们说的空结点,有些书籍上也把 NIL 叫做外部结点。就如下图所示:

NIL 是为了方便准确的标识出所有路径,《算法导论》在后续讲解实现的细节中也忽略了 NIL 结点,所以我们知道一下这个概念即可。
3. 红黑树的效率
假设 N 是红黑树树中结点数量,h 最短路径的长度,以下图为例:

因为对于一个满二叉树来说,二叉树高度为h,则总节点个数就是 2^h - 1 。因此对于红黑树来说,结点数量和最短路径长度h的关系就是: 2h − 1 <= N < 2^(2∗h) −1, 由此推出 h ≈ logN,也就是意味着红黑树增删查改最坏也就是走最长路径2∗logN,那么时间复杂度还是O(logN)。
红黑树的表达相对 AVL 树要抽象一些,AVL 树通过高度差直观的控制了平衡。红黑树通过 4 条规则的颜色约束,间接的实现了近似平衡,他们效率都是同一档次,但是相对而言,插入相同数量的结点,红黑树的旋转次数是更少的,因为他对平衡的控制没那么严格。
4. 红黑树的模拟实现
4.1 红黑树的结构

这里使用的enum是C++ 中的枚举类型定义,用来表示红黑树节点的颜色(红色 / 黑色)。
enum是 C++ 的关键字,用于定义枚举类型 (把一组离散的、有限的取值,用符号化的名称表示);这里定义的枚举类型名为Colour(颜色);枚举类型的取值(枚举常量)是RED和BLACK,分别对应 "红色" 和 "黑色"------ 后续在红黑树的节点结构体中,可以用Colour _col;这样的成员变量,来存储节点的颜色。
剩下的部分就是和AVL树的结构一模一样了,都有左右子节点和父节点变量,并且都有key-value值。
4.2 红黑树的插入
4.2.1 插入的思路
插入一个值按二叉搜索树规则进行插入 ,插入后我们只需要观察是否符合红黑树的 4 条规则。
如果是空树插入,新增结点是黑色结点。如果是非空树插入,新增结点必须红色结点,因为非空树插入,新增黑色结点就破坏了规则 4,规则 4 是很难维护的。
非空树插入后,新增结点必须红色结点,如果父亲结点是黑色的,则没有违反任何规则,插入结束。
非空树插入后,新增结点必须红色结点,如果父亲结点是红色的,则违反规则 3。
进一步分析,c 是红色,p 为红,g 必为黑,这三个颜色都固定了,因为如果g是红色,那么在插入之前这棵就是错的。但是u的颜色不能确定,所以关键的变化看 u 的情况,需要根据 u 分为以下三种情况分别处理:1. u 不存在。 2. u 存在且为黑。 3. u 存在且为红。

说明:图中假设我们把新增结点标识为 c (cur),c 的父亲标识为 p (parent),p 的父亲标识为 g (grandfather),p 的兄弟标识为 u(uncle)。
4.2.2 变色
因为上面说到:新增节点都是先假设为红色,那有可能就会出现新增节点的父节点也是红色,这就影响了规则三,因此涉及到变色的概念,以下图为例:
单纯变色的场景出现时,一定是:c 为红,p 为红,g 为黑,u 存在且为红。
则将 p 和 u 变黑,g 变红。在把 g 当做新的 c,继续往上更新。
分析:因为 p 和 u 都是红色,g 是黑色,把 p 和 u 变黑,左边子树路径各增加一个黑色结点,g 再变红,相当于保持 g 所在子树的黑色结点的数量不变,同时解决了 c 和 p 连续红色结点的问题。
需要继续往上更新是因为,g 是红色,++如果 g 的父亲还是红色++ ,就回到了和刚刚重复的问题,需++要把g变成新的c,然后继续处理++ ;++如果 g 的父亲是黑色,则处理结束了++ ;++如果 g 就是整棵树的根,再把 g 变回黑色。++
情况 1 只变色,不旋转。所以无论 c 是 p 的左还是右,p 是 g 的左还是右,都是上面的变色处理方式。
跟 AVL 树类似,图 0 我们展示了一种具体情况,但是实际中需要这样处理的有很多种情况。
图 1 将以上类似的处理进行了抽象表达,看场景二的最左侧的图,d/e/f 代表每条路径拥有 hb 个黑色结点的子树,a/b 代表每条路径拥有 hb-1 个黑色结点的根为红的子树,hb>=0。
这里a/b之所以是hb-1,因为x的颜色是黑,如果包含a/b的父节点x,那a/b的黑色节点也是hb个。
图 2 / 图 3 / 图 4,分别展示了 hb==0 / hb==1 / hb==2 的具体情况组合分析,当 hb 等于 2 时,这里组合情况上百亿种,这些样例是帮助我们理解,不论情况多少种,多么复杂,处理方式一样的,变色再继续往上处理即可,所以我们只需要看抽象图即可。




展示四种情况只是告诉大家这个情况非常复杂,但是变色的逻辑还是一样的。接下来我们来实现代码:

这里主要需要注意最后一句代码,_root->_col = BLACK,因为单纯变色完之后有两种结果:1. 如果 g 的父亲是黑色,则处理结束了 2. 如果 g 就是整棵树的根,再把 g 变回黑色。所以这里不管三七二十一,都将根节点置为黑色,就能同时满足这两种情况。
4.2.3 变色+单旋
变色+单旋的场景出现时,一定是:c 为红,p 为红,g 为黑,u 不存在或者 u 存在且为黑。
u 不存在,则 c 一定是新增结点;u 存在且为黑,则 c 一定不是新增,c 之前是黑色的。
分析: p 必须变黑,才能解决连续红色结点的问题,u 不存在或者是黑色的,这里单纯的变色无法解决问题,需要旋转 + 变色。
如果u不存在,有以下两种情况:
如果 p 是 g 的左,c 是 p 的左,那么以 g 为旋转点进行右单旋,再把 p 变黑,g 变红即可。p 变成这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为 p 的父亲是黑色还是红色或者空都不违反规则。

如果 p 是 g 的右,c 是 p 的右,那么以 g 为旋转点进行左单旋,再把 p 变黑,g 变红即可。p 变成这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为 p 的父亲是黑色还是红色或者空都不违反规则。这里就和AVL树中的左单旋场景一样,不再赘述。
如果u存在且为黑,如下图所示:

在这里对于a/b两个子树来说,是因为有新插入的节点插入在了a/b子树上,新增节点是红色,a/b子树的根节点也是红色,这就引发了单纯变色,于是a/b子树的根节点变黑,所以a/b子树当中黑色节点就多了一个,于是黑色节点数量就从hb-1变成了hb。再让c节点变红,这时的场景是:c为红,p为红,u存在且为黑,就需要变色再加单旋。
接下来实现代码:

4.2.4 变色+双旋
c 为红,p 为红,g 为黑,u 不存在或者 u 存在且为黑,u 不存在,则 c 一定是新增结点,u 存在且为黑,则 c 一定不是新增,c 之前是黑色的,是在 c 的子树中插入,符合情况 1,变色将 c 从黑色变成红色,更新上来的。
分析:p 必须变黑,才能解决连续红色结点的问题,u 不存在或者是黑色的,这里单纯的变色无法解决问题,需要旋转 + 变色。
如果 p 是 g 的左,c 是 p 的右,那么先以 p 为旋转点进行左单旋,再以 g 为旋转点进行右单旋,再把 c 变黑,g 变红即可。c 变成这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为 c 的父亲是黑色还是红色或者空都不违反规则。

如果 p 是 g 的右,c 是 p 的左,那么先以 p 为旋转点进行右单旋,再以 g 为旋转点进行左单旋,再把 c 变黑,g 变红即可。c 变成这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为 c 的父亲是黑色还是红色或者空都不违反规则。
这里的逻辑和变色+单旋几乎一样,只是要进行两次单旋。
接下来实现代码:


5. 红黑树的平衡检测
和AVL树一样,插入完成之后我们也需要判断一下是否是一个合格的红黑树。
我们前面讲到AVL树的平衡检测的时候,是计算每个节点的左右子树的高度,再计算高度差,然后和该节点的平衡因子作比较。但是对于红黑树来说,如果沿着这个思路走,我们去写一个算法,找到红黑树的最短路径和最长路径,然后比较它们俩之间的大小有没有超过两倍。但除了最长最短路径的关系之外,还要考虑到红黑树当中有没有连续的两个红色节点。
因此我们检查红黑树是否平衡,还是去看每个节点是否满足红黑树的四个规则,只要能满足这四个规则,也就一定能做到最长路径不超过最短路径的两倍。
红黑树的四个规则:
-
每个结点不是红色就是黑色
-
根结点是黑色的
-
如果一个结点是红色的,则它的两个孩子结点必须是黑色的,也就是说任意一条路径不会有连续的红色结点。
-
对于任意一个结点,从该结点到其所有 NULL 结点的简单路径上,均包含相同数量的黑色结点
每个规则的方法:
-
规则 1 枚举颜色类型,天然实现保证了颜色不是黑色就是红色。
-
规则 2 直接检查根即可
-
规则 3 前序遍历检查,遇到红色结点查孩子不太方便,因为孩子有两个,且不一定存在,反过来检查父亲的颜色就方便多了,因为红色结点的父亲一定是黑色结点,并且必然存在,如果不是那就证明这棵树有问题。
-
规则 4 前序遍历,遍历过程中用形参记录跟到当前结点的 blackNum (黑色结点数量),前序遍历遇到黑色结点就 ++blackNum,走到空就计算出了一条路径的黑色结点数量。再任意一条路径黑色结点数量作为参考值,依次比较即可。

在这段代码种,IsBalance函数检查了根节点是否为红色,并且调用Check函数,在Check函数种使用递归实现前序遍历,去检查是否有连续的红色节点出现。那么对于规则四,我们使用一个形参去记录黑色节点的数量。

只得到一个黑色节点的数量还没有用,我们还需要得到一个基准值,在这里我们先遍历最左侧路径中的黑色节点数量,用blackNumRef去存储作为基准值,然后在递归的过程中,只要走到了nullptr节点,就代表这个路径中的黑色节点数量已经获取完了,那就和基准值进行比较,如果获取到的黑色节点数量和基准值不一样,那就说明这棵树有问题。
最后展示一下全部代码:
cpp
#pragma once
enum Colour
{
RED,
BLACK
};
template<class K, class V>
struct RBTreeNode
{
// 这里更新控制平衡也要加入parent指针
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)
{}
};
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);
cur->_col = RED;
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;
//u存在且为红,单纯变色即可
if (uncle->_col == RED)
{
uncle->_col = BLACK;
parent->_col = BLACK;
grandfather->_col = RED;
//继续向上处理
cur = grandfather;
parent = cur->_parent;
}
else //u不存在,或者u存在且为黑, 单旋+变色
{
//如果是这种结构:
// g
// p u
// c
// 右单旋
if (cur == parent->_left)
{
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
//如果是这种结构:
// g
// p u
// c
// 左右双旋
else
{
if (cur == parent->right)
{
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
}
break;
}
}
else //这种情况是:grandfather->_right == parent
{
Node* uncle = grandfather->_left;
//u存在且为红,单纯变色即可
if (uncle->_col == RED)
{
uncle->_col = BLACK;
parent->_col = BLACK;
grandfather->_col = RED;
//继续向上处理
cur = grandfather;
parent = cur->_parent;
}
else //u不存在,或者u存在且为黑, 单旋+变色
{
//如果是这种结构:
// g
// u p
// c
// 左单旋
if (cur == parent->_right)
{
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
//如果是这种结构
// g
// u p
// c
// 右左双旋
else
{
if (cur == parent->_left)
{
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
}
break;
}
}
}
_root->_col = BLACK; //将根节点置为黑色
return true;
}
bool IsBalance()
{
if (_root == nullptr)
return true;
if (_root == RED)
return false;
Node* leftMost = _root;
int blackNumRef = 0;
while (leftMost)
{
if (leftMost->_col == BLACK)
{
++blackNumRef;
}
leftMost = leftMost->_left;
}
return Check(_root,0 , blackNumRef);
}
private:
bool Check(Node* cur , int blackNum, const int blackNumRef)
{
if (cur == nullptr)
{
if (blackNum != blackNumRef)
{
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)
{
++blackNum;
}
return Check(cur->_left , blackNum , blackNumRef)
&& Check(cur->_right , blackNum , blackNumRef);
}
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR; //改变subL、subLR、parent之间的位置关系
if (subLR != nullptr)
{
subLR->_parent = parent;
}
//改变三个节点的父子关系
//先记录下原来的旋转点的父节点
Node* parentParent = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
//下面改变subL的父节点
if (parent == _root) //旋转点是根节点
{
_root = subL;
subL->_parent = nullptr;
}
else //旋转点是一个局部子树
{
if (parentParent->_left == parent)
{
parentParent->_left = subL;
}
else
{
parentParent->_right = subL;
}
subL->_parent = parentParent;
}
}
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
//先改变位置关系
parent->_right = subRL;
subR->_left = parent;
//改变父子关系
Node* parentParent = parent->_parent;
parent->_parent = subR;
if (subRL != nullptr)
{
subRL->_parent = parent;
}
if (parent == _root)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (parentParent->_left == parent)
{
parentParent->_left = subR;
}
else
{
parentParent->_right = subR;
}
subR->_parent = parentParent;
}
}
private:
Node* _root = nullptr;
};
本文到此结束,感谢各位读者的阅读,如果有讲解的不到位或者错误的地方,欢迎大家批评和指正。