前言:
上文我们提到了AVL树,AVL树是二叉搜索树的绝对平衡版本,是能够让我们的搜索效率最为接近 l o g 2 N log2N log2N的满足二叉搜索树性质的,但由于自身过多的旋转过程,导致一旦有频繁的修改操作时,其效率就会变得很低。因此,本次我们来介绍一种更为通用且好用的RB树,即大名鼎鼎的红黑树。
红黑树的性质与规则:
1.性质
如图,这就是一颗最为常规的红黑树,让我们仔细观察这颗树,我们会发现红黑树的节点是由红色和黑色两种颜色组成的,既然带了颜色,则这些颜色一定尤其特殊的含义,由此我们总结出我们红黑树的性质:
1. 每个结点不是红色就是黑色
2. 根节点是黑色的
3. 如果一个节点是红色的,则它的两个孩子结点是黑色的
4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均 包含相同数目的黑色结点
5. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
2.红黑树的特点:
那红黑树满足这样的规律是为了什么呢?
再一次观察上面的图片,我们会发现,红黑树的最长路径和最短路径之间不会超过2倍,这是因为黑色节点带着红色节点向下延申,黑色节点可以相邻,但红色节点不行,这就保证了红色和黑色可以穿插或者黑色相邻向后延申。因此哪怕是最长的路径,也是红色和黑色相同,而最短的路径则是全是黑色,这就保证了最长的路径的极限也就是最短路径的两倍。因此,虽然红黑树不是完全平衡的树,但通过这种方式提高了查找的效率,且不亚于AVL树。
红黑树的实现:
介绍完红黑树的性质后,现在我们就来实现一下红黑树吧。
1.红黑树节点的定义:
对于红黑树节点的定义,同AVL树大体相同,只不过将对应的平衡因子换成颜色变量即可,相应的创建代码如下:
enum Colour//颜色枚举体
{
BLACK,
RED
};
template<class K,class V>
struct RBTreeNode//红黑树节点类
{
RBTreeNode<K,V>* _left;
RBTreeNode<K,V>* _right;
RBTreeNode<K,V>* _parent;
Colour _col;
pair<K, V> _kv;
RBTreeNode(const pair<K,V>& kv)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_kv(kv)
,_col(RED)//控制节点颜色的成员变量
{}
};
在这里我们使用枚举体变量来代表颜色,然后由于涉及到旋转操作,为了方便我们同样封存一个parent指针指向上面的父亲。
这里值得注意的一个问题是,我在这里默认红黑树的节点的颜色为红色,那这是为什么呢?
观察这张图片,我们就会发现,红黑树的最低层都是红色节点,这是因为红黑树需要保证每一条路径上的黑色节点个数相同,但对于红色节点却没有这样的限制,因此,我们红色节点的多少是不影响整个红黑树的。
但黑色节点则不同,一旦插入一个黑色节点,这意味着其他路径上也要同样插入相同的黑色节点从而保证数量上是相同的,这样就大大降低了效率,因此,我们默认插入的新节点都是红色的,在后续的颜色平衡更新的过程中根据需要进行修改即可。
2.红黑树功能的实现:
1.基础框架:
在这里不多解释,二叉树的大体框架都是大差不差的,和AVL树差不多,他们的差别基本都体现在插入后的平衡过程中,框架如下:
template<class K,class V>
class RBTree//红黑树类
{
typedef struct RBTreeNode<K, V> Node;
public:
//在这里实现红黑树的一些功能
private:
Node* _root=nullptr;
}
2.红黑树的插入:!!!!
和AVL树一样,红黑树的插入也是最为精髓和关键的,在这里,我也将其分为两个方面:
1.节点的插入过程
2.颜色的平衡更新过程
1.节点的插入过程:
和常规的AVL树一样,前面的插入过程我不做过多的赘述,就是BS树的插入逻辑,代码如下:
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->_left;
}
else if (cur->_kv.first < kv.first)
{
parent = cur;
cur = cur->_right;
}
else
{
return false;
}
}
//新增节点给红色,默认构造出来了
cur = new Node(kv);
if (parent->_kv.first > kv.first)
{
parent->_left = cur;
cur->_parent = parent;
}
else
{
parent->_right = cur;
cur->_parent = parent;
}
//插入完毕,开始红黑节点调整
//这里便是对节点的颜色进行调整的过程
//有可能最后的节点为grandfather,此时根结点为红色,但要求根节点为黑色,因此不管是否为根节点,我们都最后将root改为黑色,这样就处理了所有情况
_root->_col = BLACK;
return true;
}
2.节点颜色的平衡更新:
在针对节点的插入完成后,我们便需要进行对节点颜色进行平衡更新,以满足让其符合我们前面所说的红黑树的性质。
和AVL树一样,我们同样采取向上遍历更新的策略,同样以parent指针作为我们向上更新的核心指针变量,因此基本的框架如下:
while (parent&&parent->_col == RED)//这里要加上parent的判断,否则会出现空指针访问报错,先检验parent是否指向空,否则后续的parent->_col很有可能出现空指针访问
{
//在这里进行对不同情况的红黑树插入进行平衡调整
}
在这里值得注意的两个问题是:
1.我们循环的条件是parent&&parent->_col==RED,先不说为什么parent一定要是红色的,由于我们这里需要对parent内部的变量进行访问,因此首先要保证parent不是指向空指针,也就是说来到根节点的情况,否则不加上对parent本身是否为空指针的判断而是直接访问里面的数据,就很有可能会出现空指针访问的问题。
2.其次我们再来说一说为什么我们要求我们的parent一定要是红色呢?
如图:
在我这个简易的图中,如果parent为黑色的话,我们由于默认的cur为红色的,这样是不需要调整的,直接插入就好。但如果parent为红色,cur也为红色,这就与 “根节点若为红色,子节点必须为黑色” 的规则所冲突了,因此我们此时就需要对这种情况进行调整,所以我们的循环条件是parent为红色而不是黑色,这是我们要进行调整的根本逻辑,要搞清楚
颜色更新的情况及其对应的方法:
对于红黑树的颜色更新,我们只关注其四个对应的节点指针,如图:
如图,即插入的节点cur,它对应的父亲parent,对应的祖父节点grandfather,以及祖父的另一个子节点叔叔节点uncle,由于在这里我们默认parent,cur,grandfaher的颜色是固定的,因此对应的种类就与uncle的颜色有关,由此我们遇到的情况如下:
第一类情况:uncle为grandfather的右节点
上面已经说道,我们是需要一个uncle节点的,然后根据uncle节点的不同位置来调整,因此基础的大框架为:
Node* grandfather = parent->_parent;
if (parent == grandfather->_left)//父亲在左边,即叔叔在右边的情况
{
Node* uncle = grandfather->_right;
//针对uncle不同情况的修改的位置
//
}
情况一: cur为红,p为红,g为黑,u存在且为红
如图:
这是整个红黑树的颜色修改中最为简单的一种。
针对这种情况,我们采取的策略是,直接将g改为红色,将p和u改为黑色,如下:
这样就再一次符合了红黑树的性质,相应的代码如下:
if (uncle&&uncle->_col == RED)//uncle不为空且为红色的情况下
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
此时由于我们的grandfather的颜色进行了修改变成了红色,因此很可能需要向上接着修改,因为很有可能grandfather的上一级父节点为红色的,因此这种情况下是不会退出循环的,要接着向上修改。
情况二: cur为红,p为红,g为黑,u不存在/u存在且为黑
如图:
1.u为黑色
2.u不存在
通过上面的图片,我们可以观察出这样的一个结论:
针对这种情况,我们就需要使用旋转来解决了,涉及到旋转,就涉及到cur的位置问题,从而决定是双旋转还是单旋转,和AVL的平衡调整思路相同如下:
p为g的左孩子,cur为p的左孩子,则进行右单旋转;
p为g的左孩子,cur为p的右孩子,则先进行左单旋转,再进行右单旋转
p、g变色–p变黑,g变红
我们最后达到的效果如下:
图一:
注意:在这张图片里,你可能会觉得黑色节点不平衡,但注意cur其实就是上一个调整的grandfather,也就是说,cur的两个子节点都是黑色的,这样就保证了黑色节点的颜色问题。
图二:
代码如下:
else//叔叔节点为空或者黑色的情况
{
if (cur == parent->_left)//对cur的左右位置进行讨论,可能单旋,也可能双旋,这里为单旋
{
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else//双旋
{
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;//调整一次后,对上面就影响了,根节点为黑色,无论上面是什么颜色都不冲突,且不影响整体的黑色节点个数,因此直接跳出循环不要再进行了.
void RotateL(Node* parent)//左单旋旋转调整
{
Node* sub1 = parent->_right;
Node* sub2 = sub1->_left;
Node* PPparent = parent->_parent;
parent->_right = sub2;
if (sub2)//注意这里,可能涉及到sub2为空的情况,比如h==0,此时要对sub2是否为空进行一次判断
{
sub2->_parent = parent;
}
sub1->_left = parent;
parent->_parent = sub1;
if (_root == parent)
{
_root = sub1;
sub1->_parent = nullptr;
}
else
{
if (PPparent->_right == parent)
{
PPparent->_right = sub1;
}
else if (PPparent->_left == parent)
{
PPparent->_left = sub1;
}
sub1->_parent = PPparent;
}
}
void RotateR(Node* parent)//右单旋旋转调整
{
Node* sub1 = parent->_left;
Node* sub2 = sub1->_right;
Node* PPparent = parent->_parent;
parent->_left = sub2;
if (sub2)
{
sub2->_parent = parent;
}
sub1->_right = parent;
parent->_parent = sub1;
if (_root == parent)
{
_root = sub1;
sub1->_parent = nullptr;
}
else
{
if (PPparent->_right == parent)
{
PPparent->_right = sub1;
}
else if (PPparent->_left == parent)
{
PPparent->_left = sub1;
}
sub1->_parent = PPparent;
}
}
注意这里的一个细节,旋转操作后,由于根节点被更新为BLACK,所以不需要再向上调整颜色了,直接跳出循环即可。
第二类情况:uncle为grandfather的左节点
针对第二类情况,其本质就是第一类情况的镜面,因此在这里我不做再一次的阐述了,根据第一类情况去照葫芦画瓢就可以,代码如下:
else//叔叔在左边的情况
{
Node* uncle = grandfather->_left;
if (uncle && uncle-> _col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else
{
if (cur == parent->_right)
{
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;//这里如果不停止,则父子关系后续是全乱的,后续在循环就会出现大问题,而且由于改为黑色节点已经没必要再向上修改了,及时退出即可
}
}
void RotateL(Node* parent)//左单旋旋转调整
{
Node* sub1 = parent->_right;
Node* sub2 = sub1->_left;
Node* PPparent = parent->_parent;
parent->_right = sub2;
if (sub2)//注意这里,可能涉及到sub2为空的情况,比如h==0,此时要对sub2是否为空进行一次判断
{
sub2->_parent = parent;
}
sub1->_left = parent;
parent->_parent = sub1;
if (_root == parent)
{
_root = sub1;
sub1->_parent = nullptr;
}
else
{
if (PPparent->_right == parent)
{
PPparent->_right = sub1;
}
else if (PPparent->_left == parent)
{
PPparent->_left = sub1;
}
sub1->_parent = PPparent;
}
}
void RotateR(Node* parent)//右单旋旋转调整
{
Node* sub1 = parent->_left;
Node* sub2 = sub1->_right;
Node* PPparent = parent->_parent;
parent->_left = sub2;
if (sub2)
{
sub2->_parent = parent;
}
sub1->_right = parent;
parent->_parent = sub1;
if (_root == parent)
{
_root = sub1;
sub1->_parent = nullptr;
}
else
{
if (PPparent->_right == parent)
{
PPparent->_right = sub1;
}
else if (PPparent->_left == parent)
{
PPparent->_left = sub1;
}
sub1->_parent = PPparent;
}
}
红黑树的检验:
红黑树的检验,我们需要检验的关键就和AVL树完全不同,根据我们红黑树的规则:
1.每一条路径上的黑色节点的数量必须相同
2.红色父节点的两个子节点必须是黑色节点
这两条规则便是我们检验的关键,因此我们的大体思路如下:
首先对一棵红黑树的任意一条路径搜索到底,统计出这条路径上的黑色节点的个数,作为其余每一条红黑树路径上的参考值,然后利用一个递归函数对每一条路径上的黑色节点进行统计,若全部相同且不存在父子都为红色节点的情况,则证明为红黑树。
因此我们大体的代码如下:
bool Check(Node* root, int blacknum,const int refVal)//节点检查函数,注意这里要传值返回而不是传地址,这是因为保证每一层的递归时blacknum的数据不变,保持原来的值,而是把之前的值传到下一个递归去,这样都相互不影响
{
//对每一条路径进行递归判断比较
}
bool IsBalance()//判断是否为红黑树
{
if (_root == nullptr)
{
return true;
}
if (_root->_col == RED)//若根节点为红色节点,直接不符合红黑树要求根节点为黑色的规则,则此树必不可能是红黑树
{
return false;
}
//首先先随便找一树的一个分支,在这里我找的是树的最左分支,对其一直遍历,计算出一个黑色节点的参考值,用来对后续的红黑树中黑色节点数量是否相等做出判断
int refVal = 0;//黑色节点参考值,倘若不同就说明红黑树出现问题了,黑色节点不相同
Node* cur = _root;
while (cur)
{
if (cur->_col == BLACK)
{
refVal++;
}
cur = cur->_left;
}
int blacknum = 0;
return Check(_root,blacknum,refVal);
}
但这里我们就再一次遇到一个问题,针对黑色节点的个数判断是简单的,但怎样针对当前父节点的两个子节点同时判断其为红呢,要知道我们这里涉及到的情况太多了,父亲节点为红色的时候,两个子节点不能有红色,父亲节点为黑色的时候,两个子节点可以为红也可以为黑?
既然顺着向下不行,那我们就反过来向上就好,根据规则,我们要求父子两个不能相邻都为红色,那就检验当前节点和它的父亲节点是否都为红色节点,一旦出现,说明红色节点相邻,不符合红黑树的规则,直接返回false即可。
代码如下:
bool Check(Node* root, int blacknum,const int refVal)//节点检查函数,注意这里要传值返回而不是传地址,这是因为保证每一层的递归时blacknum的数据不变,保持原来的值,而是把之前的值传到下一个递归去,这样都相互不影响
{
if (root == nullptr)
{
if (blacknum != refVal)
{
cout << "存在黑色节点数量不相等的路径" << endl;
return false;
}
return true;
}
if (root->_col == RED && root->_parent->_col == RED)//由于我们向下检查子节点的时候,子节点的情况太多不好检查,我们要检查两个节点是否为红,非常麻烦,所以我们采取向上检查的方式来检查子和父节点是否都为红色即可,因此我们反其道而行之向上检查,即检查当前节点和父节点是否都为红色,若都为红色则这个树不是红黑树
{
cout << "有连续的红色节点" << endl;
return false;
}
if (root->_col == BLACK)
{
blacknum++;
}
return Check(root->_left,blacknum,refVal) && Check(root->_right,blacknum,refVal);
}
由此,我们就可以完成对红黑树的检验了。
红黑树的性能分析:
红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O(
l
o
g
2
N
log_2 N
log2N),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。
比如我们接下来要模拟实现的map set容器,其底层就是依托红黑树
总结:
通过本篇文章的讲解,我们对于二叉树的进阶结构有了更深的理解,至此,BS,AVL,RB三种进阶的二叉树我们就都学会了运用和理解,在实际的项目中,我们根据实际情况的需要要灵活的去使用和运用,希望通过我的讲解,能让你对RB树等各种二叉树结构有了更深的理解。