在之前对二叉搜索树(二叉排序树)的插入进行优化后,查找效率能基本维持(O(log_2N))AVL树。但是我们可以发现,AVL树的要求其实有点严格,那就是任何一个结点的左右子树如果高度差超过1,就要进行旋转调节。旋转调节的次数太多会影响效率。那么有没有特化一下AVL树使其旋转次数会相对变少呢?
红黑树就是针对于AVL树的特化。在之前AVL树的插入实现是利用平衡因子控制,任意结点左右子树高度差不超过1,而红黑树是利用颜色进行控制,任意结点高度差不超过短的2倍。
AVL树的插入实现博客在这里哦~
【C++】AVL树的插入实现_柒海啦的博客-CSDN博客
链表二叉树的基本结构和实现可以看这篇C语言博客的哦~
用c语言实现一个简单的链表二叉树_柒海啦的博客-CSDN博客_创建链式二叉树c语言
话不多说,我们直接开始吧~
(紬壮壮可爱捏~~~)
目录
一、红黑树的结构
1.红黑树的结点
代码实现:
2.红黑树的性质
二、红黑树的插入实现
1.插入新结点
代码实现:
2.不旋转
2.1不变色
2.2变色
变色操作:
代码实现:
3.变色+旋转
3.1变色+单旋
3.11左单旋
ABC模块介绍:
左单旋过程:
左单旋代码实现:
3.12右单旋
右单旋过程:
右单旋代码实现:
3.2变色+双旋
3.21左右双旋
3.22右左双旋
变色+旋转代码实现:
4.总结
三、红黑树的验证实现
代码实现:
四、综合代码
因为同样是平衡二叉搜索树,旋转是必须的,所以需要三叉链。其次,由于是利用颜色进行调整红黑树的性质,所以自然有颜色成员。颜色的值可以利用枚举去举例,当然也可以设置宏或者const成员去代替, 颜色分为红色和黑色。
初步实现红黑树的时候,里面存的值为pair key-value结构,具体的介绍可以看博主AVL树里的介绍。所以结点也需要定义成模板类型K-V。
综上,红黑树的结点构成如下:
Node* 三叉链:
指向左子树的指针 left
指向右子树的指针 right
指向父结点的指针 parent(没有哨兵位的话,当此结点为根结点时,父节点为空)
Color 颜色成员:
红色 RED(0)
黑色 BLACK(1)
(括号内数字表示此数字对应代表红/黑)
pair k-val数据:
[pair]->frist K
[pair]->second V
([]表示pair的具体的一个实例化对象)
在结点类型中的构造函数中除了传入基本的key-val外,还需要传入一个颜色。新结点默认红色。为什么默认红色呢?等讲完了红黑树的性质就理解了:
// 颜色可以使用枚举
enum Color
{
Red,
Black
};
// 红黑树结点结构
template
struct RBTreeNode
{
// 三叉链控制-方便之后的旋转
RBTreeNode* _left;
RBTreeNode* _right;
RBTreeNode* _parent;
pair _kv;
Color _col; // 红黑树控制性质元素 - 颜色
RBTreeNode(const pair& data, Color col = Red)
:_left(nullptr), _right(nullptr), _parent(nullptr)
,_kv(data), _col(col)
{}
};
对于红黑树能够通过颜色控制任意结点两条子树高度差不超过短的二倍原因有五:
性质1:结点只能是红色和黑色
性质2:根节点为黑色
性质3:每个红色结点的子结点为黑色
性质4:从任意结点到每个叶子的路径上黑色结点相同
性质5:nullptr可以视作黑色结点
性质1的体现在我们定义枚举。性质3的意思也就是不允许出现连续的红色结点。性质5其实也就表示了空树也是一个红黑树。
那么为什么保证这些性质就能保持任意结点两条子树高度差不超过短的二倍呢?原因就是在极端条件下,一条路径上全是黑色结点,那么在另一条路径上最大也就是此黑色结点的二倍。因为只能黑-红-黑的进行排布。并且也是每条路径上的。
如上图就是展示了一个1到9按照其顺序插入的红黑树。
下面,围绕着维护红黑树的性质,进行红黑树的插入思路和算法实现:
首先,我们可以将其红黑树的框架搭建起来。
成员为_root,类型为上述二叉树结点的指针,用来指向根结点。其次就是我们要开始的插入方法:
template
class RBTree
{
typedef RBTreeNode Node;
public:
// 插入实现
private:
Node* _root = nullptr; // 默认构造即可,就会自动初始化为空指针(C++11)
};
接下来,开始介绍我们的Insert方法,目前初步实现先返回bool值,因为属于不准数据冗余版本,所以插入pair结构时如果和插入的pair的frist冲突就会返回false。
首先自然是先插入结点。插入结点那一套就按照二叉搜索树的那一套走。首先定义两个游标,一个指向根结点(c),一个记录其父结点(p)。然后比较插入k-val结点k值,大就往右走,小就往左走。直到走到空,根据父结点的k值与插入piar的k值进行比较就知道该插入到左子树还是右子树了,但是如果是第一次插入就要特别处理。另外别忘了三叉链,所以新结点的父节点需要指向其父。
bool Insert(const pair& kv)
{
// 同理,先按照搜索二叉树基本的走
Node* p = nullptr;
Node* c = _root;
while (c)
{
if (kv.first > c->_kv.first)
{
p = c;
c = c->_right;
}
else if (kv.first < c->_kv.first)
{
p = c;
c = c->_left;
}
else // 不允许数据冗余
{
return false;
}
}
// 首先插入
c = new Node(kv);
// 小心一开始就为空树
if (p == nullptr)
{
_root = c;
c->_col = Black; // 性质2根节点为黑
return true;
}
if (kv.first > p->_kv.first)
{
p->_right = c;
}
else p->_left = c;
c->_parent = p;
// 变色+旋转处理
return true;
}
另外如果是第一次插入,那么也就需要满足性质2。因为默认插入结点为红色。
为什么默认插入结点设置为红色呢?现在就可以解释了:
可以看到左边那张图是新插入黑色结点,右边那张图是新插入红色结点。
很明显,根据性质,可以看到左边那张图违反的是性质4,但是右边那张图违反的是性质3。这两条性质哪条造成的影响比较严重呢?自然是性质4,每条路径。但是性质3只是在局部,不会影响其他路径的。为了方便起见,自然选择新插入红色结点。
现在根结点第一次插入的问题解决了,现在先来讨论插入不进行旋转的情况:
不变色很简单,就是如果此时p(父结点)本身就是黑色结点,那么此时新插入的红色结点不会违反任何性质,自然也就不用做什么处理。
如果此时P为红结点,所以可以肯定P就不是根结点,不是根节点的话那么P必然会存在父亲节点。P的父亲节点设置为G,如果G的另一个孩子(除开P结点)存在,设置为U。
前提:P(父结点)此时为红色结点,并且其U(父の兄弟结点)存在且为红色结点,那么此时只需要变色即可,但是需要向上调整。
我们分别对P结点和U结点变成黑色(红->黑),G结点变成红色(黑->红),然后向上继续调整就可以解决问题。
可以看到,为什么只变色就可以解决问题呢?那是因为此时G结点联通的是两个路径。如果将G结点的黑色分别给其U和P(满足前提)不就可以维持性质4了吗,然后也能将连续的红色结点(性质3)给破除。
下面解释向上调整的原因(结合两个情景查看--错误请大佬指正~~)
但是还是没有结束。可以看到虽然变色了,但是两个情景最后的处理却不一样,这就是需要向上调整的原因。因为此时G结点变为红了,在这个局部的区域里我们不知道G结点的具体信息,只知道G原来是黑结点,那么我们就不知道G的P结点存不存在,是黑结点还是红结点。不存在(情景1)就表示此时G就是根结点,就需要变回黑色结点;存在且为黑(情景2)那么此时插入调整结束;如果存在且为红呢?那么此时是不是又破坏规则了?就要再来一次插入调整,这也就是需要向上调整的原因。
所以在变色结束后,将变成红色的G结点当成新的插入结点C,P在给对应G的父节点即可,向上继续调整。
前提:P(父结点)此时为红色结点,并且其U(父の兄弟结点)存在且为红色结点
操作:1.P结点和U结点变成黑色(红->黑),G结点变成红色(黑->红)2.G结点当成新的插入结点C,P在给对应G的父节点
// 调节 == 变色+旋转
while (p && p->_col == Red) // 之前的G作为C,那么其P可能不存在
{ // 进来g就一定存在
Node* g = p->_parent;
Node* u = nullptr;
if (g->_left == p) u = g->_right;
else u = g->_left;
if (u && u->_col == Red) // 只变色
{
u->_col = Black;
p->_col = Black;
g->_col = Red;
c = g; // 注意,如果此时g为根结点,那么出去就不为black了,需要做改变
p = c->_parent;
}
// ......
}
_root->_col = Black; // 结合情景1
可以看到,上面的情况就是P为黑色结点和P为红色结点且U结点存在且为红结点(G是否存在上面已经解释了哦~)
那么如果U结点不存在或者U结点为黑色结点呢?会出现什么情况呢?
前提:如果此时P结点为红色结点,U不存在或者U结点为黑色结点,并且P是G的一端子树,C也是P的同一端子树,引发变色+单旋。
这里的同一端子树如何理解?即P是G的左子树,C也是P的左子树,或者P是G的右子树,C也是P的右子树。那么这里也就要分两种单旋来实现上面两种不同的情况了:
此时P是G的右子树,C是P的右子树。
这里来解释一下图中ABD中的含义:(同样的适用于下面)
A:要么存在U结点,要么不存在。存在的话此子树(G上存在一条连接线,如果其父存在就是子树,不存在就是整棵树)的任意一条路径应该都有两个黑色结点(性质4+U结点为黑)
B:U不存在话,B就是一个空树(性质3);如果U存在,因为此路径才一个黑色结点(此子树/树根节点),所以B就是一个根结点为黑色的并且只存在一个黑色结点的子树(性质4+U结点为黑),这种子树有四种情况:
D:U如果不存在,那么D就不存在(性质4);U如果存在,那么就应该是在C的子树中插入的,然后经过向上调整到达此时的C:
此时就可以抽象出左单旋的情况,可以发现,首先变色:P变为黑色,G变为红色,U不管其存不存均不变色。然后就对G结点进行左单旋即可。因为此时的目的和AVL树类似,因为此时虽然满足性质3了,但是性质4却不满足了,观察抽象图,我们只需要将G结点左子树降下高度即可。
那么左单旋是如何旋转的呢?
此时就可以利用二叉搜索树的特性,因为我们是想让此子树的根结点以及左子树旋转到右子树的第一个节点P下去,因为右子树都是大于根加左的值的,所以旋转到P的左子树后,P原本的左子树(B)就应该待在G的右子树,然后在经过一系列设置三叉链结点即可:
我们可以利用下面抽象图来解释左单旋旋转过程:
如图所示,一共要修改的大概需要6个指针。蓝色圆圈表示红色结点或者黑色结点,方框表示可能存在的结点或子树。
但是修改六个结点的时候就需要注意subRL和pp。首先subRL有可能不存在,不存在3就不可执行;如果node此时为根结点(整棵树的),那么pp就不存在,6就不可执行。细节一定要把握住~~
private:
void RotateL(Node* node)
{
Node* subR = node->_right;
Node* subRL = subR->_left;
node->_right = subRL;
if (subRL) subRL->_parent = node;
subR->_left = node;
Node* pp = node->_parent;
if (pp == nullptr) _root = subR; // 此时修改的node为根结点 subR要代替成为根结点
else
{
if (pp->_left == node) pp->_left = subR;
else pp->_right = subR;
}
subR->_parent = pp;
node->_parent = subR;
}
综上,变色+左单旋就可以解决此种情况的问题,因为此时变完后此子树/树的原本的性质都没有违背,并且黑色结点和原本的一样,此时就不需要继续向上调整了,直接break退出即可。
此时P是G的左子树,C是P的左子树。
和左单旋的情况类似,只是变换了方向,详细可以查看左单旋的讲解,这里只放出抽象图和右单旋的抽象图:
此时就可以抽象出右单旋的情况,可以发现,首先变色:P变为黑色,G变为红色,U不管其存不存均不变色。然后就对G结点进行右单旋即可。
void RotateR(Node* node)
{
Node* subL = node->_left;
Node* subLR = subL->_right;
node->_left = subLR;
if (subLR) subLR->_parent = node;
subL->_right = node;
Node* pp = node->_parent;
if (pp == nullptr) _root = subL; // 此时修改的node为根结点 subR要代替成为根结点
else
{
if (pp->_left == node) pp->_left = subL;
else pp->_right = subL;
}
subL->_parent = pp;
node->_parent = subL;
}
可以发现,上面在U不存在或者存在且为黑的前提下,对C究竟插入P的子树左右是否和P为G的左右子树是否一致。那么如果不一致呢?此时就要引发变色+双旋了。
前提:如果此时P结点为红色结点,U不存在或者U结点为黑色结点,并且P是G的一端子树,C也是P的另一端子树,引发变色+双旋。
另一端子树如何理解?比如P是G的右子树,但是C是P的左子树;P是G的左子树,但是C是P的右子树。同样的,也要进行分情况讨论:
此时P是G的左子树,C是P的右子树:
此时就可以抽象出来左右双旋的情况:首先对P结点进行一个左单旋,此时就可以发现旋转后就变成了变色+单旋的情况,然后C结点变成黑色,G结点变成红色,对G结点进行一个右单旋即可完成操作。
此时P是G的右子树,C是P的左子树:
此时就可以抽象出来右左双旋的情况:首先对P结点进行一个右单旋,此时就可以发现旋转后就变成了变色+单旋的情况,然后C结点变成黑色,G结点变成红色,对G结点进行一个左单旋即可完成操作。
else if (g->_left == p)
{
if (p->_left == c)
{
p->_col = Black;
g->_col = Red;
RotateR(g);
}
else
{
// 先将p结点左单旋下去,变成上面单旋的情况
RotateL(p);
c->_col = Black;
g->_col = Red;
RotateR(g);
}
break; // 此时直接退出循环即可
}
else
{
if (p->_right == c)
{
p->_col = Black;
g->_col = Red;
RotateL(g);
}
else
{
RotateR(p);
c->_col = Black;
g->_col = Red;
RotateL(g);
}
break;
}
综上,对于插入的情况可以综合如下几种:
1.不变色不旋转:
此时p(父结点)本身就是黑色结点或者此时的第一次插入。
2.只变色:
前提:P(父结点)此时为红色结点,并且其U(父の兄弟结点)存在且为红色结点
操作:1.P结点和U结点变成黑色(红->黑),G结点变成红色(黑->红)2.G结点当成新的插入结点C,P在给对应G的父节点。
3.变色+旋转:
单旋:前提:此时P结点为红色结点,U不存在或者U结点为黑色结点,并且P是G的一端子树,C也是P的同一端子树
左单旋:此时P是G的右子树,C是P的右子树
首先变色:P变为黑色,G变为红色,U不管其存不存均不变色。然后就对G结点进行左单旋即可
右单旋:此时P是G的左子树,C是P的左子树。
首先变色:P变为黑色,G变为红色,U不管其存不存均不变色。然后就对G结点进行右单旋即可
双旋:前提:此时P结点为红色结点,U不存在或者U结点为黑色结点,并且P是G的一端子树,C也是P的另一端子树
左右双旋:此时P是G的左子树,C是P的右子树
首先对P结点进行一个左单旋,然后C结点变成黑色,G结点变成红色,对G结点进行一个右单旋
右左双旋:此时P是G的右子树,C是P的左子树
首先对P结点进行一个右单旋, 然后C结点变成黑色,G结点变成红色,对G结点进行一个左单旋。
4.字母解释:
C为当前插入结点-
P为其父节点-
G为P的父节点-
U为G的另一个孩子,和P是兄弟-null
经历了九九八十一难后,我们终于完成了对红黑树的插入实现。实现了是一回事,但是要如何证明此树是红黑树呢?
在之前实现AVL树中,我们利用了高度差进行判断,那么对于红黑树来说这种方法奏效吗?自然不行,因为即使验证出来也不知道它究竟是红黑树还是AVL树呀~所以,我们必须针对于红黑树的性质进行验证。
性质1:结点只能是红色和黑色
性质2:根节点为黑色
性质3:每个红色结点的子结点为黑色
性质4:从任意结点到每个叶子的路径上黑色结点相同
性质5:nullptr可以视作黑色结点
性质有5,其一enum就可以验证出来,其二需要验证,也就是第一次进来的时候判断根节点是否为空,为空就是,不为空判断其颜色,为红就输出错误返回false。那么接下来就便可以递归调用了。验证性质3在递归的过程中分别比较其父和自己的结点颜色即可,验证性质4比较麻烦。
性质4是要每条路径中的黑色结点一致。我们首先不知道一个路径上黑色结点的个数,那么首先可以先遍历左结点,把最左的那条路径上的黑色结点的个数给统计出来,然后以此为基准值递归去比较。当然也可以传入引用,在分别递归每条路径的时候进行统计,第一次到尾(nullptr)将其变为基准值即可。
// 验证是否红黑树
bool isValidRBTree()
{
Node* root = _root;
if (root == nullptr) return true; // 空树也是红黑树
if (root->_col != Black)
{
cout << "根节点必须为黑色" << endl;
return false; // 根结点为黑
}
int k = 0, count = 0; // 一个为当前路径的黑色结点值,一个是基准值(以第一个跑的路径统计的黑色结点为准)
return _isValidRBTree(root, k, count);
}
private:
bool _isValidRBTree(Node* node, int k, int& count) // 注意基准值是引用
{
if (node == nullptr)
{
if (count == 0) count = k;
else if (count != k)
{
cout << "存在路径上的黑色结点不等" << endl;
return false; // 不等,不符合每条路径上黑色结点一致
}
return true;
}
if (node->_col == Black) k++;
Node* p = node->_parent;
if (p && p->_col == Red && node->_col == Red)
{
cout << "存在一个路径上两个红色结点" << endl;
return false; // 连续红,不符合性质
}
return _isValidRBTree(node->_left, k, count) && _isValidRBTree(node->_right, k, count);
}
综上,代码总体实现如下:--欢迎大佬指正错误~~~红黑树的相关实现还未结束哦~
// RBTree.h
#pragma once
// 红黑树代码实现 - 控制好红黑树的性质就可以维持红黑树的特性:
// 红黑树的特性:1.根节点为黑色。2.不存在连续的红节点。3.每条路径上的黑色结点个数一致。
// 颜色可以使用枚举
enum Color
{
Red,
Black
};
// 红黑树结点结构
template
struct RBTreeNode
{
// 三叉链控制-方便之后的旋转
RBTreeNode* _left;
RBTreeNode* _right;
RBTreeNode* _parent;
pair _kv;
Color _col; // 红黑树控制性质元素 - 颜色
RBTreeNode(const pair& data, Color col = Red) // 新插入元素默认设置为红色 -- 违反第二个规则就好了,就进行调整。第三个规则代价大,不好调整
:_left(nullptr), _right(nullptr), _parent(nullptr)
,_kv(data), _col(col)
{}
};
template
class RBTree
{
typedef RBTreeNode Node;
public:
void InOrder()
{
// 中序非递归
stack s;
Node* cur = _root;
while (cur || !s.empty())
{
if (cur)
{
s.push(cur);
cur = cur->_left;
}
else
{
Node* tmp = s.top();
cout << tmp->_kv.first << ":" << tmp->_kv.second << " ";
cur = tmp->_right;
s.pop();
}
}
cout << endl;
}
bool Insert(const pair& kv)
{
// 同理,先按照搜索二叉树基本的走
Node* p = nullptr;
Node* c = _root;
while (c)
{
if (kv.first > c->_kv.first)
{
p = c;
c = c->_right;
}
else if (kv.first < c->_kv.first)
{
p = c;
c = c->_left;
}
else // 不允许数据冗余
{
return false;
}
}
// 首先插入
c = new Node(kv);
// 小心一开始就为空树
if (p == nullptr)
{
_root = c;
c->_col = Black; // 性质2根节点为黑
return true;
}
if (kv.first > p->_kv.first)
{
p->_right = c;
}
else p->_left = c;
c->_parent = p;
// 调节 == 变色+旋转
while (p && p->_col == Red) // 之前的G作为C,那么其P可能不存在
{ // 进来g就一定存在
Node* g = p->_parent;
Node* u = nullptr;
if (g->_left == p) u = g->_right;
else u = g->_left;
if (u && u->_col == Red) // 只变色
{
u->_col = Black;
p->_col = Black;
g->_col = Red;
c = g; // 注意,如果此时g为根结点,那么出去就不为black了,需要做改变
p = c->_parent;
}
else if (g->_left == p)
{
if (p->_left == c)
{
p->_col = Black;
g->_col = Red;
RotateR(g);
}
else
{
// 先将p结点左单旋下去,变成上面单旋的情况
RotateL(p);
c->_col = Black;
g->_col = Red;
RotateR(g);
}
break; // 此时直接退出循环即可
}
else
{
if (p->_right == c)
{
p->_col = Black;
g->_col = Red;
RotateL(g);
}
else
{
RotateR(p);
c->_col = Black;
g->_col = Red;
RotateL(g);
}
break;
}
}
_root->_col = Black;
return true;
}
// 验证是否红黑树
bool isValidRBTree()
{
Node* root = _root;
if (root == nullptr) return true; // 空树也是红黑树
if (root->_col != Black)
{
cout << "根节点必须为黑色" << endl;
return false; // 根结点为黑
}
int k = 0, count = 0; // 一个为当前路径的黑色结点值,一个是基准值(以第一个跑的路径统计的黑色结点为准)
return _isValidRBTree(root, k, count);
}
private:
bool _isValidRBTree(Node* node, int k, int& count) // 注意基准值是引用
{
if (node == nullptr)
{
if (count == 0) count = k;
else if (count != k)
{
cout << "存在路径上的黑色结点不等" << endl;
return false; // 不等,不符合每条路径上黑色结点一致
}
return true;
}
if (node->_col == Black) k++;
Node* p = node->_parent;
if (p && p->_col == Red && node->_col == Red)
{
cout << "存在一个路径上两个红色结点" << endl;
return false; // 连续红,不符合性质
}
return _isValidRBTree(node->_left, k, count) && _isValidRBTree(node->_right, k, count);
}
// 左单旋、右单旋
void RotateL(Node* node)
{
Node* subR = node->_right;
Node* subRL = subR->_left;
node->_right = subRL;
if (subRL) subRL->_parent = node;
subR->_left = node;
Node* pp = node->_parent;
if (pp == nullptr) _root = subR; // 此时修改的node为根结点 subR要代替成为根结点
else
{
if (pp->_left == node) pp->_left = subR;
else pp->_right = subR;
}
subR->_parent = pp;
node->_parent = subR;
}
void RotateR(Node* node)
{
Node* subL = node->_left;
Node* subLR = subL->_right;
node->_left = subLR;
if (subLR) subLR->_parent = node;
subL->_right = node;
Node* pp = node->_parent;
if (pp == nullptr) _root = subL; // 此时修改的node为根结点 subR要代替成为根结点
else
{
if (pp->_left == node) pp->_left = subL;
else pp->_right = subL;
}
subL->_parent = pp;
node->_parent = subL;
}
private:
Node* _root = nullptr;
};