一、概念
红黑树是一棵二叉搜索树,它的每个结点增加一个存储位来表示结点的颜色,可以是红色或者黑色。通过对任何一条从根到叶子的路径上各个结点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出2倍,因此是接近平衡的。(没有AVL树那么严格)

1.1红黑树规则
1.每个结点不是黑色就是红色
2.根结点是黑色的
3.如果一个结点是红色的,那么它的两个孩子结点必须是黑色的,也就是说任意一条路径不会有连续的红色结点(不红红)
4.对于任意一个结点,从该结点到其所有NULL结点的简单路径上,均包含相同数量的黑色结点
1.2红黑树如何确保最长路径不超过最短路径的2倍的?
·由规则4可知,从根到NULL结点的每条路径都有相同数量的黑色结点,所以极端场景下,最短路径就是全黑的路径,假设最短路径长度为bh(black height)
·由规则2、3可知,任意一条路径上不会有连续的红色结点,所以极端场景下,最长路径就是一黑一红间隔组成,那么最长路径的长度为2*bh
·综合红黑树的四点规则,理论上的全黑路径和一黑一红的最长路径并不是在每棵红黑树都存在的。所以假设任意一条从根到NULL结点路径的长度为h,则bh<=h<=2*bh

二、红黑树的实现
2.1红黑树的结构
2.2红黑树的插入
2.2.1红黑树插入一个值的大概过程
1.插入一个值按二叉搜索树规则进行插入,插入后需要观察是否符合红黑树的4条规则
2.如果是空树插入,新增结点是黑色结点。
如果是非空树插入,新增结点必须是红色结点。(因为若是非空插入黑结点会破坏规则4)
3.非空树插入,新增结点必须是红色结点,若父节点是黑色结点,则没有问题,插入结束
4.非空树插入,新增结点必须是红色结点,若父节点是红色结点,则违反规则3(不红红),这需要进一步分析。
c为红,p为红,g必为黑,这三个颜色固定了,关键的变化看u的情况,需要根据u分为以下几种情况进行处理。(关键是要看叔叔!)
注:这里把新增结点标识为c(cur),c的父亲标识为p(parent),p的父亲标识为g(grandfather),p的兄弟标识为u(uncle)
2.2.2情况1:变色
c为红,p为红,g为黑,u存在且为红,则需要将p和u变黑,g变红,然后并未结束,还要再把g当作新的c,往上更新。


为什么呢?
因为p和u都是红色,g是黑色,把p和u变黑,左边子树路径各增加一个黑色结点,g再变红,相当于保持g所在子树的黑色结点的数量保持不变,同时解决了c和p连续红色结点的问题,需要再往上更新是因为,g是红色,如果g的父亲还是红色,那么就需要继续处理,如果g的父亲是黑色,则处理结束,如果g是整棵树的根,再把g变成黑色。
2.2.3情况2:单旋+变色
c为红,p为红,g为黑,u不存在或者u为黑---u不存在,则c一定是新增结点;u存在且为黑,则c一定不是新增节点,它之前是黑色的,是在c的子树中插入的时候,符合情况1,把c从黑色变为红色,更新上来的。
p是必须要变黑的,才能解决"红红"问题,u不存在或者u为黑,这里单纯的变色无法解决**(因为不能保证每条路上黑色结点数量相同),需要旋转+变色**。

这里又分为两种情况:
1.如果parent是grandparent的左孩子,cur是parent的左孩子,那么以grandparent为旋转点进行右单旋,再把parent变黑,grandparent变红。(parent会变成这棵树新的根)

2.如果parent是grandparent的右孩子,cur是parent的右孩子,那么以grandparent为旋转点进行左单旋,再把parent变黑,grandparent变红。(parent会变成这棵树新的根)

3.如果叔叔不存在这种情况,与叔叔存在且为黑的这种情况解决方法是一样的:

2.2.4情况3:双旋+变色
c为红,p为红,g为黑,u不存在或者u为黑---u不存在,则c一定是新增结点;u存在且为黑,则c一定不是新增节点,它之前是黑色的,是在c的子树中插入的时候,符合情况1,把c从黑色变为红色,更新上来的。
这里双旋+变色又分为两种情况:
1.如果parent是grandparent的左,cur是parent的右,那么先以parent为旋转点进行左单旋,再以grandparent为旋转点进行右单旋,然后把cur变红,grandparent变红即可。(cur会变成这棵树新的根)


2.如果parent是grandparent的右,cur是parent的左,那么先以parent为旋转点进行右单旋,再以grandparent为旋转点进行左单旋,然后把cur变红,grandparent变红即可。(cur会变成这棵树新的根)


2.2.3插入代码的实现
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)
{
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;
//看它插在parent的哪边 左还是右
if (parent->_kv.first > kv.first)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
//要记得与父亲进行链接
cur->_parent = parent;
//父亲要是红,"红红"需要有操作
while (parent->_col == RED)
{
Node* grandfather = parent->_parent;
//去找叔叔 再分几种情况
//1.叔叔在右边
if (parent == grandfather->_left)
{
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED)//叔叔存在且为红
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
//继续往上更新
cur = grandfather;
parent = cur->_parent;
}
else//先整叔叔为黑的情况,再整叔叔不存在的情况,其实叔叔不存在处理手法一样
{
//旋转+变色
//叔叔存在
//cur是parent的左孩子
if (cur == parent->_left)
{
RotateR(grandfather);
grandfather->_col = RED;
parent->_col = BLACK;
}
else//cur是parent的右孩子
{
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
else//2.叔叔在左边
{
Node* uncle = grandfather->_left;
//叔叔存在且为红
if (uncle && uncle->_col == RED)
{
uncle->_col = parent->_col = BLACK;
grandfather->_col = RED;
//继续向上更新
cur = grandfather;
parent = cur->_parent;
}
else//叔叔不存在或者叔叔为黑
{
//叔叔为黑
//cur在parent的右
if (cur == parent->_right)
{
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else//cur在parent的左
{
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
//根直接置黑,解决这种情况:爷变红,结果爷是根
_root->_col = BLACK;
return true;
}
2.4红黑树的查找
与二叉搜索树逻辑一致,大了往右找,小了往左找
cpp
Node* Find(const K& key)
{
Node* cur = root;
while (cur)
{
if (cur->_kv.first > key)
{
cur = cur->_left;
}
else if (cur->_kv.first < key)
{
cur = cur->_right;
}
else
{
return cur;
}
}
return nullptr;
}
2.5红黑树的验证
这里检查最长路径不超过最短路径的2倍是不可行的,因为就算满足这个条件,红黑树也可能颜色不满足规则,当前没问题,后续插入可能会出问题。
所以我们需要去检查四点规则,满足这四点,则一定没问题。
1.规则1枚举颜色类型,天然实现保证可颜色不是黑色就是红色
2.规则2直接检查根即可
3.规则3前序遍历检查,遇到红色结点检查孩子不太方便,因为孩子有两个,且不一定存在,但是反过来检查父亲的颜色就方便很多了。
4.规则4前序遍历,遍历过程中用一个形参来记录当前结点的黑色数量,前序遍历遇到黑色结点就++,走到空就可以计算出一条路径的黑色结点数量。再任意一条路径黑色结点数量作为参考值,依次比较即可。
cpp
bool check(Node* root, int blacknum, const int refnum)
{
if (root == nullptr)//走到空说明一条路径走完了
{
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)
}