🌈前言
本篇文章进行数据结构中RBTree(红黑树)的学习!!!
🌅红黑树
🌷1、RBTree的概念
概念:
- 红黑树:它是二叉搜索树的一种,区别在于在每个结点上增加一个存储位表示结点的颜色,可以是 “RED” 或 “RLACK”
- 它通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是"接近"平衡的
红黑树的规则:
- 根节点一定为黑色
- 如果一个节点为红色,则它的左右孩子均为黑色(没有连续的红节点)
- 对于每个结点,从该结点到其所有后代的叶结点的路径上,均包含相同数目的黑色结点
- 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
- 红黑树的最长路径的黑色节点不超过最短路径的二倍
🌸2、RBTree节点的定义
// 红黑树的颜色
enum Color
{
RED,
BLACK
};
// RBTree的节点
template <typename K, typename V>
struct RBTreeNode
{
RBTreeNode(pair<K, V> _kv)
: left(nullptr)
, right(nullptr)
, parent(nullptr)
, kv(_kv)
, col(RED)
{}
RBTreeNode<K, V>* left; // 节点的左孩子
RBTreeNode<K, V>* right; // 节点的右孩子
RBTreeNode<K, V>* parent; // 节点双亲
pair<K, V> kv; // 数值
Color col; // 颜色
};
注意:RB树节点与avl树节点相似,不同的是增加了节点的颜色
🌰3、RBTree的插入
红黑树是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树的插入可分为两步:
- 按搜索树的规则进行插入新的节点
// 红黑树的规则:1.根节点是黑色的,黑节点的左右孩子可能为红也可能为黑
// 2.如果一个节点为红色,则它的左右孩子节点为黑色(没有连续的红节点)
// 3.对于每个节点,从该节点到其所有后代叶节点的路径上,均包含相同的数目的黑色节点(每条路径包含相同数目的黑色节点)
// 4.叶节点(nullptr)是黑色的
bool insert(const pair<K, V>& kv)
{
if (root == nullptr)
{
root = new Node(kv);
root->col = BLACK; // 根节点为黑色
return true;
}
// 按搜索树规则插入
Node* cur = root;
Node* parent = nullptr;
while (cur != nullptr)
{
if (cur->kv.first > kv.first) {
parent = cur;
cur = cur->left;
}
else if (cur->kv.first < kv.first) {
parent = cur;
cur = cur->right;
}
else {
return false;
}
}
cur = new Node(kv);
// 如果插入节点是红色,可能破坏规则2 --- 如果插入节点是黑色,一定会破坏规则3(3很难处理)
cur->col = RED;
if (parent->kv.first > kv.first) {
parent->left = cur;
}
else {
parent->right = cur;
}
cur->parent = parent;
// ...
}
注意:
- 插入新节点的颜色必须为红色,因为是红色的话可能会破坏规则2
- 如果插入节点的颜色是黑色,则必定破坏规则3,规则3最难处理,最好别惹
- 检测新节点插入后,红黑树的性质是否造到破坏
前言:因为新节点的默认颜色是红色,因此:
如果其双亲节点的颜色是黑色,没有违反红黑树任何性质,则不需要调整;
如果新插入节点的双亲节点颜色为红色时,就违反了规则三:“不能存在连续的红色节点”,此时需要对红黑树分情况来讨论:
约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点💗💗💗
- 情况1,cur为红,p为红,g为黑,u存在且为红
解决方式:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整
// 存在连续红色节点时,需要一直向上调整
while (parent != nullptr && parent->col == RED) // 因为插入节点是红色,如果parent也是红色(连续红节点),那么需要调整
{
Node* grandfather = parent->parent; // cur的祖父
// 找叔叔时需要判断在祖父的哪一边
if (grandfather->left == parent)
{
Node* uncle = grandfather->right;
// 情况一:grandfather为黑,parent为红,uncle为红,cur为红 -- 需要调整g,p,u的颜色
if (uncle && uncle->col == RED)
{
parent->col = BLACK;
uncle->col = BLACK;
grandfather->col = RED;
// 调整的这颗树可能为局部子树,需要继续向上处理,祖父的父亲可能为红节点, 更新cur,parent
cur = grandfather;
parent = cur->parent;
}
else
{
Node* uncle = grandfather->left;
if (uncle != nullptr && uncle->col == RED)
{
parent->col = BLACK;
uncle->col = BLACK;
grandfather->col = RED;
cur = grandfather;
parent = cur->parent;
}
- 情况2:cur为红,p为红,g为黑,u不存在或u存在且为黑
// 存在连续红色节点时,需要一直向上调整
while (parent != nullptr && parent->col == RED) // 因为插入节点是红色,如果parent也是红色(连续红节点),那么需要调整
{
Node* grandfather = parent->parent; // cur的祖父
// 找叔叔时需要判断在祖父的哪一边
if (grandfather->left == parent)
{
Node* uncle = grandfather->right;
// 情况一:grandfather为黑,parent为红,uncle为红,cur为红 -- 需要调整g,p,u的颜色
if (uncle && uncle->col == RED)
{
parent->col = BLACK;
uncle->col = BLACK;
grandfather->col = RED;
// 调整的这颗树可能为局部子树,需要继续向上处理,祖父的父亲可能为红节点, 更新cur,parent
cur = grandfather;
parent = cur->parent;
}
// 情况二:grandfather为黑,parent为红,uncle为nullptr/黑,cur为红 -- 需要旋转+变色处理
else
{
// 右旋,插入节点为parent的左边,祖父为旋转点,parent为祖父的左边,uncle为祖父的右边
if (cur == parent->left)
{
// g
// p
// c
RotateR(grandfather);
// 更新颜色 -- 旋转后parent为根,根要变黑(根变黑后所有子树的路径的黑节点数量都加1) -- grandfather为根的子树,变红
parent->col = BLACK;
grandfather->col = RED;
}
}
else
{
Node* uncle = grandfather->left;
if (uncle != nullptr && uncle->col == RED)
{
parent->col = BLACK;
uncle->col = BLACK;
grandfather->col = RED;
cur = grandfather;
parent = cur->parent;
}
else
{
// 左旋,插入节点为parent的右边,祖父为旋转点,parent为祖父的右边,uncle为祖父的左边
// g
// p
// c
if (cur == parent->right)
{
RotateL(grandfather);
// 更新颜色
parent->col = BLACK;
grandfather->col = RED;
}
}
`
具体情况分析 + 解决方法:
- p为g的左孩子,cur为p的左孩子,则进行右单旋转;相反,
- p为g的右孩子,cur为p的右孩子,则进行左单旋转
- 旋转后更新颜色,p色黑色,g变红色
- 情况三:cur为红,p为红,u不存在或u存在且为黑
注意:情况1是直线的情况,而情况三是折线的情况,即cur插入的方向与p指向c的方向相反
具体图 + 解决方案:
p为g的左孩子,u为p的右孩子或不存在,c为p的右孩子时,使用左右双旋解决,左旋用p作为旋转点,,右旋使用g为旋转点,旋转后将g颜色改为红,c改为黑
p为g的右孩子,u为p的左孩子或不存在,c为p的左孩子时,使用右左双旋解决,右旋用p作为旋转点,,左旋使用g为旋转点,旋转后将g颜色改为红,c改为黑
- 完整插入代码 – 包括这里的情况三
bool insert(const pair<K, V>& kv)
{
if (root == nullptr)
{
root = new Node(kv);
root->col = BLACK; // 根节点为黑色
return true;
}
// 按搜索树规则插入
Node* cur = root;
Node* parent = nullptr;
while (cur != nullptr)
{
if (cur->kv.first > kv.first) {
parent = cur;
cur = cur->left;
}
else if (cur->kv.first < kv.first) {
parent = cur;
cur = cur->right;
}
else {
return false;
}
}
cur = new Node(kv);
// 如果插入节点是红色,可能破坏规则2 --- 如果插入节点是黑色,一定会破坏规则3(3很难处理)
cur->col = RED;
if (parent->kv.first > kv.first) {
parent->left = cur;
}
else {
parent->right = cur;
}
cur->parent = parent;
// 存在连续红色节点时,需要一直向上调整
while (parent != nullptr && parent->col == RED) // 因为插入节点是红色,如果parent也是红色(连续红节点),那么需要调整
{
Node* grandfather = parent->parent; // cur的祖父
// 找叔叔时需要判断在祖父的哪一边
if (grandfather->left == parent)
{
Node* uncle = grandfather->right;
// 情况一:grandfather为黑,parent为红,uncle为红,cur为红 -- 需要调整g,p,u的颜色
if (uncle && uncle->col == RED)
{
parent->col = BLACK;
uncle->col = BLACK;
grandfather->col = RED;
// 调整的这颗树可能为局部子树,需要继续向上处理,祖父的父亲可能为红节点, 更新cur,parent
cur = grandfather;
parent = cur->parent;
}
// 情况二:grandfather为黑,parent为红,uncle为nullptr/黑,cur为红 -- 需要旋转+变色处理
else
{
// 右旋,插入节点为parent的左边,祖父为旋转点,parent为祖父的左边,uncle为祖父的右边
if (cur == parent->left)
{
// g
// p
// c
RotateR(grandfather);
// 更新颜色 -- 旋转后parent为根,根要变黑(根变黑后所有子树的路径的黑节点数量都加1) -- grandfather为根的子树,变红
parent->col = BLACK;
grandfather->col = RED;
}
else // 第三种情况,左右双旋,cur插入parent的右边,且parent为g的左边,,形成折线
{
// g
// p
// c
RotateL(parent);
RotateR(grandfather);
// 更新颜色
grandfather->col = RED;
cur->col = BLACK;
}
break;
}
}
else
{
Node* uncle = grandfather->left;
if (uncle != nullptr && uncle->col == RED)
{
parent->col = BLACK;
uncle->col = BLACK;
grandfather->col = RED;
cur = grandfather;
parent = cur->parent;
}
else
{
// 左旋,插入节点为parent的右边,祖父为旋转点,parent为祖父的右边,uncle为祖父的左边
// g
// p
// c
if (cur == parent->right)
{
RotateL(grandfather);
// 更新颜色
parent->col = BLACK;
grandfather->col = RED;
}
else // 右左双旋,cur插入parent的左边,且parent为g的右边,形成折线
{
// g
// p
// c
RotateR(parent);
RotateL(grandfather);
// 更新颜色
grandfather->col = RED;
cur->col = BLACK;
}
break;
}
}
}
root->col = BLACK; // 调整完后,根节点可能是红,需要调整为黑
return true;
}
- 左旋与右旋代码
// 左旋,右边高,左边矮
void RotateL(Node* parent)
{
Node* subR = parent->right;
Node* subRL = subR->left;
parent->right = subRL;
if (subRL != nullptr)
subRL->parent = parent;
Node* PpNode = parent->parent;
subR->left = parent;
parent->parent = subR;
if (parent == root)
{
root = subR;
root->parent = nullptr;
}
else
{
if (PpNode->left == parent) {
PpNode->left = subR;
}
else {
PpNode->right = subR;
}
subR->parent = PpNode;
}
}
// 右旋,左边高,右边矮
void RotateR(Node* parent)
{
Node* subL = parent->left;
Node* subLR = subL->right;
parent->left = subLR;
if (subLR != nullptr)
subLR->parent = parent;
Node* PpNode = parent->parent;
subL->right = parent;
parent->parent = subL;
if (parent == root)
{
root = subL;
root->parent = nullptr;
}
else
{
if (PpNode->left == parent) {
PpNode->left = subL;
}
else {
PpNode->right = subL;
}
subL->parent = PpNode;
}
}
🌭4、红黑树的删除
【其他文章】
红黑树的删除不做讲解(比插入难很多,性价比低):《算法导论》或者《STL源码剖析》
- 这里我只实现了第一步,以搜索树规则删除节点(注意空指针访问问题)
bool erase(const K& key)
{
Node* cur = root;
Node* Cparent = nullptr;
while (cur)
{
if (cur->kv.first > key)
{
Cparent = cur;
cur = cur->left;
}
else if (cur->kv.first < key)
{
Cparent = cur;
cur = cur->right;
}
else // 找到开始删除
{
// 删除节点存在左节点
if (cur->right == nullptr)
{
// 右节点为空且没有父节点,说明只有右子树或只有一个根节点
if (Cparent == nullptr)
{
root = root->left;
if (root !=nullptr)
root->parent = nullptr;
}
else
{
if (cur == Cparent->left) {
Cparent->left = cur->left;
if (cur->left)
cur->left->parent = Cparent;
}
else
{
Cparent->right = cur->left;
if (cur->left)
cur->left->parent = Cparent;
}
}
}
// 删除节点存在右节点
else if (cur->left == nullptr)
{
// 右节点为空且没有父节点,说明只有右子树或只有一个根节点
if (Cparent == nullptr)
{
root = root->right;
if (root != nullptr)
root->parent = nullptr;
}
else
{
if (cur == Cparent->right)
{
Cparent->right = cur->right;
if (cur->right)
cur->right->parent = Cparent;
}
else
{
Cparent->left = cur->right;
if (cur->right)
cur->right->parent = Cparent;
}
}
}
else // 删除节点存在左右节点 -- 替换数据法
{
Node* Left_Max = cur->left;
Node* LMparent = cur;
while (Left_Max->right)
{
LMparent = Left_Max;
Left_Max = Left_Max->right;
}
// 交换Cparent左子树和删除节点的值
std::swap(Left_Max->kv.first, cur->kv.first);
std::swap(Left_Max->kv.second, cur->kv.second);
// Cparent链接cur中左/右节点
if (LMparent->left == Left_Max)
{
LMparent->left = Left_Max->left;
if (cur->left)
Left_Max->left->parent = LMparent;
}
else
{
LMparent->right = Left_Max->left;
if (Left_Max->left)
Left_Max->left->parent = LMparent;
}
cur = Left_Max;
}
while (Cparent && Cparent->col == RED)
{
Node* grandfather = Cparent->parent;
if (grandfather && grandfather->right == Cparent)
{
Node* uncle = grandfather->left;
break;
}
else
{
Node* uncle = grandfather->right;
break;
}
}
if (root)
root->col = BLACK; // 删除节点可能为根 -- 删除的处理是找替代节点,只有二个节点时让root指向下一个节点
delete cur;
return true;
}
}
return false;
}
🌮5、红黑树与AVL树的比较
红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O( l o g 2 N log_2 N log2N),红黑树不追
求绝对平衡,其只需保证最长路径不超过最短路径的2倍相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多
🌯6、 红黑树的应用
- C++ STL库 – map/set、mutil_map/mutil_set
- Java 库
- linux内核
- 其他一些库
本文含有隐藏内容,请 开通VIP 后查看