📒博客主页:Morning_Yang丶
🎉欢迎关注🔎点赞👍收藏⭐️留言📝
📌本文所属专栏:【C++拒绝从入门到跑路】
🙏作者水平有限,如果发现错误,敬请指正!感谢感谢!
文章目录
一、红黑树的概念
红黑树,是一种平衡二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是 Red 或 Black。通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出2倍(最长路径最多是最短路径的2倍),因而是接近平衡的。
AVL树和红黑树:
- AVL树:严格平衡(左右子树高度差不超过1),所以AVL树的查找、插入、删除效率高:O(logN),但插入和删除节点后,要维持树的平衡状态,做的旋转处理还是很多的。
- 红黑树:近似平衡(控制最长路径不超过最短路径的2倍),变了一种方式来控制树的平衡,相较于AVL树,没有那么严格。
红黑树更多是一种折中的选择,它舍弃平衡二叉树的严格平衡,换取节点插入时尽可能少的调整。
因为红黑树的旋转情况少于AVL树,使得红黑树整体性能略优于AVL树,不然map和set底层怎么会用红黑树呢,包括很多语言的库里面都用了红黑树。
二、红黑树的性质
2.1 性质
【核心 & 重要】
-
每个结点不是红色就是黑色
-
根节点是黑色的
-
如果一个节点是红色的,则它的两个孩子结点必须是黑色的(即红黑树里面没有连续的红色节点)
-
对于每个结点,从该结点到其所有可到达的叶结点的路径中,均包含相同数目的黑色结点
(即==每条路径都有相同数量的黑色节点==,注意:路径是走到 NIL 空节点)
-
每个 NIL 叶子结点都是黑色的(此处的叶子结点指的是空结点)
如图,这颗红黑树有11条路径,每条路径都有两个黑节点(不包括NIL)
【思考】
为什么满足以上性质后,就能保证 最长路径中节点个数不会超过最短路径中节点个数的2倍 了呢(不包括NIL)
当某条路径最短时,这条路径必然都是由黑色节点构成。当某条路径长度最长时,这条路径必然是由红色和黑色节点交替构成的(性质3限定了不能出现两个连续的红色节点)。
而性质4又限定了从任一节点到其每个叶子节点的所有路径必须包含相同数量的黑色节点,这么来说最长路径上的黑节点的数目和最短路径上的黑节点的数目相等。
最短路径:全是黑节点,最长路径:一黑一红,交替出现,所以最长路径刚好是最短路径的2倍。
2.2 红黑树和AVL树效率对比
对于一棵拥有 n 个内部结点(不包括NIL叶子结点)的红黑树,树的最大高度为 h = 2 l o g 2 ( n + 1 ) log_2(n + 1) log2(n+1)
当红黑树是一颗满二叉树时,高度最小 h = l o g 2 ( n + 1 ) log_2(n + 1) log2(n+1),当红黑树中最长路径刚好是最短路径2倍的时候,红黑树的高度最大 h = 2 l o g 2 ( n + 1 ) log_2(n + 1) log2(n+1)
如果数据量是10亿:
查找效率 | 查找次数(约等于) |
---|---|
AVL树: l o g 2 n log_2n log2n | 30 |
红黑树:2 l o g 2 n log_2n log2n | 60 |
【结论】:
- 虽然AVL树的查找效率优于红黑树,但对于现在的CPU,查找30次和60次是没有什么区别的,可以认为红黑树和AVL树的查找效率几乎是一样的,简化后为 O(log2n)。
- 红黑树整体性能略优于AVL树(因为红黑树旋转情况少于AVL树)。
- 红黑树的插入删除比AVL树更便于控制操作。
- 红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O(log2n),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。
三、红黑树的结构(KV模型)
和AVL树类似。
1、定义红黑树的节点结构
// 定义红黑颜色
enum Colour // 枚举类型,枚举值默认从0开始,往后逐个加1(递增)
{
RED,
BLACK,
};
// 红黑树节点的定义(KV模型)
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, const Colour col = RED)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_kv(kv)
,_col(col)
{
}
};
2、定义红黑树结构
// 红黑树的定义(KV模型)
template<class K, class V>
class RBTree
{
typedef RBTreeNode<K, V> Node;
private:
Node* _root;
public:
RBTree() :_root(nullptr) {
} // 构造函数
bool Insert(const pair<K, V>& kv); // 插入节点
void RotateL(Node* parent); // 左单旋
void RotateR(Node* parent); // 右单旋
bool CheckBlance(); // 检测红黑树是否平衡
void InOrder(); // 中序遍历
// ......
private:
void _InOrder(Node* root); // 中序遍历子函数
// ......
};
【思考】:在节点的定义中,为什么要将节点的默认颜色给成红色的?
- 如果插入黑色节点,一定会破坏性质4(每条路径的黑色节点数量相等),会影响其他路径,影响很大;
- 如果插入红色节点,可能会破坏性质3(树中不能出现两个连续的红色节点),只会影响当前路径,影响不大;
- 所以默认给红色。
四、红黑树的插入
红黑树是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树的插入可分为两步:
- 按照二叉搜索树的规则插入新节点
- 检测新节点插入后,红黑树的性质是否遭到破坏,然后进行平衡化操作
4.1 插入节点
思考:插入的新节点是红色好还是黑色好呢?
红色好,如果插入黑色,一定会破坏性质4(每条路径的黑色节点数量相等);
如果插入红色,可能会破坏性质3(树中不能出现两个连续的红色节点)
插入一个红色新节点后,检测红黑树的性质是否遭到破坏,分为2种情况:
- 如果其父节点颜色是黑色,没有违反红黑树任何性质,则不需要调整;
- 如果其父节点颜色是红色,则违反了性质3(树中不能出现两个连续的红色节点),此时需要对红黑树进行平衡化操作,分为以下几种情况来讨论👇
4.2 平衡化操作(🌟)
约定:cur 为当前插入的节点,p (parent)为父节点,g (grandfather)为祖父节点,u (uncle)为叔叔节点
调整的关键:主要是看 cur 的叔叔节点 u 是否存在,以及叔叔节点 u 的颜色。
👇注意:此处看到的树,可能是一颗完整的树,也可能是一颗子树。所以可能会一直调整到根节点才停止。
① 情况一
情况一:cur为红,p为红,g为黑,u存在且为红
对情况一进行平衡化操作:先调整颜色,再往上调整
无论父亲 p 是祖父 g 的左孩子还是右孩子,无论 cur 是父亲 p 的左孩子还是右孩子,处理方式都一样:
-
调整颜色:将 cur 的父亲 p 和叔叔 u 变黑,祖父 g 变红
g为什么变红?因为如果是子树,插入新节点前,这两个路径上都只有一个黑节点g,插入cur后要保持不变,改变 p 和 u 就要将g变红。
-
把祖父 g 当成新的 cur,往上调整(即往上检测新的子树破坏了性质,因为g的p可能是红色),分为以下情况: p存在且为黑或者p不存在时停止(判断cur是否为根,是就变黑)
- 如果 cur 父亲 p 不存在,说明 cur 就是根节点,调整到头了,此时将根节点改为黑色!
- 如果 cur 父亲 p 存在且为黑色,无需调整(没有违反任何性质)
- 如果 cur 父亲 p 存在且为红色,继续调整,判断是否产生了情况二或三:
【注意】:
情况一在向上调整的过程中,可能会产生情况二或三,处理方式:旋转(先要判断是哪种旋转) + 变色处理
② 情况二
情况二: cur为红,p为红,g为黑,u不存在 / u存在且为黑