🤡博客主页:醉竺
🥰本文专栏:《高阶数据结构》
😻欢迎关注:感谢大家的点赞评论+关注,祝您学有所成!
✨✨💜💛想要学习更多《高阶数据结构》点击专栏链接查看💛💜✨✨
目录
[1. 什么是红黑树?](#1. 什么是红黑树?)
[1.1 红黑树存在的意义](#1.1 红黑树存在的意义)
[1.2 红黑树的定义和性质](#1.2 红黑树的定义和性质)
[1.3 判断是否为红黑树(练习)](#1.3 判断是否为红黑树(练习))
[2. 红黑树的插入](#2. 红黑树的插入)
[2.1 插入案例详细图解](#2.1 插入案例详细图解)
[2.2 案例练习](#2.2 案例练习)
[3 红黑树完整代码实现](#3 红黑树完整代码实现)
[3.1 红黑树结点的定义](#3.1 红黑树结点的定义)
[3.2 插入代码](#3.2 插入代码)
[3.3 红黑树的验证](#3.3 红黑树的验证)
[3.4 红黑树的查找](#3.4 红黑树的查找)
1. 什么是红黑树?
在 本文之前,我写了有关"平衡二叉树"的性质、插入和删除的文章,引入了"平衡性"的概念:
《二叉搜索树的插入、删除和查找(精美图解+完整代码)》https://blog.csdn.net/weixin_43382136/article/details/140280070?spm=1001.2014.3001.5501《平衡二叉树(AVL)的插入(4种旋转方法+精美图解+完整代码)》https://blog.csdn.net/weixin_43382136/article/details/140403187?spm=1001.2014.3001.5501《平衡二叉树(AVL)的删除和调整》https://blog.csdn.net/weixin_43382136/article/details/140577239主要目的是让这棵二叉搜索树左右看起来比较"平衡",不出现左子树很高、右子树很矮,或者左子树很矮、右子树很高的情形。这样,在进行节点的查找、插入、删除等操作时效率会比较高。
但这同时也带来了缺点------在插入和删除节点时,为了调整平衡,必须对树中的节点进行旋转,从而在一定程度上影响程序的执行效率。
平衡二叉树的一条重要性质是"任一节点的左子树和右子树的高度之差不超过1"。这里引入一个" 非严格平衡二叉树"的概念。
非严格平衡二叉树指的是 不完全符合 前面平衡二叉树的定义,或者说并不是一种严格意义上的平衡二叉树。但这种二叉树最小高度仍旧在(n代表节点数)附近,或者说,查找操作的时间复杂度仍旧为。所以,我们仍旧可以认为这种二叉树是一种平衡二叉树。这就引出了这节课要讲解的"红黑树"。
1.1 红黑树存在的意义
如果你正在思考为什么已经有AVL树了还需要引入红黑树,那么简单来说可以这样理解。
-
如果AVL树 很大 ,那么在 插入和删除 操作时会进行大量的旋转操作以达到AVL树的平衡,尤其是删除节点时,可能要经过若干次的旋转操作,甚至可能需要从最底部的节点一直到根节点都进行平衡性调整。
-
但在红黑树中,当进行插入和删除操作时,维护红黑树的平衡性成本就比较低------多数情况下只需要旋转两次或者不需要旋转只需要对节点的颜色进行修改,也就是把红色修改为黑色。
所以,虽然AVL树比红黑树更加平衡,但针对 插入和删除 操作,红黑树可以保证平均情况时间复杂度更接近。换句话说,如果从单纯 搜索 角度来讲, AVL树更快 ,但如果 频繁 进行 插入和删除 操作,因为 红黑树 需要更少的旋转,无疑 效率会更高,而且红黑树的查找、插入、删除操作性能较稳定。
下面一张图总结为什么需要存在红黑树:
1.2 红黑树的定义和性质
红黑树(Red-Black Tree),简称R-B树,是一种高效的二叉查找树,由Rudolf Bayer在1972年发明的,当时被称为"对称/平衡二叉B树",后来在1978年由Leo J. Guibas和Robert Sedgewick 修改为"红黑树"。
红黑树首先是一棵 二叉搜索树, 也是一种典型的非严格平衡二叉树,或者你也可以理解成一种 特殊/特化 的AVL树,甚至在很多资料中, 提到平衡二叉树指的就是红黑树 。红黑树在 插入和删除 节点时,会通过特定的操作保持二叉查找树的相对平衡,从而获得 比较高的查找效率。既然是相对平衡,所以任一节点的左子树和右子树的高度之差很可能会超过1。
下面先看一张红黑树的图片,我们比对着图片来讲解红黑树的性质:
性质1 :每个节点或是红色,或是黑色
性质2 :根节点必须是黑色 的。
性质3 :叶子节点(外部节点、Nil节点、失败节点)均是黑的。
性质4 :**不存在两个连续的红节点。**同时,下面几种说法都是一样的理解一种即可。
红色节点是被黑色节点分隔开的。
红色节点的两个孩子节点或者父亲节点必须是黑色的。
从叶节点到根的所有路径上不可以有两个连续的红色节点。
性质5: 对于每个节点,从该节点到任一叶节点的简单路径上,所含黑节点的数目相同。
这个黑色节点数量叫做" 黑高度 "或者" 黑高 "(bh),用于保证黑色节点的平衡性。
与"黑高"相关的推论:
根据红黑树需要同时满足 性质4 和 性质5还能得到红黑树有两个特点(为了减少记忆负担了解即可,重要的还是上述性质):
性质1证明:
-
从根到叶的最短路径:这一路径下全是黑色节点没有红色节点。
-
从根到叶的最长路径:这个路径将包含尽可能多的红色节点。由于红黑树的性质,红色节点后面必然跟随一个黑色节点,因此每增加一个红色节点,就会增加一个黑色节点。当黑色节点数和最短路径上的黑色节点数量相同时,这个最长路径的节点数量(红+黑)就是最短路径黑色节点数量的两倍了。
性质2证明:
设根节点的黑高为 h, 则 内部节点数最少有个;
若红黑树的高度为 h, 则 根节点的黑高 ≥ h/2;(最短路径),因此内部节点的个数 n最少有,即,由此推出
1.3 判断是否为红黑树(练习)
为了方便记忆红黑树以上的定义和性质,可以背诵下面的顺口溜:
接下来我们练习一下,来判断下面的树是不是红黑树?
例1:
例2:
例3:
例4:
2. 红黑树的插入
我们先来想想,红黑树插入节点操作一般分为几种情况呢?
- 首先,对于没有任何节点的空树,直接创建一个 黑色 节点作为根节点即可。
- 其次,对于非空的树,查找要插入的位置并插入一个 红色 节点,然后要判断该节点的父节点。
为什么插入的新节点是红色的而不是黑色的呢?
若我们插入的是黑色结点,那么插入路径上黑色结点的数目就比其他路径上黑色结点的数目多了一个,即破坏了红黑树的性质5("黑路同"),就会影响红黑树的多条路经。而之所以插入的是红色,是因为红色节点不会增加黑高度,从而尽量减少插入节点后导致的平衡性调整。
- 插入红色节点之后,如果其父节点为黑色,此时不需要做任何调整。
- 若其父节点为红色,最多会违背性质红黑树的性质4("不红红"),此时必须进行平衡性调整。
红黑树的插入看似复杂,其实不难,相比于平衡二叉树而言,红黑树的插入无需更新平衡因子,只是在旋转调整的基础上,再对相关节点进行变色即可!
下面是对红黑树的插入进行的总结,只需要理解下面的内容就能完全理解红黑树的插入,请认真阅读下图。
注意: 当叔叔不存在(为叶子节点,Nil,NULL)也属于**"黑叔"。**
LL型: 新插入节点是 爷爷左孩子的左孩子 (右旋)
RR型: 新插入节点是 爷爷右孩子的右孩子 (左旋)
LR型: 新插入节点是 爷爷左孩子的右孩子 (先左旋,后右旋)
RL型: 新插入节点是 爷爷右孩子的左孩子 (先右旋,后左旋)
对上图内容进行分析:
我们知道,插入一个新的红节点时,若父亲也是红色,则最多违背了性质4("不红红"),此时必须进行调整。 从上图中我们了解到红黑树插入后进行调整时:需要看新节点叔叔的颜色。
因此,当我们插入一个新的红节点并且违背了红黑树的性质需要进行调整时,此时必有下面情况:
- 父亲一定是红色 (否则插入后怎么会违背性质呢?);同理,此时爷爷一定是黑色(否则在没插入时就已经父爷俩节点就违背了"不红红")。
- 若新插入节点的叔叔为红色 ,则有 叔父为红,爷为黑。
- 若新插入节点的叔叔为黑色(叔叔不存在也为黑色Nil) ,则有 父为红,爷叔为黑。
上述 3 条无需记忆,只是进行分析理解即可,为后续代码实现以及调整过程中的"染色"进行铺垫。
上图调整过程中,所谓的"变色",就是把相关节点的颜色进行 " 红变黑,黑变红 "。
根据上面3条,以及"染色",我们可以推出:
调整染色过程中,爷爷要变为红,父亲要变为黑,叔叔不变(原来啥颜色就啥颜色)。
有一点需要注意:若爷爷变为红后,但其是整个红黑树的根,还要将爷爷恢复成黑色(性质1)。
2.1 插入案例详细 图解
1. 红叔:变色+变新(无需旋转)
调整方法: 无论什么型,**父叔爷**变色(父、叔变黑,爷变红),再将爷爷看作为新插入的节点(继续向上调整)
- 若爷爷是整个二叉树的根节点,那么为了满足红黑树性质1 "根是黑色",要把爷爷节点变回黑色。此时整个调整才完毕。
- 如果爷爷节点不是整个二叉树的根节点,则还需要继续沿着爷爷节点向上调整,调整中如果遇到一个黑色的前辈节点,则整个平衡性调整完毕。
2.黑叔(或者叔叔为空):旋转+变色
调整方法:
LL型:右单旋,父换爷并变色
RR型:左单旋,父换爷并变色
LR型:左、右双旋,儿换爷并变色
RL型:右、左双旋,儿换爷并变色
LL型: 新插入节点是爷爷左孩子的左孩子,叔叔节点不存在或者存在但为黑色。以爷爷节点为根向右旋转。接着将原父亲节点变为黑色,原爷爷节点变为红色。
LR型: 新插入节点是爷爷右孩子的右孩子,叔叔节点不存在或者存在但为黑色。首先以父亲节点为根向左旋转,然后再以爷爷节点为根向右旋转。接着将原来的新节点变为黑色,原来的爷爷节点变为红色。
RR型: 新插入节点是爷爷右孩子的右孩子,叔叔节点不存在或者存在但为黑色。以爷爷节点为根向左旋转。接着将原父亲节点变为黑色,原爷爷节点变为红色。
RL型:新 插入节点是爷爷右孩子的左孩子,叔叔节点不存在或者存在但为黑色。首先以父亲节点为根向右旋转,然后再以爷爷节点为根向左旋转。接着将原来的新节点变为黑色,原来的爷爷节点变为红色。
2.2 案例练习
上述2.1节已经涵盖红黑树所有插入和修改的情况,接下来请根据上图的插入调整步骤,看一下下面的实战练习,图比较多!
3 红黑树完整代码实现
3.1 红黑树结点的定义
我们这里直接实现 KV模型 的红黑树,为了方便后序的旋转操作,将红黑树的结点定义为三叉链结构,除此之外还新加入了一个成员变量,用于表示结点的颜色。
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)
{}
};
红黑树类的定义框架:
cpp
template<class K, class V>
class RBTree
{
typedef RBTreeNode<K, V> Node;
private:
Node* _root = nullptr;
};
下面红黑树相关成员函数的实现,都是在RBTree这个类中实现的。
3.2 插入代码
cpp
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
_root->_col = BLACK;
return true;
}
// 1.找到插入位置
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (kv.first < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
else if (kv.first > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else
{
return false;
}
}
// 2.插入新节点,新增节点给红色
cur = new Node(kv);
cur->_col = RED;
if (kv.first < parent->_kv.first)
{
parent->_left = cur;
cur->_parent = parent;
}
else
{
parent->_right = cur;
cur->_parent = parent;
}
// 3.若插入节点的父亲是黑色的,则不需要调整,
// 若父亲是红色的,则需要修正红黑树的性质进行调整
while (parent && parent->_col == RED)
{
Node* grandfather = parent->_parent;
if (parent == grandfather->_left) // LL型插入+LR型插入
{
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED) // 红叔 : 父叔爷变色+爷变新节点(继续往上处理)
{
// 变色
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
// 继续往上更新处理
cur = grandfather;
parent = cur->_parent;
}
else // 黑叔(Nil)
{
if (cur == parent->_left) // 黑叔(Nil)+LL型插入 : 右旋+父爷变色
{
// R单旋
// g
// p bu(Nil)
// c
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else //(cur == parent->_right) 黑叔(Nil)+LR型插入 : 左旋+右旋+儿爷变色
{
// LR双旋
// g
// p bu(Nil)
// c
RotateLR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
else // RR型插入+RL型插入(parent == grandfather->_right)
{
Node* uncle = grandfather->_left;
if (uncle && uncle->_col == RED) // 红叔 : 父叔爷变色+爷变新节点(继续往上处理)
{
// 变色
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
// 继续往上处理
cur = grandfather;
parent = cur->_parent;
}
else // 黑叔(Nil)
{
if (cur == parent->_right) // 黑叔(Nil)+RR型插入 : 左旋+父爷变色
{
// L单旋
// g
// bu p
// c
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else //(cur == parent->_left) // 黑叔(Nil)+RL型插入 : 右旋+左旋+儿爷变色
{
// g
// bu p
// c
RotateRL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
_root->_col = BLACK; // 根节点必须是黑色(无论中间过程如何调整过)
return true;
}
对应的4种旋转代码(看不懂的话,请移步我的文另一篇文章每种旋转方式讲解的特别清晰!)
cpp
// LL型插入,右旋
void RotateR(Node* gf)
{
Node* p = gf->_left;
Node* pr = p->_right;
Node* ggf = gf->_parent;
gf->_left = pr;
if (pr != nullptr)
pr->_parent = gf;
p->_right = gf;
gf->_parent = p;
if (_root == gf)
{
_root = p;
p->_parent = nullptr;
}
else
{
if (ggf->_left == gf)
{
ggf->_left = p;
}
else
{
ggf->_right = p;
}
p->_parent = ggf;
}
}
// RR型插入,左旋
void RotateL(Node* gf)
{
Node* p = gf->_right;
Node* pl = p->_left;
Node* ggf = gf->_parent;
gf->_right = pl;
if (pl != nullptr)
pl->_parent = gf;
p->_left = gf;
gf->_parent = p;
if (_root == gf)
{
_root = p;
p->_parent = nullptr;
}
else
{
if (ggf->_left == gf)
{
ggf->_left = p;
}
else
{
ggf->_right = p;
}
p->_parent = ggf;
}
}
// LR型插入,左旋+右旋
void RotateLR(Node* gf)
{
RotateL(gf->_left);
RotateR(gf);
}
// RL型插入,左旋+右旋
void RotateRL(Node* gf)
{
RotateR(gf->_right);
RotateL(gf);
}
3.3 红黑树的验证
cpp
bool ISRBTree()
{
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 _ISRBTree(_root, blacknum, refVal);
}
// 根节点->当前节点这条路径的黑色节点的数量
bool _ISRBTree(Node* root, int blacknum, const int refVal)
{
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 _ISRBTree(root->_left, blacknum, refVal) && _ISRBTree(root->_right, blacknum, refVal);
}
3.4 红黑树的查找
cpp
//查找函数
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (key < cur->_kv.first) //key值小于该结点的值
{
cur = cur->_left; //在该结点的左子树当中查找
}
else if (key > cur->_kv.first) //key值大于该结点的值
{
cur = cur->_right; //在该结点的右子树当中查找
}
else //找到了目标结点
{
return cur; //返回该结点
}
}
return nullptr; //查找失败
}
创作不易,对你有用的话麻烦点个收藏或者评论吧~❤