1 问题引入
为什么有AVL树,还要引入红黑树?
在进行多次的插入和删除时:
1)AVL树会存在大量的旋转操作,追求的是严格平衡;
2)红黑树通过为节点增加颜色来换取增删节点时旋转次数的降低,任何不平衡都会在三次旋转之内解决,追求的是部分平衡;
因此,在增删节点时,根据不同的情况,AVL树旋转的次数比红黑树多。
2 红黑树的概念
1)是一种二叉搜索树,但在每个节点上增加一个存储位表示节点的颜色(红色/黑色);
2)通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡。**即最长路径不会超过最短路径的2倍;**比如,假设最短路径是h,最长就是2h,其他路径的长度则为[h,2h]。
2.1 红黑树的性质
1)每个节点不是红色就是黑色;
2)根节点是黑色的;
3)如果一个节点是红色的,则它的俩个孩子节点必须是黑色的。即没有连续的红色节点;但是并没有说黑色节点的孩子必须是红色节点;
4)对于每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点。即每条路径都会包含相同数量的黑色节点;
5)每个叶子结点都是黑色的(此处的叶子结点指的是空节点,即NIL节点 ),避免数错了路径个数(路径:从根走到NIL节点)。
所以我们可以根据上面的性质思考一下,为什么满足上面的性质,红黑树就能保证:从根结点到NULL节点,其最长路径中节点个数不会超过最短路径节点个数的俩倍?

在极端场景下我们可以进行分析:
整体红黑树图,此时满足红黑树的性质:

最短路径:全黑

最长路径:一黑一红

2.2 AVL树与红黑树的比较
1)AVL树的高度接近log2N;
2)红黑树的高度,如果是从最短路径看,则是全黑,相当于一个满二叉树;

而满二叉树的高度计算如下:
N=2h−1 --》 h=log2(N+1) ≈ log2N;
那么我们可以知道,最长路径(一黑一红),则是2* log2N;
我们可以举一个实际的例子:
假如有10亿个数,那么log2N=10亿 --》N=30;
AVL树:需要寻找30次;
红黑树:需要寻找60次;
虽然红黑树寻找的次数比AVL数多,但是对于现有的cpu来说,并没有太大的影响。
总结:
1)从查找效率来说,AVL树与红黑树相差不大;
2)从增删操作来说,AVL树需要通过多次旋转来降低高度来保持严格平衡,但是旋转是有代价的。而红黑树并不是这样,只是近似平衡,从而减少了旋转的次数。
总的来说,红黑树的效率要比AVL树好。
3 红黑树的定义及插入操作讨论
3.1 红黑树的定义
cpp
enum Colour
{
RED,
BLACK
};
template<class K,class V>
struct RBTreeNode
{
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
pair<K, V> _kv;
Colour _col;
RBTreeNode(const pair<K,V>& kv)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_kv(kv)
,_col(RED)//新增节点给为红色
{
}
};
3.2 红黑树的插入操作讨论
红黑树也是一颗二叉搜索树,所以当进行插入操作的时候我们也可以通过之前所学系的AVL树的插入操作进行实行。 但是在插入过程中我们得遵循红黑树的性质,因此,我们对其进行了一些讨论,如下:
首先,我们先定义一个红黑树操作的框架:
cpp
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* 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->_left = cur;
cur->_parent = parent;
}
else
{
parent->_right = cur;
cur->_parent = parent;
}
//检验是否满足红黑树的性质,所以在此进行讨论
}
private:
Node* _root;
};
3.2.1 实施过程中的问题
根节点是黑色,这是红黑树的性质要求的。那么新插入结点开始为什么要是红色?
因为根据红黑树的性质要求,每条路径的黑色节点的数量都是相同的,那么如果插入结点是黑色的话,那么其他路径就都少了一个黑色节点,这样调整起来工程量较大,十分麻烦。所以插入结点颜色一定是红色。但是如果插入节点的父亲节点是黑色,那是没有问题的。若插入结点的父亲节点是红色的,那么就会违反红黑树的性质(没有连续的红色节点),那就需要进行处理。
3.2.2 对于叔叔节点(u)的讨论
接下来,我们插入红色节点,将父亲节点设定为红色,从而破坏了红黑树(没有连续的红色节点)的性质。 因为爷爷节点(g)、父亲节点(p)和当前节点(cur)都是固定的,所以下面我们将要对叔叔节点(u)进行讨论。
假设 :
①g(grandfather):爷爷节点
②p(parent):父亲节点
③u(uncle):叔叔节点
④cur(current):当前节点
1)情形一 :叔叔节点(u)存在且为红色
g = 黑色,p = 红色,u = 红色 ,cur = 红色。

问题说明:
①孩子节点(cur)是红色的,父亲节点(p)是红色的,爷爷节点(g)是黑色的,此时叔叔节点是我们的关键。
②根据红黑树的性质,俩个红色不能相连,所以我们需要将父亲节点(p)变为黑色,但是父亲节点(p)变黑,那么叔叔节点(u)就少了一个黑色。
此时我们将爷爷节点(g)变为红色。

③但是,此时叔叔节点(u)的路径就少了一个黑色节点。因此,我们将叔叔节点(u)变为黑色。

④爷爷节点(g)是否为根节点?
1)若爷爷节点(g)是根节点,则将爷爷节点(g)变为黑色。

2)若爷爷节点(g)是子树,爷爷节点(g)一定有父亲节点,且爷爷节点(g)的父亲如果是红色,则需要继续向上调整。

以上为第一种情况的抽象图,下面我们对具体的图形进行解析:
①a/b/c/d/e为空树

②c/d/e是下面4个子树中的一种(一个黑色节点):


从上我们可以看一下共有多少种情况:
c/d/e为上面4种中的任意一种:4×4×4=64种
插入位置共有4中位置
所以此红黑树共有64×4=256种情况。
总结(情况一(叔叔节点(u)存在且为红)的解决方案):

①p/u变为黑色,g变为红色;
②如果g为根,再把g变为黑色;
③若g不为根,则继续往上处理(g可以当做现在的cur);
④p/u是g的左或者右没有影响,cur是p的左或者右也没有影响,处理方式是相同的。
2)情况二:叔叔节点(u)不存在/叔叔节点(u)存在且为黑
①叔叔节点(u)不存在,则当前节点(cur)就是新增,因为如果cur不是新增节点,则cur和p一定有一个节点的颜色是黑色,就不满足每条路径黑色节点个数相同的性质。
②叔叔节点(u)存在,则一定是黑色。(因为改为叔叔节点(u)存在,且为红色的情况我们已经分析了),那么当前节点(cur)则必定为黑色。 第二步中的cur节点为红色是因为在调整过程中由黑色变为了红色。
总结(情况二:叔叔节点(u)不存在/叔叔节点(u)存在且为黑的解决方案):
①p为g的左孩子,cur为p的左孩子,则进行右单旋;
②p为g的右孩子,cur为p的右孩子,则进行左单旋;
③p、g变色:p变为黑色,g变为红色。
3)情况三:叔叔节点(u)不存在/叔叔节点(u)存在且为黑
虽然情况三和情况二的条件是一样的,但是cur和p不在同一侧。
1)叔叔节点(u)不存在,cur为新增节点
2)叔叔节点(u)存在且为黑

总结(情况三:叔叔节点(u)不存在/叔叔节点(u)存在且为黑的解决方案):
1)p为g的左孩子,cur为p的右孩子,则进行左右双旋+变色;
2)p为g的右孩子,cur为p的左孩子,则进行右左双旋+变色;
3)g、cur变色:g变为红色,cur变为黑色。
3.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);//新插入结点是红色,这是在初始化的时候就定义的红色
if (cur->_kv.first < parent->_kv.first)
{
parent->_left = cur;
cur->_parent = parent;
}
else
{
parent->_right = cur;
cur->_parent = parent;
}
//检验是否满足红黑树的性质,所以在此进行讨论
//1.此时cur为红,parent为红,此时就违反了红黑树的性质
//8.继续向上处理的判断是parent存在且为红色的情况,所以记性增加parent存在的情况
while (parent && parent->_col == RED)
{
//3.插入之前可以认为是红黑树,cur为红,parent为红,则肯定有爷爷节点
Node* grandfather = parent->_parent;
//4.此时,cur、parent、grandfather是固定的,则需要找uncle节点,从而看如何变化使其不违反红黑树性质
if (parent == grandfather->_left)
{
Node* uncle = grandfather->_right;
//5.情况一:叔叔节点uncle存在且为红
if (uncle && uncle->_col == RED)
{
parent->_col = BLACK;
uncle->_col = BLACK;
grandfather->_col = RED;
//6.继续往上处理
cur = grandfather;
parent = cur->_parent;
//7.第一种情况如果g为根,则将grandfather的颜色变为黑色,这个可以在循环外进行处理
//如果parent->_colour == 黑色,则无需进行处理
//所以我们需要进行处理的是parent->_colour == 红色,所以我们可以在while循环处进行下一次向上处理判断
}
else //8.情况二:叔叔节点uncle不存在/为黑色,但是此时不需要考虑uncle节点,因为旋转和变色都与其无关
{
//9.上面满足了p为g的左孩子,此时应满足cur是p的左孩子条件
if (cur == parent->_left)
{
RotalR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
//10.情况三:情况三和情况二的条件是一样的,但是cur和p不在同一侧。
//此时要求cur是p的右孩子情况
else
{
RotalLR(grandfather);
grandfather->_col = RED;
cur->_col = BLACK;
}
//11.如果是双旋,那么因为parent是红色,不能通过while循环判断,并且也不会违反规则了,所以可以直接break;
break;
}
}
else
{
Node* uncle = grandfather->_left;
//5.情况一:叔叔节点uncle存在且为红
if (uncle && uncle->_col == RED)
{
parent->_col = BLACK;
uncle->_col = BLACK;
grandfather->_col = RED;
//6.继续往上处理
cur = grandfather;
parent = cur->_parent;
//7.第一种情况如果g为根,则将grandfather的颜色变为黑色,这个可以在循环外进行处理
//如果parent->_colour == 黑色,则无需进行处理
//所以我们需要进行处理的是parent->_colour == 红色,所以我们可以在while循环处进行下一次向上处理判断
}
else //8.情况二:叔叔节点uncle不存在/为黑色,但是此时不需要考虑uncle节点,因为旋转和变色都与其无关
{
//9.上面满足了p为g的右孩子,此时应满足cur是p的右孩子条件
if (cur == parent->_right)
{
RotalL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
//10.情况三:情况三和情况二的条件是一样的,但是cur和p不在同一侧。
//此时要求cur是p的右孩子情况
else
{
RotalRL(grandfather);
grandfather->_col = RED;
cur->_col = BLACK;
}
//11.如果是双旋,那么因为parent是红色,不能通过while循环判断,并且也不会违反规则了,所以可以直接break;
break;
}
}
}
//7.如果grandfather是根,则可以再次进行处理
_root->_col = BLACK;
//2.如果parent不是红色,则说明没有问题
return true;
}
//旋转部分:与AVL树是一样的,只是减少了平衡因子
//左单旋
void RotalL(Node * parent)
{
//定义阶段
Node* subR = parent->_right;
Node* subRL = subR->_left;
//更改阶段
parent->_right = subRL;
if (subRL)//子树可能是空
{
subRL->_parent = parent;
}
//先记录parent的父结点,因为parent可能是一个子树,也可能是一个根
Node* ppnode = parent->_parent;
subR->_left = parent;
parent->_parent = subR;
//如果是根
if (parent == _root)
{
_root = subR;
subR->_parent = nullptr;
}
else//parent不是根,那么parent可能是父结点的左节点还是右结点
{
if (parent == ppnode->_left)
{
ppnode->_left = subR;
}
else
{
ppnode->_right = subR;
}
subR->_parent = ppnode;
}
}
//右单旋
void RotalR(Node* parent)
{
//1.定义节点
//2.更改阶段
//3.注意子树可能为空
//4.parent是否为根
//5.更新平衡因子
Node* subL = parent->_left;
Node* subLR = subL->_right;
//更改阶段
parent->_left = subLR;
if (subLR)
{
subLR->_parent = parent;
}
Node* ppnode = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (parent == _root)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (parent == ppnode->_left)
{
ppnode->_left = subL;
}
else
{
ppnode->_right = subL;
}
subL->_parent = ppnode;
}
}
//左右旋
void RotalLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
//进行旋转
RotalL(parent->_left);//以30为旋转点进行旋转
RotalR(parent);//以90为旋转点进行旋转
}
//右左旋
void RotalRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
//先进行右旋转,再左旋转
RotalR(subR);
RotalL(parent);
}
4 判断是否为红黑树
如果让你判断一棵树是否为红黑树,你会如何判断?
1)A同学:红黑树中有这样一条概念:最长路径不会超过最短路径的2倍,能否使用这个概念来进行判断?
是不可以的,因为如果这颗树的确满足最长路径不超过最短路径2倍这个概念,但是如果路径的颜色如果不满足红黑树的性质,那么这棵树显而易见也不是红黑树。所以这个思路是不可行的。
2)B同学:我们可以通过红黑树的性质来进行判断这颗树是一颗红黑树?
是可以的,因为若满足红黑树的性质,则可以判断其是一颗红黑树。并且性质中的每条路径的黑色节点数量相同,则可以限制最长路径不会超过最短路径俩倍这个概念。

判断一颗树是否为红黑树,我们主要满足红黑树性质的2、3、4条性质。
其中第2条性质很好判断:
cpp
//性质2:根节点是黑色的
if (_root->_col != BLACK)
{
return false;
}
性质3却存在一个小问题:如果一个节点是红色的,则它的俩个孩子节点必须是黑色的。
那么我们是那当前节点和它的孩子节点比较还是和它的父亲节点进行比较? 如果是孩子节点,那么我们首先需要判断它是否有孩子节点,其次,需判断是它的左孩子还是右孩子,这样写起来,代码就显得比较繁琐复杂。
因此,我们换种思路,如果当前节点和其父亲节点进行比较,那么该节点只有一个父亲节点,并且这样判断也可以满足不能有连续红色节点的性质,所以这样是比较优秀的解法。
cpp
//检查是否满足性质3:没有连续的红色节点
if (cur->_col == RED && cur->_parent->_col == RED)
{
return false;
}
但是性质4确实有些难度:
首先,我们可以使用递归来检验这棵树。
cpp
bool check(Node* cur)
{
if(cur == nullptr)
{
return true;
}
return check(cur->_left) && check(cur->_right);
}
bool IsRBTree()
{
if(_root == nullptr)
{
return true;
}
check(_root);
}
其次,我们可以将上面的我们写好的性质的代码添加进去。
cpp
bool check(Node* cur)
{
if(cur == nullptr)
{
return true;
}
//检查是否满足性质3:没有连续的红色节点
if (cur->_col == RED && cur->_parent->_col == RED)
{
return false;
}
return check(cur->_left) && check(cur->_right);
}
bool IsRBTree()
{
if(_root == nullptr)
{
return true;
}
//性质2:根节点是黑色的
if (_root->_col != BLACK)
{
return false;
}
check(_root);
}
最后,我们只需要满足性质4:每条路径有相同数量的黑色节点即可。

此时有这样一个思路:
1)首先我们计算一条路径中的黑色节点数量作为一个参考值;
2)递归每条路径并计算黑色节点的数量与这个参考值进行比较,若相等则正确,不相等则报错。
cpp
bool check(Node* cur,int BlackNum,int RefNum)
{
if (cur == nullptr)
{
//到达叶子结点时进行判定黑色节点数量与算出来的数量是否相同
//检查是否满足性质4
if (BlackNum != RefNum)
{
return false;
}
return true;
}
//检查是否满足性质3:没有连续的红色节点
if (cur->_col == RED && cur->_parent->_col == RED)
{
return false;
}
//如果当前节点cur为黑色节点,则黑色节点数量++
if (cur->_col == BLACK)
{
BlackNum++;
}
//该节点的左节点和右结点继续递归,没有加&,则表示每一层多少个,不受下一层的影响
return check(cur->_left, BlackNum, RefNum) && check(cur->_right,BlackNum,RefNum);
}
bool IsRBTree()
{
if (_root == nullptr)
{
return true;
}
//性质2:根节点是黑色的
if (_root->_col != BLACK)
{
return false;
}
//因为每条路径的黑色节点的个数是相同的
//所以我们可以设置一个参考值
int RefNum = 0;
Node* left = _root;
while (left)
{
//如果节点的颜色为黑色,则++
if (left->_col == BLACK)
{
RefNum++;
}
left = left->_left;
}
//通过check()进行检查
return check(_root,0,RefNum);
}
我们进行验证这个红黑树是否正确:
cpp
int main()
{
RBTree<int,int> r1;
int a[] = { 3,2,4,5,6,7,8,10,9,1 };
for (auto e : a)
{
r1.Insert(make_pair(e,e));
}
r1.Inorder();
cout << r1.IsRBTree() << endl;
}

验证正确。