传统艺能😎
小编是双非本科大一菜鸟不赘述,欢迎各位指点江山(期待~)(QQ:1319365055)
此前博客点我!点我!请搜索博主 【知晓天空之蓝】
🎉🎉非科班转码社区诚邀您入驻🎉🎉
小伙伴们,打码路上一路向北,彼岸之前皆是疾苦
一个人的单打独斗不如一群人的砥砺前行
这是和梦想合伙人组建的社区,诚邀各位有志之士的加入!!
社区用户好文均加精(“标兵”文章字数2000+加精,“达人”文章字数1500+加精)
直达: 社区链接点我
红黑树是对树结构的一种高度综合运用,涉及到多叉树,树平衡调整,节点旋转等等,这些是对数据结构基本功的最佳历练,也是面试场上令人闻风丧胆的冷面一刀。能否给出完整的定义,能否介绍自己对红黑树的认识,能否通过旋转,染色等操作在给定的场景下对一颗红黑树进行调整使其符合定义…这些才是我们应该在学习后得到的信息
概念🤔
红黑树也是一种二叉搜索树,但是每个节点都存储了一个颜色,该颜色可以为黑可以为红,因此也叫红黑树。
红黑树和 AVL 树的区别就是红黑树属于近似平衡,他并不是完全平衡,红黑树不会像 AVL 树一样做到严密的平衡(高度差控制在 1 ),他是依靠每条路径上红黑节点的排布与限制,确保最长路径长度不超过最短路径长度的 2 倍.
五大特性🤔
红黑树有着标志性的五大特性:
- 根节点一定为黑色
- 一个节点只能是红或黑
- 节点为红色,则该节点的两个子节点都为黑色
- 对于每个节点,从根节点到叶子结点的简单路径上,黑色节点个数相等
- 每个叶子结点(即空节点)都是黑色
那么问题来了,仅仅依靠这五大特性是如何确保最长路径长度 < 最短路径长度的 2 倍?
根据第 3,4 特性,我们不妨思考一下极端场景,最短可能路径其实就是全黑的情况,假设此时有 n 个节点,长度就为 n:
而最长可能路径就是在红色节点存在的条件下,以一红一黑的方式进行排列,此时假设依然有 n 个黑色节点,那么红色节点数目与黑色相同,则长度为 2n,以此证明。
节点定义🤔
这里和 AVL 树一样我们直接实现 KV 模型,为了方便后序的旋转操作,将红黑树的结点定义为三叉链结构,将 AVL 树中的平衡因子换成了新成员——结点的颜色。
因为节点颜色是贯穿的,我们可以直接定义成全局变量,并用枚举变量来给颜色赋予逻辑:
enum Color
{
Red, //0
Black //1
};
接下来就可以定义节点了:
template<class K,class V>
struct RBTreeNode
{
RBTreeNode(const pair<K, V>& kv, Color color = Red)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _color(color)
{
}
RBTreeNode<K,V>* _left;
RBTreeNode<K,V>* _right;
RBTreeNode<K,V>* _parent;//三叉链结构
pair<K,V> _kv;//存储键值对
Color _color;//节点颜色
};
注意这里有个细节:为什么构造结点时,默认将结点的颜色设置为红色?
因为我们要考虑插入场景,不论此时根节点为黑色还是红色,我们插入黑色节点,那么他一定会破坏性质 4 ,某条路径上的黑色节点一定会增加一个,那么为了维护结构,我们岂不是要给其他所有路径都加上一个黑色节点?但如果此时插入的是红色节点,如果根节点为红色,那么又会破坏性质 3 出现了连续的红色节点,但是如果根节点为黑色,就不需要进行调整。
这就是一个简单的伤害最小化理论:插入黑节点,一定会破坏性质 4,必须进行调整;插入红节点,可能破坏性质 3,可能进行调整,因此我们选择将插入节点定为红色。
红黑树插入🤔
插入逻辑依然和二叉搜索树差不多,三步走:
- 按二叉搜索树的插入方法,找到待插入位置。
- 将待插入结点插入到树中。
- 若插入结点的父结点是红色的,则需要对红黑树进行调整。
插入的关键就在于这里的第三步,这调整里面有大学问。
插入调整🤔
我们给出一个基本模型(可以是一个完整的树,也可以是一个子树):
并约定:
cur :当前节点
p:parent,父节点
g:grandfather,祖父节点
u:uncle,叔叔节点
a,b,c,d,e:子树
顺便提一下,树的路径不是一路走到叶子结点算一条,要走到空节点(NIL 节点)算一条:
比如上图中该红黑树的有效路径不是 4 条而是 9 条。
情况一😎
cur 为红,p 为红,g 为黑, u 存在且为红
首先你需要知道,红黑树的调整关键看叔叔,因为节点的颜色是固定的,祖父存在且一定为黑,为什么?因为父节点为红色那么他一定不会是根,他一定有一个父节点且为黑,只有 u 节点不知道嘛情况。
我们调整并不是像 AVL 一样旋转,这里是进行变色,首先将叔叔变黑,此时就没有连续的红节点了,但是为了保持黑节点数目不变父亲也要变黑,万事大吉了吗?没有,紧接还要将祖父变红,为什么呀!?不要忘了,这里可能只是一个子树,叔叔父亲变黑了那么路径上黑节点就会多出一个,对整个结构必然有影响,所以还需要将祖父变红保持黑色数量不变。
最后不要忘了将祖父当成 cur 节点继续向上调整,因为在上一层父亲的颜色不知道,祖父改变可能违反规则还会继续调整。这种情况我们左右方向,翻转过来处理方法一样,只变色不旋转。
具体代码表示:
while (parent != _pHead && parent->_color == Red)
{
Node* grandfather = parent->_parent;
assert(grandfather); //祖父节点有效性
if (parent == grandfather->_left)
{
Node* uncle = grandfather->_right; //情况一叔叔在祖父右
if (uncle && uncle->_color == Red) //叔叔在右且为红
{
parent->_color = Black;
uncle->_color = Black;
grandfather->_color = Red;
cur = grandfather; //继续向上调整
parent = cur->_parent;