目录
一、红黑树的概念
红黑树是一种特殊的二叉搜索树。
其特点是:节点中含有一个记录颜色的标记位,可以是Red或Black中的任意一种颜色。通过对每条从根节点到空节点的路径上的节点的颜色进行限制,红黑树确保没有一条路径会比其他路径长出两倍(最长路径不会超过最短路径的两倍),因此红黑树的高度接近平衡。
二、红黑树的特性
2、根节点必须是黑色
3、不能有连续的红色节点
4、从根节点到空节点的每条路径中,黑色节点的数量都相同
5、空节点视为黑色节点
通过特性3和特性4,就可以保证红黑树中最长路径不会超过最短路径的两倍。
首先根据特性4,我们假设每条路径的黑色节点的数量都为N。
则当路径中不存在红色节点时,就是最短路径,其长度为N。
而要使路径最长,则要尽可能多的往N个黑色节点中插入红色节点。
而根据特性三不能有连续的红色节点这一原则,我们最多在两个黑色节点中插入一个红色节点。
因此,一条路径上红色节点最多的情况就是一黑一红相间,因此最长路径的长度最多为2N。
因此,红黑树上所有路径的长度都介于[N,2N]之间,故没有一条路径会比其他路径长出两倍。
三、红黑树节点的定义
与普通二叉搜索树节点定义不同的是,我们需要在节点中增加一个用于记录颜色的标记位。另外,还需要一个指向父节点的指针,方便我们向上层遍历。
节点定义的代码如下:
//用枚举表示颜色
enum Color
{
Red,
Black
};
template<class valueType>
struct RBTreeNode
{
//构造函数
RBTreeNode(const valueType& val)
:_val(val)
,_color(Red)//新节点的颜色默认为红色
,_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
{
}
//成员
valueType _val;//数据
Color _color;//颜色
RBTreeNode<valueType>* _left;//左孩子
RBTreeNode<valueType>* _right;//右孩子
RBTreeNode<valueType>* _parent;//父亲
};
我们在创建新节点时,默认将新节点的颜色设置成红色。
这是因为新节点的颜色如果是黑色,必然会使得当前路径的黑色节点数量比其他路径多一个。也就必然会违背红黑树的第四条特性。
而新节点的颜色如果是红色,只有当父节点的颜色也是红色时才会违背特性三。
权衡利弊以后,我们将新节点默认设置为红色,并且将其视为一条不能违背的规则。
四、红黑树的插入
1、红黑树的插入步骤
红黑树的插入分为以下三个步骤:
1、根据二叉搜索树的特性找到插入位置
2、创建新节点并链接到父节点下面
3、对异常结构进行调整
查找和插入的代码逻辑如下:
if (_root == nullptr)//当前是空树
{
_root = new TreeNode(val);
_root->_color = Black;//根节点必须是黑色
return true;
}
//不是空树,先根据二叉搜索树的性质找到插入位置
TreeNode* parent = nullptr, * cur = _root;
while (cur)
{
if (cur->_val < val)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_val > val)
{
parent = cur;
cur = cur->_left;
}
else//节点已存在
{
return false;
}
}
//cur为空,抵达插入位置,创建新节点并链接到parent下面
cur = new TreeNode(val);
cur->_parent = parent;
if (parent->_val < val)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
//如果父亲节点不存在或父亲节点的颜色为黑色,则插入完成
//如果父亲节点的颜色也是红色,则出现连续的红色节点,需要进行调整
2、新节点插入后的情况分析及处理
如果父节点是黑色,则新节点插入后不违背红黑树的任何一条特性,插入完成。
如果父节点是红色,则违背特性三,需要分情况对红黑树进行调整。不过在继续细分之前,需要先达成两个共识:
1、由于根节点是黑色,此时红色的父节点一定不是根节点,因此祖父节点一定存在。另外,由于不能存在连续的红色节点,祖父节点必然是黑色。
2、由于新插入的节点必须是红色无法修改,必须将父节点改为黑色。
此时的情形是,新节点为红色、父节点为红色、祖父节点为黑色,唯一的变数就是叔叔节点,我们根据叔叔节点的三种不同形态分以下三种情况进行调整:
情况一:cur为红色、parent为红色,grandpa为黑色。uncle存在且为红色。
结构模型如下:(g、p、u分别表示grandpa、parent、uncle)
首先,我们将父节点改为黑色。为了使经过cur节点的路径上的黑色节点数量不变,我们同时将祖父节点改为红色。
调整前、后的结构模型如下:
颜色调整后,经过uncle的路径同时面临黑色节点减少、以及连续的红色节点两个问题。此时只需要将uncle节点也改为黑色就能同时解决这两个问题。
调整前、后的结构模型如下:
但此时问题还没有彻底解决,因为grandpa的父节点也有可能是红色,因此需要将grandpa当作新插入的节点,继续向上调整(cur=grandpa;parent=grandpa->parent)。
情况一的解决方式为:将father和unlce改为黑色,将grandpa改为红色,然后将grandpa作为新插入节点继续向上调整
情况二:cur为红色、parent为红色,grandpa为黑色。uncle存在且为黑色。
分析:cur节点一定不是新插入的节点,否则经过cur的路径上的黑色节点至少要比经过uncle的路径上的黑色节点少一个。因此,cur必然是通过第一种情况转化而来,其原来的颜色为黑色,且其左、右孩子必然都是黑色节点(情况二、三调整完以后不会再继续往上层调整)。
此时有如下四种结构模型:
我们先看第一种左直线的结构模型,grandpa、parent、cur三点连成一条偏向左边的斜线。
同样地,先将parent改为黑色,将grandpa改为红色,修改前、后的结构模型如下:
此时经过uncle的路径的黑色节点数量减少了一个。为使经过uncle的路径的黑色节点数量恢复至修改前的数量,此时我们对grandpa及uncle子树进行一个右旋操作,具体做法是:
1、将parent的右子树c链接到grandpa的左子树(p<c<g)
2、将grandpa链接到parent的右子树
3、将parent作为这颗子树的根节点,链接到grandpa原来的父节点下面
操作流程如下图:
右旋的代码逻辑如下:
//右旋
void RotateR(TreeNode* parent)
{
assert(parent);
TreeNode* leftTree = parent->_left;//左子树
TreeNode* leftTree_right = leftTree->_right;//左子树的右子树
TreeNode* grandpa = parent->_parent;//祖父
//先将左子树的右子树链接到parent的左边
parent->_left = leftTree_right;
if (leftTree_right)
leftTree_right->_parent = parent;
//然后将parent链接到左子树的右边
leftTree->_right = parent;
parent->_parent = leftTree;
//最后,左子树作为整棵子树的新的根节点,链接到祖父的下面
leftTree->_parent = grandpa;
if (grandpa == nullptr)//parent就是根节点
{
_root = leftTree;//将根换成左子树
}
else//grandpa不为空
{
if (grandpa->_left == parent)
{
grandpa->_left = leftTree;
}
else
{
grandpa->_right = leftTree;
}
}
}
对比刚插入时与最终调整后的状态:
此时不再含有连续的红色节点,且每条路径的黑色节点数量和插入前相同。且子树的根节点为黑色,不用再继续向上调整。
左直线的解决方式为:将parent改为黑色,将grandpa改为红色,然后对grandpa进行右旋操作
接着看第二种右直线的结构模型,grandpa、parent、cur三点连成一条偏向右边的斜线。
同样地,先将parent改为黑色,将grandpa改为红色,修改前、后的结构模型如下:
和左直线的情况类似,经过uncle的路径的黑色节点数量也减少了一个。为使经过uncle的路径的黑色节点数量恢复至修改前的数量,我们对grandpa及uncle子树进行一个左旋操作,具体做法是:
1、将parent的左子树c链接到grandpa的右子树(g<c<p)
2、将grandpa链接到parent的左子树
3、将parent作为这颗子树的根节点,链接到grandpa原来的父节点下面
操作流程如下图:
左旋的代码逻辑如下:
//左旋
void RotateL(TreeNode* parent)
{
assert(parent);
TreeNode* rightTree = parent->_right;//右子树
TreeNode* rightTree_left = rightTree->_left;//右子树的左子树
TreeNode* grandpa = parent->_parent;//祖父
//先将右子树的左子树链接到parent的右边
parent->_right = rightTree_left;
if (rightTree_left)
rightTree_left->_parent = parent;
//然后将parent链接到右子树的左边
rightTree->_left = parent;
parent->_parent = rightTree;
//最后,右子树作为整棵子树的新的根节点,链接到祖父的下面
rightTree->_parent = grandpa;
if (grandpa == nullptr)//parent就是根节点
{
_root = rightTree;//
}
else//grandpa不为空
{
if (grandpa->_left == parent)
{
grandpa->_left = rightTree;
}
else
{
grandpa->_right = rightTree;
}
}
}
对比刚插入时与最终调整后的状态:
此时不再含有连续的红色节点,且每条路径的黑色节点数量和插入前相同。且子树的根节点为黑色,不用再继续向上调整。
右直线的解决方式为:将parent改为黑色,将grandpa改为红色,然后对grandpa进行左旋操作。
接着看第三种左偏右折的结构模型,grandpa、parent、cur三点连成一条折线,从上往下先往左偏,然后往右边折。
我们先将parent改成黑色,将grandpa改成红色。此时经过uncle的路径的黑色节点数量同样减少了一个。但是当我们尝试按照之前的方式对grandpa进行右旋时,却发现cur链接到grandpa的左子树时,再次出现连续红色节点的现象,模型结构如下:
因此不能直接按照左直线的处理方式处理。
此时的做法是:先将左偏右折的结构调整成左直线的结构,再按照左直线的结构进行处理。
只需要先对parent进行一个左旋操作即可。
调整前后的结构如下 :
此时再按照左直线的方式处理即可。
左偏右折的处理方式:先对parent进行左旋,使grandpa、cur、parent三点连成一条偏向左边的斜线。然后将cur改为黑色,grandpa改为红色,再对grandpa进行右旋
最后再看第四种右偏左折的结构模型。grandpa、parent、cur三点连成一条折线,从上往下先往右偏,然后往左边折。
此时的处理方式和左偏右折相同,都是先将折线结构调整成直线结构。
调整前后的结构如下:
然后再按照右直线的方式进行处理。
右偏左折的处理方式:先对parent进行右旋,使grandpa、cur、parent三点连成一条偏向右边的斜线。然后将cur改为黑色,grandpa改为红色,再对grandpa进行左旋 。
情况三:cur为红色、parent为红色,grandpa为黑色。uncle不存在。
情况二的四种模型结构的处理中,uncle子树始终链接在grandpa的下面,没有进行过任何结构或颜色上的调整。也就是说,无论uncle子树是怎样的结构,都不需要对其进行调整。
因此,uncle为空的情况直接按照情况二的处理方式处理即可。
不过需要注意的是,此时的cur节点一定是新插入的节点。因为如果cur不是新插入的节点,则cur的下方一定存在黑色节点,则经过cur的路径的黑色节点数量就会多于经过uncle的路径。
结构调整的代码逻辑如下:
//如果父亲节点不存在或父亲节点的颜色为黑色,则插入完成
//如果父亲节点的颜色也是红色,则出现连续的红色节点,需要进行调整
while (parent && parent->_color == Red)
{
TreeNode* grandpa = parent->_parent;//祖父一定存在,且祖父的颜色一定是黑色
TreeNode* uncle = nullptr;//叔叔节点
if (parent == grandpa->_left)
uncle = grandpa->_right;
else
uncle = grandpa->_left;
//叔叔分为:存在且为红色、存在且为黑色、不存在三种情况
if (uncle && uncle->_color == Red)//叔叔也是红色
{
//将父亲和叔叔改成黑色,祖父改成红色
parent->_color = Black;
uncle->_color = Black;
grandpa->_color = Red;
//曾祖父有可能也是红色,因此需要将祖父当作新插入的节点继续进行调整
cur = grandpa;
parent = grandpa->_parent;
}
else //叔叔为黑色或叔叔不存在,处理方式相同
{
if (parent == grandpa->_left)//父亲是爷爷的左孩子(左偏)
{
if (cur == parent->_left)//左直线
{
//将parent改为黑色,将grandpa改为红色, 然后对grandpa进行右旋操作
parent->_color = Black;
grandpa->_color = Red;
RotateR(grandpa);//右旋
}
else//左偏右折
{
//先对parent进行左旋,使grandpa、cur、parent三点连成一条偏向左边的斜线。
//然后按照左直线的方式处理
RotateL(parent);//左旋
cur->_color = Black;//parent旋转后,cur成为grandpa的左孩子
grandpa->_color = Red;
RotateR(grandpa);//右旋
}
}
else//父亲是祖父的右孩子(右偏)
{
if (cur == parent->_right)//右直线
{
//将parent改为黑色,将grandpa改为红色,然后对grandpa进行左旋
parent->_color = Black;
grandpa->_color = Red;
RotateL(grandpa);//左旋
}
else//右偏左折
{
//先对parent进行右旋,使grandpa、cur、parent三点连成一条偏向右边的斜线。
//然后按照右直线的方式处理
RotateR(parent);//右旋
cur->_color = Black;//parent旋转后,cur成为grandpa的右孩子
grandpa->_color = Red;
RotateL(grandpa);//左旋
}
}
break;//调整完成后,子树的根节点为黑色,不用再向上调整
}
}
//调整完成
_root->_color = Black;//根节点有可能被改成红色,保险起见,统一将根节点置为黑色
最后需要注意的是,由于情况一有可能将根节点修改成红色。因此在插入完成后,统一将根节点置为黑色。如果根节点本来就是黑色,则无任何改变。如果根节点由红改黑,则相当于所有路径增加一个黑色节点。
3、插入总结:
1、插入分为三个步骤:
a.根据二叉搜索树的特性找到插入位置
b.创建新节点并链接到父节点下面
c.对异常结构进行调整
2、新插入的节点必须是红色
3、如果插入后父节点是黑色,则直接完成插入
4、如果插入后父节点是红色,则祖父节点一定存在且为黑色,且需要将父节点改为黑色
5、父节点是红色时,根据叔叔节点存在且为红、存在且为黑、不存在,在结构上分为三种情况;
但在实际处理时,分为两种情况,叔叔阶段为黑或不存在时的处理方式相同
6、父节点为红色、叔叔节点也为红色时,此时的处理方式为:将父节点和叔叔节点改为黑色,将祖父改为红色,并将祖父当作新插入的节点继续向上调整
7、父节点为红色、叔叔节点为黑色或不存在时。
如果grandpa、parent、cur在一条直线上,此时的处理方式为:将parent改为黑色,将grandpa改为红色,然后对grandpa进行左旋或右旋操作;
如果grandpa、parent、cur形成一条折线,此时的处理方式为:先对parent进行左旋或右旋,使grandpa、parent、cur形成一条直线,再按照直线的情况进行处理。
8、插入完成后,根节点有可能被修改成红色。统一将根节点置为黑色。如果根节点本来就是黑色,则无任何改变。如果根节点由红改黑,则相当于所有路径增加一个黑色节点。
五、红黑树的验证
红黑树的验证除了要验证其中序遍历是否为升序序列以外,更要对每条特性进行严格的检测。
连续红节点的验证:在递归遍历的过程中,只需检查每个红色节点的父节点是否为红色即可。
所有路径黑色节点数量相同的验证:对于经过某个节点的所有路径,根节点抵达该节点的这部分作为所有路径的前半段都是相同的,因此只需要检测以该节点为起点的每条路径上的黑色节点数量是否相同。我们只需要检测每个节点作为根节点时,左、右子树的黑色节点的高度是否相同即可。只要一棵树中所有节点的左、右子树中黑色节点的高度都相同,就能保证整棵树所有路径的黑色节点数量都相同。
红黑树验证的代码逻辑如下:
//中序遍历
void Inorder()
{
Inorder(_root);
}
//中序遍历(子函数)
void Inorder(TreeNode* root)
{
if (root == nullptr) return;
Inorder(root->_left);
cout << root->_val << endl;
Inorder(root->_right);
}
//红黑树的验证
bool IsRBTree()
{
if (_root == nullptr) return true;
if (_root->_color == Red)//检查根节点是否为黑
{
cout << "结构异常,根节点为红色" << endl;
return false;
}
//检查是否存在连续的红色节点,以及每棵子树中,是否每条路径的黑色节点数量都相同
pair<bool, int> result = IsRBTree(_root);
if (result.first)
{
cout << "结构正常" << endl;
return true;
}
else
{
cout << "结构异常" << endl;
return false;
}
}
//红黑树的验证(子函数)
typename pair<bool,int> IsRBTree(TreeNode* root)
{
if (root == nullptr)
return pair<bool,int>(true,0);
pair<bool, int> left = IsRBTree(root->_left);
pair<bool, int> right= IsRBTree(root->_right);
int num_black = max(left.second, right.second);
if (root->_color == Black)
++num_black;
//检查左右子树是否违背规则
if (!left.first || !right.first)
return pair<bool, int>(false, num_black);
//检查左、右子树的路径中黑色节点的个数是否相同
if (left.second != right.second)
{
cout << root->_val << "子树中不同路径的黑色节点数量不同" << endl;
return pair<bool, int>(false, num_black);
}
//检查是否存在相邻的红色节点(对父亲的颜色进行判断)
if (root->_color == Red)
{
TreeNode* parent = root->_parent;//根节点一定是黑色,则红色节点一定存在父亲
if (parent->_color == Red)//当前节点和父节点都是红色
{
cout << parent->_val << "和" << root->_val << "是连续的红色节点" << endl;
return pair<bool, int>(false, num_black);
}
}
return pair<bool,int>(true, num_black);
}
六、红黑树的删除
1、删除的思路与基本流程
二叉搜索树在删除节点时,并不会真的删除对应的目标节点。虽然以这样的方式删除节点同样有办法能维持其二叉搜索树的性质,但是会很大程度的改变原来的结构。通常都是采用狸猫换太子的方法,将目标节点左子树中最大的节点或右子树中最小的节点赋值给目标节点,然后删除左子树中最大的节点或右子树中最小的节点。以这种方式删除节点不会对原来的结构造成太大改变。
红黑树也是通过这种狸猫换太子的方式删除节点,删除节点的基本流程为:
1、根据二叉搜索树的特性找到目标节点
2、从目标节点的右子树中找到最小的节点(最左边的节点)替代目标节点进行删除
(用左子树中最大的节点进行替代是同样的道理)
3、对删除后的结构进行调整
当目标节点的一棵子树为空时,我们直接删除这个节点,然后将另一棵子树链接到其父节点下面。
当目标节点左、右子树都不为空时,我们选择其右子树中最左边的节点作为替代节点进行删除,而替代节点的左子树一定为空,因此可以直接删除替代节点,并将替代节点的右子树链接到其父节点下面。
本质上,直接删除目标节点或删除替代节点都是在一棵子树为空的情况下进行的。
2、不同情况的删除方式
当要删除的节点的一棵子树为空时,根据其颜色的不同分为以下三种情况:
情况1:要删除的节点为红色,一棵子树为空,则另一棵子树也一定为空
处理方式:直接删除该红色叶子节点
情况2:要删除的节点为黑色,一棵子树为空,另一棵子树为一个红色叶子节点
处理方式:将红色叶子节点的值赋给要删除的节点,然后删除红色叶子节点
情况3:要删除的节点是一个黑色叶子节点
处理方式:需要继续分情况讨论
三种情况的结构模型如下:
情况一和情况二都很好处理,真正不好处理的是目标节点为黑色叶子节点的情况,因为此时删除目标节点必然会导致其中一条路径的黑色节点数量减一。
接下来我们重点对目标节点为黑色叶子节点的情况进行分析处理。
3、目标节点为黑色叶子节点时的分析与处理
此时,我们重点关注目标节点的兄弟节点,根据兄弟节点的不同情况进行不同的处理。
1、兄弟节点不存在。
分析:一个没有兄弟节点的黑色叶子节点在整棵红黑树中只可能存在一个,就是根节点。
处理方式:直接删除目标节点,将根节点置为空
2、兄弟节点存在且为黑。
分析:此时必然存在父节点,且父节点有可能是黑色,也有可能是红色。在接下来的示例图中,我们用白色来表示这种不确定的颜色。
此时存在以下三种结构模型:
模型Ⅰ、brother含有一个与parent、brother连成一条直线的红色孩子。分别以下四种结构:
分析:此时每条路径都含有一黑一白的核心节点。我们删除后目标节点后,要保持每条路径的核心节点数量不变。
处理方式:
右直线的模型结构(a、b),先对p进行左旋,然后将新的根节点b改成白色(拷贝p的颜色),将p和bR(b的右孩子)改成黑色。操作流程如下:
左直线的模型结构(c、d),先对p进行右旋,然后将新的根节点b改成白色(拷贝p的颜色),
将p和bL(b的左孩子)改成黑色。操作流程如下:
模型Ⅱ、brother没有与parent、brother连成一条直线的红色孩子。但是另外有一个红色节点,与parent、brother连成一条折线。
结构模型如下:
分析:此时每条路径都含有一黑一白的核心节点。我们删除后目标节点后,要保持每条路径的核心节点数量不变。
处理方式:
左偏右折先对b进行左旋,使parent、brother、bR构成一条左直线,然后按照左直线的方式处理。
右偏左折先对b进行右旋,使parent、brother、bL构成一条右直线,然后按照右直线的方式处理。
操作流程如下:
模型Ⅲ、brother左、右孩子中不存在红色节点。此时分为父节点为红色和父节点为黑色两种结构模型。
先看父节点为红色的情况,结构模型如下:
分析:此时左右两边各一个黑色节点,删除后要保持每条路径各右一个黑色节点。
处理方式: 将parent改为黑,将brother改为红
操作流程如下:
再来看父节点为黑的情况,结构模型如下:
分析:此时左右两边都有两个黑色节点,但删除节点后无论怎么调整都不可能调平,因此只能想办法在上级公共路径上增加一个黑色节点进行弥补。而之前我们删除黑色叶子节点时,实际上都是在目标节点的路径上新增了一个黑色节点以弥补缺失的黑色节点,因此我们只需要将parent当作要删除的黑色节点,再按照之前删除黑色叶子节点的处理方式处理,就能在上级路径增加一个黑色节点。
通过结构模型对比可以更好地观察出这个规律:
模型Ⅰ、brother含有一个与parent、brother连成一条直线的红色孩子:
模型Ⅱ、brother没有与parent、brother连成一条直线的红色孩子。但是另外有一个红色节点,与parent、brother连成一条折线:
模型Ⅲ、brother左、右子树不存在红色节点
当parent为红色时,模型结构如下:
当parent为黑时
分析:由于cur是迭代而来,这意味着cur的下层曾经至少有一个黑色节点,也就意味着brother的下层至少有一个黑色节点。而brother孩子中存在红色节点的情况刚才讨论过了,此时的情况brother的孩子中没有红色节点,则brother的左、右孩子必然都是黑色节点。结构模型如下
分析:此时cur子树比brother子树的黑色节点的高度少一,且在当前parent子树中无法调平,我们只能再次往上层迭代,想办法在上层增加一个黑色节点。不过在继续往上层迭代前,同样要把brother改为红色,使得当前子树左、右平衡,如果不断出现这种情况就不断往上迭代直到出现其他情况或cur走到根节时停止。
往上迭代的过程如下:
由图形结构可以看出,如果向上迭代的过程中不断出现极端情况,最终所有路径的黑色节点数量都会减一。
解决方式:将b改成红色(使当前子树左右两边黑色节点数量保持一致),则整棵子树的所有路径的黑色节点数量都减少了一个。然后将p当作要删除的节点向上层遍历。如果再次出现parent、cur、brother都是黑色、且brother不存在红色孩子的情况,就继续将brother改成红色,然后继续往上层遍历。如果往上遍历的过程遇到其他情况,则可以通过其他情况的处理方式完成调整。
看到这里大家可能会存在疑问,是不是还漏了一种兄弟节点为红色的情况?兄弟节点为红色时怎么处理?万一往上迭代的时候出现兄弟节点为红色的情况怎么处理?
是的,确实还有一种兄弟节点为红色的情况没有讲解。不过不用担心,兄弟节点为红色的结构模型需要先转化成兄弟节点为黑色的结构模型,然后按照我们已经分析的兄弟节点为黑色的结构进行处理。这样一来,往上层迭代的过程不管遇到什么情况都有对应的方式可以解决。
3、兄弟节点存在且为红。
分析:此时父节点一定存在且为黑,且兄弟节点的左、右孩子一定都是黑节点。
模型结构如下:
以cur在左为例,此时右边偏高,按照传统思路我们尝试先对p进行左旋。
旋转前、后的结构如下:
分析:此时右边路径少了一个黑色节点,因此必须将b改为黑色。但这样又使得左边路径的黑色节点数量增加了一个。此时有两种选择:一是将bL改成红色,将cur删除。但由于bL可能存在红色孩子,因此这样操作不可行。二是将p改为红色,暂时不删除cur。此时达到了平衡状态,但紧接着又将面临cur的删除问题,此时对于cur而言,兄弟节点变成了bL,且兄弟节点为黑色,因此模型结构转化成兄弟节点为黑色的模型结构中的一种,按照兄弟节点为黑色的处理方式处理即可。
解决方案:
cur在左时对parent进行左旋,然后将brother改为黑色,将parent改为红色;结构调整后cur的兄弟变成黑色(bL节点),再按照兄弟节点为黑色的处理方式处理。
cur在右时对parent进行右旋,然后将brother改为黑色,将parent改为红色;结构调整后cur的兄弟变成黑色(bR节点),再按照兄弟节点为黑色的处理方式处理。
操作流程如下:
4、删除总结
1、删除的基本思路是替代删除。只有当目标节点本身含有一棵空子树时才会直接删除目标节点,目标节点的左、右子树都不为空时,选择左子树中最大节点或右子树中最小节点来替代删除。
2、不管是直接删除目标节点,还是删除替代节点。都是在存在一棵空子树的情况下进行删除
3、删除的基本流程:
a、根据二叉搜索树的特性找到目标节点
b、从目标节点的右子树中找到最小的节点(最左边的节点)替代目标节点进行删除
(用左子树中最大的节点进行替代是同样的道理)
c、对删除后的结构进行调整
4、目标节点一棵子树为空、且目标节点为红色时,另一棵子树必然也为空,直接删除红色叶子节点;
5、目标节点一棵子树为空、且目标节点为黑色时,则另一棵子树要么为空,要么为一个红色叶子节点。当另一棵子树为红色叶子节点时,将红色叶子节点的值赋给目标节点,然后删除红色叶子节点。
6、目标节点为黑色叶子节点时,根据兄弟节点是否存在、兄弟节点的颜色、及兄弟节点为黑色时是否存在对应的红色孩子节点的不同情况,进行不同的处理,重点看兄弟节点。
删除的完整代码如下:
//删除
bool Erase(const valueType& val)
{
TreeNode* target = Find(val);
if (target == nullptr) //没找到
return false;
//找到了
TreeNode* parent = target->_parent;
if (target->_left == nullptr)//右子树要么为空,要么为一个红色的叶子节点
{
if (target->_color == Red)//右子树只能为空,直接删除cur
{
//红色节点一定存在父亲
if (target == parent->_left)
parent->_left = nullptr;
else
parent->_right = nullptr;
delete target;
return true;//删除完成
}
else//黑色、左子树为空,则右子树要么为空,要么为一个红色的叶子节点
{
if (target->_right)//右子树不为空,则必然为红色叶子节点
{
//target右子树的红色节点赋值给cur,再删除target的右子树即可
target->_val = target->_right->_val;
delete target->_right;
target->_right = nullptr;
return true;//删除完成
}
//else
//右子树为空(目标节点是黑色叶子节点),待会分情况处理
}
}
else if (target->_right == nullptr)//左子树不为空,则只能是一个红色的叶子节点
{
//target节点一定是黑色,target左子树的红色节点赋值给target,
//再删除target的左子树即可
target->_val = target->_left->_val;
delete target->_left;
target->_left = nullptr;
return true;//删除完成
}
else//左、右子树都不为空,查找右子树中最左边的替代节点
{
parent = target;
TreeNode* replace = target->_right;
while (replace->_left)
{
parent = replace;
replace = replace->_left;//不断往左边在
}
target->_val = replace->_val;//将替代节点的值赋给原目标节点
target = replace;//替代节点成为新的目标节点,此时目标节点的左子树一定为空
if (target->_color == Red)//右子树只能为空,直接删除target
{
if (target == parent->_left)
parent->_left = nullptr;
else
parent->_right = nullptr;
delete target;
return true;//删除完成
}
else//黑色,左子树为空。则右子树要么为空,要么为一个红色的叶子节点
{
if (target->_right)//右子树不为空,则必然为红色叶子节点
{
//cur右子树的红色节点赋值给cur,再删除cur的右子树即可
target->_val = target->_right->_val;
delete target->_right;
target->_right = nullptr;
return true;//删除完成
}
//else
//右子树为空,target为黑色叶子节点,分情况讨论
}
}
//走到此处target一定是黑色叶子节点
if (parent == nullptr)//兄弟节点不存在的只有根节点一种情况,target就是根节点
{
delete target;
_root = nullptr;
return true;
}
//target不是根节点
while (parent)//向上遍历到根节点时停止
{
//兄弟节点一定存在
TreeNode* brother = nullptr;
if (target == parent->_left)
brother = parent->_right;
else
brother = parent->_left;
//根据兄弟节点的颜色分情况处理
if (brother->_color == Black)//兄弟节点是黑色
{
if (brother == parent->_right)//右偏
{
//brother含有一个与parent、brother连成一条直线的红色孩子(右直线)
if (brother->_right && brother->_right->_color == Red)
{
//先对parent进行左旋,然后将新的根节点brother改成parent的颜色
//然后将parent和brother的右孩子brother_R改成黑色。
TreeNode* brother_R = brother->_right;//brother的右孩子
RotateL(parent);
brother->_color = parent->_color;
parent->_color = Black;
brother_R->_color = Black;
//如果taregt就是叶子节点,就将其删除
if (target->_left == nullptr && target->_right == nullptr)
{
delete target;
parent->_left = nullptr;//target是parent的左孩子
}
break;//调整完成,退出循环
}
//brother没有与parent、brother连成一条直线的红色孩子。
//但是另外有一个红色节点,与parent、brother连成一条折线(右偏左折)
else if (brother->_left && brother->_left->_color == Red)
{
//先对brother进行右旋,使parent、brother、brother_L构成一条右直线
//然后对parent进行左旋,然后将新的根节点brother_L改成parent的颜色
//然后将parent和brother改成黑色
TreeNode* brother_L = brother->_left;//brother的左孩子
RotateR(brother);
RotateL(parent);
brother_L->_color = parent->_color;
parent->_color = Black;
brother->_color = Black;
//如果taregt就是叶子节点,就将其删除
if (target->_left == nullptr && target->_right == nullptr)
{
delete target;
parent->_left = nullptr;//target是parent的左孩子
}
break;//调整完成,退出循环
}
//brother左、右孩子中不存在红色节点
else if (parent->_color == Red)//父节点是红色
{
//将parent改为黑,将brother改为红
parent->_color = Black;
brother->_color = Red;
//如果taregt就是叶子节点,就将其删除
if (target->_left == nullptr && target->_right == nullptr)
{
delete target;
parent->_left = nullptr;//target是parent的左孩子
}
break;//调整完成,退出循环
}
else//parent、target、brother都是黑色,且brother左、右孩子中不存在红色节点
{
//将brother改成红色,然后把parent当作要删除的节点向上层遍历
brother->_color = Red;
//如果taregt就是叶子节点,就将其删除
if (target->_left == nullptr && target->_right == nullptr)
{
delete target;
parent->_left = nullptr;//target是parent的左孩子
}
target = parent;
parent = target->_parent;
}
}
else// brother == parent->_left (左偏)
{
//brother含有一个与parent、brother连成一条直线的红色孩子(左直线)
if (brother->_left && brother->_left->_color == Red)
{
//先对parent进行右旋,然后将新的根节点brother改成parent的颜色,
//然后将parent和brother的左孩子brother_L改成黑色
TreeNode* brother_L = brother->_left;
RotateR(parent);
brother->_color = parent->_color;
parent->_color = Black;
brother_L->_color = Black;
//如果taregt就是叶子节点,就将其删除
if (target->_left == nullptr && target->_right == nullptr)
{
delete target;
parent->_right = nullptr;//target是parent的右孩子
}
break;//调整完成,退出循环
}
//brother没有与parent、brother连成一条直线的红色孩子。
//但是另外有一个红色节点,与parent、brother连成一条折线(左偏右折)
else if (brother->_right && brother->_right->_color == Red)
{
//先对brother进行左旋,使parent、brother、brother_R构成一条左直线,
//然后对parent进行右旋,然后将新的根节点brother_R改成parent的颜色,
//然后将parent和brother改成黑色
TreeNode* brother_R = brother->_right;
RotateL(brother);
RotateR(parent);
brother_R->_color = parent->_color;
parent->_color = Black;
brother->_color = Black;
//如果taregt就是叶子节点,就将其删除
if (target->_left == nullptr && target->_right == nullptr)
{
delete target;
parent->_right = nullptr;//target是parent的右孩子
}
break;//调整完成,退出循环
}
//brother左、右孩子中不存在红色节点
else if (parent->_color == Red)
{
//parent改为黑,将brother改为红
parent->_color = Black;
brother->_color = Red;
//如果taregt就是叶子节点,就将其删除
if (target->_left == nullptr && target->_right == nullptr)
{
delete target;
parent->_right = nullptr;//target是parent的右孩子
}
break;//调整完成,退出循环
}
else//parent、brother、target都是黑色节点,且brother左、右孩子中不存在红色节点
{
//将brother改成红色,然后把parent当作要删除的节点向上层遍历
brother->_color = Red;
//如果taregt就是叶子节点,就将其删除
if (target->_left == nullptr && target->_right == nullptr)
{
delete target;
parent->_right = nullptr;//target是parent的右孩子
}
target = parent;
parent = target->_parent;
}
}
}
else//兄弟节点是红色
{
if (target == parent->_left)//target在左
{
//先对parent进行左旋,然后将brother改为黑色,将parent改为红色;
RotateL(parent);
brother->_color = Black;
parent->_color = Red;
//结构调整后target的兄弟变成黑色(brother_L节点),
//再按照兄弟节点为黑色的处理方式处理。
}
else//target在右
{
//先对parent进行右旋,然后将brother改为黑色,将parent改为红色;
RotateR(parent);
brother->_color = Black;
parent->_color = Red;
//结构调整后cur的兄弟变成黑色(brother_R节点),
//再按照兄弟节点为黑色的处理方式处理。
}
}
}
return true;//删除完成
}
总结
红黑树的插入与删除情况太多了,必须先把思路先滤清,最好是先画出对应的结构图来辅助理解,直接撸代码真的不太现实。
最后总结成一句话:插入看叔叔,删除看兄弟