此文章为从二叉树到红黑树系列文章的第六节,主要介绍介绍红黑树,相信,有了之前BST,AVL和B树的铺垫,你会很快地理解红黑树。但红黑树的情况也十分复杂,因此,推荐分两天来看红黑树。一天看插入,一天看删除。
文章目录
一、所有文章链接~(点击右边波浪线可以返回目录)
在阅读本文前,强烈建议你看下前面的文章的目录、前言以及基本介绍,否则你无法理解后面的内容。链接如下:
- 基本二叉树节点,通用函数 二叉树节点
- 基本二叉树类的定义和实现 二叉树基类
- BST(二叉搜索树的实现) BST
- AVL(二叉平衡搜索树的实现)AVL
- B树的实现(如果你只想了解B树,可以跳过所有章节,直接看B树,B树的理解是红黑树的基础)B树
- 红黑树的实现 RedBlack
理解红黑树之前,需要了解的知识点:~
- 在本系列文章第三部分BST中的删除的基本原理
- 在本系列文章第四部分AVL中的connect34和rotateAt的基本原理
- 在本系列文章第五部分B树中的插入和删除,上溢和下溢的基本原理
- 如果你还不了解,那么看接下来的内容,你可能会有点吃力。
二、引入红黑树~
AVL树尽管可以保证最坏情况下的单次操作速度,但需在节点中嵌入平衡因子等标识;更重要的是,删除操作之后的重平衡可能需做多达⌊log2n⌋次旋转,从而频繁地导致全树整体拓扑结构的大幅度变化。
红黑树即是针对后一不足的改进。通过为节点指定颜色,并巧妙地动态调整,红黑树可保证:在每次插入或删除操作之后的重平衡过程中,全树拓扑结构的更新仅涉及常数个节点。
三、红黑树的性质~
1.红黑树的外部节点~
一棵树,所有叶节点都有空孩子指针,因此,为了方便理解,可以将这些空孩子指针全部视为外部节点。即假想地加入外部节点(实际并没有加入),使得树中任何节点都可以视为有左右孩子。
下面是一颗红黑树
若按1中给这颗树增加外部节点,就可以得到
2.红黑树的性质~
提示:如果你看红黑树的算法,感到某些地方不好理解时,不妨来看看红黑树的性质,你就会明白算法为什么要这么设计。
由红、黑两色节点组成的二叉搜索树若满足以下条件,即为红黑树
(1) 树根始终为黑色
(2) 外部节点均为黑色(NULL LEAF)(假想,实际不存在)
(3) 其余节点若为红色,则其孩子节点必为黑色,反之,其父亲也必然为黑色。
(4) 从根节点到任一外部节点的沿途,黑节点的数目相等(黑深度相等)
红黑树性质解读~
- 由条件(1)(2)可知,红节点必然为内部节点。
- 由条件(3)可知红节点的孩子和父亲必然为黑色。即树中任何一条通路中绝对不可能有相邻的红节点。
- 由以上两个分析可知,在从根节点通往任一节点的沿途,黑节点都不少于红节点。
- 从根节点到任意节点所经的黑节点数目称为该节点的黑深度(由上往下)。(根节点黑深度为0)。由条件(4)可知,所有外部节点的黑深度必然相等。
- 从外部节点到内部任意节点,所经的黑节点的个数的最大值,称之为这个内部节点的黑高度(由下往上)。因此,外部节点的黑高度为0,根节点的黑高度等于外部节点的黑深度。
- 由以上可以得知,任意一个节点,其左右子树的黑高度都必然相等。
3.红黑树的适度平衡~
由2中的红黑树的性质解读的第三条,可以得知
在从根节点通往任一节点的沿途,黑节点都不少于红节点。
而一棵树,就是由红节点和黑节点组成,这样就代表,黑节点的数目,至少比全树所有节点的数目的一半大。而这一点,恰恰就是红黑树适度平衡的条件。
更严格的有log2(n + 1) <= h <=2∙log2(n + 1)(证明略)
尽管红黑树不能如完全树那样可做到理想平衡,也不如AVL树那样可做到较严格的适度平衡,但其高度仍控制在最小高度的两倍以内,从渐进的角度看仍是O(logn),依然保证了适度平衡—这正是红黑树可高效率支持各种操作的基础。
4.红黑树与B树(2,4)树的关系(提升变换)~
往下看之前,建议你理解一下B树的上溢和下溢。不懂的就看看本系列文章的第五部分,我对B树进行了详解。
在后面就可以得知,经适当转换之后,红黑树和(2,4)树相互等价!
具体地,自顶而下逐层考查红黑树各节点。每遇到一个红节点,都将对应的子树整体提升一层,从而与其父节点(必黑)水平对齐,二者之间的联边则相应地调整为横向。
由红黑树的性质(3)可得,对于有红孩子的黑节点而言,提升过程中,所涉及的节点至多不超过3个(可能为2个,当只有一个红孩子时),因为其最多只有两个红孩子,而对应的红孩子必然只有黑孙子,没有红孙子。
因此由变换之后的结果可以观察到,可以把变换之后的3个节点(或2个节点)看做一个整体,其恰好可以构成4阶B树(3个关键码)中的一个节点。因此,变换之后,每一颗红黑树都对应一颗(2,4)树。
提升变换的四种组合~
1、 通往黑节点的边对红黑树的黑高度有贡献,以实线表示,保留下来。
2、 通往红节点的边对红黑树的黑高度没有贡献,以虚线表示,不予保留。
从上图可以看出,对应的(2,4)B树。每个节点有且仅有一个黑色的关键码,同时红色的关键码不超过两个,若某个节点果真包含两个红关键码,则黑关键码的位置必然居中。
四、红黑树类~
(一)定义变量和接口~
1.利用已有的变量~
在第一部分定义二叉树节点的时候,我们定义了一个
RBColor _color;//红黑树专用
这个枚举类,主要是用于表示红黑树的颜色信息。具体为
namespace {
enum class RBColor {
RED, BLACK };
}
并且同样,我们会用到在BST定义的_hot节点
BinNodePtr _hot;//"命中节点"的"父亲"
2.需要的接口~
由于在BST中,我们已经定义了查找search算法,因此,不需要给RedBlack重新写查找算法,只需要对插入和删除算法进行重写既可(并且在后面可以发现,其插入和删除的本质,跟BST和AVL一模一样!)。并且在BinTree中,我们也定义了遍历算法,因此,也沿用即可。
在树中插入一个节点insert
在树中删除一个节点remove
3.重要辅助函数~
(1)重写高度更新算法~
由于红黑树的高度的表示方式为黑高度,所以其高度更新的算法也需要进行重写
更新高度updateHeight
(2)双红,双黑缺陷~
这两个辅助函数,正是红黑树得以保持平衡的最主要原因。在接下来介绍插入时,会解释双红缺陷,在介绍删除时,会解释双黑缺陷。
solveDoubleRed解决双红缺陷
solveDoubleBlack解决双黑缺陷
4.类内辅助静态函数~
为了加快算法执行的效率,和方便理解,在红黑树类内定义了4个静态内联函数。前两个很好理解,后面两个在介绍插入和删除算法时会进行解释。
IsBlack//判黑//当然x为空,也为黑色
IsRed//非黑即红
IsBlackHeightBalanced//判断是否需要更新黑高度
uncle//获取当前节点的叔叔
5.RedBlack.h~
template<typename T=int>
class RedBlack :public BST<T> {
protected:
using BinNodePtr = BinNode<T>*;
protected:
void solveDoubleRed(BinNode<T>* x);//双红修正
void solveDoubleBlack(BinNode<T>* replacer);//双黑修正
constexpr int updateHeight(BinNode<T>* x)const override;//更新高度
public:
BinNode<T>* insert(const T& data)override;//插入重写
bool remove(const T& data)override;//删除重写
/*查找沿用BST的查找*/
/*遍历沿用BinTree的遍历*/
protected:
static constexpr bool IsBlack(const BinNodePtr& x) {
//判黑//当然x为空,也为黑色
return ((!x) || (RBColor::BLACK == x->_color));
}
static constexpr bool IsRed(const BinNodePtr& x) {
//非黑即红
return !IsBlack(x);
}
static constexpr bool IsBlackHeightBalanced(const BinNodePtr& x) {
//判断是否需要更新黑高度
bool is_L_C_Equal = (stature(x->_lchild) == stature(x->_rchild));
int rbHeight = (IsRed(x) ? stature(x->_lchild) : stature(x->_lchild) + 1);//对于rbHeight的计算而言,取左孩子还是右孩子,均一样
bool is_Height_Equal = (x->_height == rbHeight);
return is_L_C_Equal && is_Height_Equal;//只要有一个为假,即为假
//所以只要左孩子和右孩子高度相等,或者x没有高度变化,就黑高度平衡
}
static inline BinNodePtr uncle(const BinNodePtr& x) {
/*获取x的叔叔*/
return IsLChild(x->_parent) ? x->_parent->_parent->_rchild : x->_parent->_parent->_lchild;
}
};//class RedBlack
(二)高度更新~
下面是我们在本系列文章第一部分定义的获取当前节点高度的全局静态函数。并且我们规定当没有节点时,高度为-1,当有一个节点时,高度为0(见第一部分关于树的语义规定中树的高度的定义)。此规定依然适用于红黑树,也就是说,哪怕红黑树此时只有一个根节点(必然为黑),其高度为0而不是1。
此规定,对于后序红黑树的平衡不造成任何影响,但若读者要获取红黑树的高度的话,就需要明白此时的黑高度,比实际的黑高度少1。
template<typename BinNodePtr>
static constexpr int stature(const BinNodePtr& x) {
//获取高度
return x ? x->_height : -1;//空指针高度为-1
}
高度更新代码~
template<typename T>
constexpr int RedBlack<T>::updateHeight(BinNode<T>* x) const//由于stature视空节点高度为-1,所以height会比黑高度少一
{
x->_height = std::max(stature(x->_lchild), stature(x->_rchild));//孩子一般黑高度相等,除非出现双黑
return IsBlack(x) ? x->_height++ : x->_height;//若当前节点为黑,则计入黑高度
}
由于重写了更新高度函数,所以此时x的高度,为黑高度。
要更新红黑树的高度(即黑高度),只有当:
(1)左右孩子黑高度不相等。
(2)x为黑节点时,其高度要加1。
(3)x为红节点时,其高度不需要额外更新。
(三)红黑树的插入代码~
红黑树的插入算法,与BST,AVL的基本插入方式一模一样,唯一不同的是后续要进行双红修复。
先用BST的search确定不存在这个节点,并且更新_hot的位置,并以_hot为父亲,创建一个新节点。并将其黑高度更新为-1,以及染色成红色(我们默认新加入的节点均为红色节点,除非新加的是根节点)。
由BinNode节点的构造函数,默认新节点为红色。
template<typename T>
BinNode<T>* RedBlack<T>::insert(const T& data)
{
BinNode<T>*& x = this->search(data);//沿用BST的查找//并更新_hot//用引用接收
if (x)//如果节点存在,则返回
return x;
x = new BinNode<T>(data, this->_hot, nullptr, nullptr, -1);//设定黑高度-1,并默认节点为红色
this->_size++;
solveDoubleRed(x);//双红修正//x此时必为红
return x;
}
(四)双红修复~
因新节点的引入,而导致父子节点同为红色的此类情况,称作“双红”(double red)。每引入一个关键码,双红修正函数都可能迭代地调用多次。在此过程中,当前节点x的兄弟及两个孩子(初始时都是外部节点),必然均为黑色。
因为x的父亲为红色,所以其只可能有黑孩子,所以x若有兄弟,则必为黑色。
由于x为新节点,其外部节点为空,即默认均为黑孩子。
将x的父亲与祖父分别记作p和g。既然此前的红黑树合法,故作为红节点p的父亲,g必然存在且为黑色。
此时的g,必然存在,否则作为树根的节点p不可能为红色;并且g作为红色节点p的父亲,其必然为黑色的
在下面的过程中,仅仅有x p g这三个节点还不够,因此,还需要一个额外的节点,即p的兄弟(x的叔叔)u。
以下,视节点u的颜色不同(若u不存在,其颜色也视为黑,这符合之前外部节点的颜色定义),分两类情况分别处置。
1) u为黑色~
u为黑色时,具体来说,对应四种结构.
此时x的兄弟和两个孩子的黑高度必然都与u的黑高度相等。
a) LL型~
在原来的树中,插入了新节点x。构成下图所示的结构。
此时,可以利用B树来理解,不妨先将红黑树,经过提升变换,提升为对应的(2,4)B树。
从B树的结构可以看出,其不满足先前提升变换的四种情况中的任何一种。
因此,要想其满足提升变换的四种情况,最简单的做法,就是将p与g互换颜色,让对应的(2,4)B树变成下图所示形式
再将对应的(2,4)B树还原成红黑树,即为
即原来的 x 变成 a,原来的 p 变成 b ,原来的 g 变成 c 。
但也注意到,相应的孩子节点的位置也发生了新的变化。
因此,如何做到这样的变化呢?此时不妨想想在本系列文章第三部分定义的AVL中的connect34算法,其对应的形状也是这样的形状
没错,只要我们将对应的x p g按照connect34的形式进行重构,就可以达到目的。
b) RR型~
同LL型一样处理,不多赘述。
c) LR型~
在原来的树中,插入了新节点x。构成下图所示的结构。此时仍然满足x的兄弟和两个孩子的黑高度必然都与u的黑高度相等这个条件。
此时,同样可以利用B树来理解,不妨先将红黑树,经过提升变换,提升为对应的(2,4)B树。
情况总是惊人的相似,可以发现,现在的形状的调整方式,不正是同LL型的调整方式一模一样么?只是x p g的相对位置有所变化。因此,也是需要进行connect34重构。
d) RL型~
同LR型一样处理,不多赘述。
2) u为红色~
u为红色时,具体来说,对应四种结构
此时,u的左、右孩子均为黑色(可能为空),其黑高度必与x的兄弟以及两个孩子相等。
a) LL型~
在原来的树中,插入了新节点x。构成下图所示的结构。
此时,同样可以利用B树来理解,不妨先将红黑树,经过提升变换,提升为对应的(2,4)B树。
在介绍LR型的时候,会展示怎么处理这种情况。
b) RR型~
同LL型一样处理,不多赘述。
c) LR型~
在原来的树中,插入了新节点x。构成下图所示的结构。此时仍然满足x的兄弟和两个孩子的黑高度必然都与u的黑高度相等这个条件。
此时,同样可以利用B树来理解,不妨先将红黑树,经过提升变换,提升为对应的(2,4)B树。
可以看到,提升变换之后,其这个大节点的关键码数目,均必然为4个,超出了4阶B树的个数限制(4阶B树的一个大节点的关键码数最多只能有3个)。所以可以仿照B树的情况,进行一次上溢。同时进行染色以满足原来B树提升变换后的四种形态。(问号节点中必然有一个为黑,按照红黑树的提升变换,只有当g染成红时才可以进行提升变换)
从红黑树的角度来看,对比没有变换之前
从宏观上来看,对于红黑树而言,只需要将p u的颜色由红色变成黑色,并且若g不为根节点的话,就将g染色成红色。当然,若g此时就是根节点,其强制变成黑色。
同样,由于g变成了红色,所以还需要继续判断g的父亲是否是红色,因此要继续进行双红修复。最坏的情况,可能要持续到根节点。(由x到g,上升了两层)。累计最多迭代logn次。
d) RL型~
同LR型一样处理,不多赘述。
3) 求当前节点的叔叔代码~
static inline BinNodePtr uncle(const BinNodePtr& x) {
/*获取x的叔叔*/
return IsLChild(x->_parent) ? x->_parent->_parent->_rchild : x->_parent->_parent->_lchild;
}
4) 双红修复代码递归版~
注意要是已经递归到树根,则树根强制转黑
template<typename T>
void RedBlack<T>::solveDoubleRed(BinNode<T>* x)
{
if (IsRoot(x)) {
//若已递归到树根,则树根转黑,整树高度也随之递增
this->_root->_color = RBColor::BLACK;
this->_root->_height++;
return;
}//否则x的父亲必然存在
BinNode<T>