【C++】---红黑树详解
- 一、什么是红黑树?
- 1、概念
- 2、性质
- 3、四个规则
- 二、红黑树的定义
- 1、红黑树 结点 定义
- (1)将新插入的结点 设置为黑色
- (2)将新插入的结点 设置为红色
- 2、红黑树的定义
- 三、红黑树插入
- 1、插入节点
- 2、控制颜色
- (1)父亲为黑色
- (2)父亲为红色
- 四、红黑树颜色处理
- 1、cur红、p红、g黑、u存在且为红
- (1)抽象图
- (2)具象图
- 2、cur红、p红、g黑、u不存在 或者(u存在且为黑)
- (1)单旋
- ①抽象图:
- ②具象图:
- (2)双旋
- ①抽象图:
- ②具象图:
- 3、红黑树 结点 的插入代码
- 五、红黑树查找
- 六、检查红黑树是否平衡
- 七、红黑树的遍历
- 八、红黑树的验证
- 九、对Insert插入 总结、红黑树的注意事项:
- (1)插入 总结
- (2)注意事项
一、什么是红黑树?
1、概念
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。
2、性质
最长路径不超过最短路径的二倍
(路径:就是从 根节点 开始到 叶子节点 结束为止,叫做一条路径!)
3、四个规则
- 每个结点不是红色就是黑色
- 根节点是黑色的
- 红色节点不能连续!
- 红黑树的每条路径上的黑色节点数量必须相同!
只要满足以上4个规则,就一定符合,红黑树最长路径不超过最短路径的二倍。
但是反过来就不一定:
二、红黑树的定义
1、红黑树 结点 定义
红黑树相比于AVL树多了一个颜色,因此我们需要一个成员变量来储存颜色,而AVL树是一种严格 控制高度平衡 的二叉搜索树,但是红黑树是一种近似平衡的二叉搜索树,所以不需要平衡因子。
所以说可以这样区分和理解:
AVL树 :它需要平衡因子!
红黑树:不需要平衡因子,但是红黑树需要一个成员变量来储存颜色!
但是目前有一个问题,因为构造函数的时候红黑树 需要设置颜色,那么我们将新插入的节点设置为红色还是黑色呢?
(这样可以回归到我们的4个规则,主要就是维持规则3和规则4)
(1)将新插入的结点 设置为黑色
假如将新节点初始化成黑色,不管父亲是黑色还是红色,一定会破坏规则(4),并且影响其他路径,影响范围广。
①当父亲是黑色时,破坏了规则(4)
②当父亲是红色时,也破坏了规则(4)
所以总之如果将新插入的节点设置为黑色,你无论怎么插入它一定会破坏规则4。
(2)将新插入的结点 设置为红色
假如将新插入节点置为红色,会有以下两种情况:
①当父亲是黑色时,没有破坏规则(3),也没有破坏规则(4)
②当父亲是红色时,破坏了规则(3),但是只需要改变一下颜色即可
虽然破坏了规则三,但是规则三这个规则可以很容易就维护了你规则四如果破坏了,你需要每条路径都要改变,每条路径都受到影响了,根本维护不过来。
因此节点要初始化成红色。
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) // 将新插入的节点 初始化为 红色!
{
}
};
2、红黑树的定义
template<class K,class V>
class RBTree
{
typedef RBTreeNode<K, V> Node;
//构造函数
RBTree()
:_root(nullptr)
{}
private:
Node* _root;
};
三、红黑树插入
1、插入节点
插入节点分为2步:
(1) 先查找,如果树中已存在该节点,则插入失败
(2)树中不存在该节点,插入
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)
{
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; // 不允许重复 相同的直接false
}
}
cur = new Node(kv);
cur->_col = RED;// 默认:新插入的结点 为红色节点!
if (kv.first < parent->_kv.first)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;
// 此时已经算是插入上了,如果cur的parent是黑色就此结束,但是红色就需要调整了!
2、控制颜色
所以在插入节点之后,为了满足红黑树的性质,还需要调整节点颜色:
(1)父亲为黑色
如果父亲是黑色,那么不需要调整,4个规则都满足,插入已完成
(2)父亲为红色
如果父亲是红色,违反了规则(3),需要调整颜色
四、红黑树颜色处理
首先需要明白一点就是:什么时候要对红黑树的颜色进行处理?
那肯定是新插入节点是红色,这是已经提前设置好的,肯定是违反了规则三,新插入节点
cur,他的父亲肯定是红色,所以才违反了规则三。那既然父亲是红色节点,那他必然有爷
爷,因为红色节点不能当作根节点,所以说cur的爷爷必然是存在的,并且一定是黑色节
点,所以说一个红黑树这三个节点已经确定了!
①cur是红色
②父亲(parent)是红色
③爷爷(grandfather)是黑色
只有叔叔不确定,叔叔就是爷爷的左子树或者右子树,所以说变量就是叔叔,他是什么颜色
或者存在不存在,我们就以叔叔(uncle)这个变量来进行分类讨论!
1、cur红、p红、g黑、u存在且为红
当cur为红,p为红,g为黑,u存在且为红时,为了满足红黑树的性质,处理方法:变色
将p和u变黑,g变红
(1)抽象图
如下a、b、c、d、e都是子树:
(1)假如g是根节点,根据红黑树性质,根节点必须是黑色,那就把g再变黑就好了
(2)假如g不是根节点,g是子树,那么g还有parent节点。
①如果g的parent是黑色,满足红黑树性质,那么停止调整。
②如果g的parent是红色,就破坏了规则(3),那么还需要继续向上调整。
调整方法为:把g当作cur,继续向上调整,直到p不存在,或p存在且为黑停止。
(2)具象图
①g是根节点,直接把p和u变黑,g变红
②g不是根节点,g是子树,把p和u变黑,g变红之后,还要继续向上调整,直到p不存在,或p存在且为黑停止
2、cur红、p红、g黑、u不存在 或者(u存在且为黑)
这种情况下,g、p、cur形成直线,先看cur为红,p为红,g为黑,u为黑的情况
这是由情况一cur红,p红,g黑,u存在且为红处理以后变换而来,比如以下情况:
在这种情况下,cur原来的颜色一定是黑色,只不过在处理的过程当中,将cur的颜色变成了红色,所以cur不是新增节点,而是新增节点的g节点。
他这种(cur红、p红、g黑、u不存在 或者(u存在且为黑) )一定是有2层,x>=2。
解释如下:
(1)单旋
①抽象图:
如下a、b、c、d、e都是子树,由于要旋转,所以要分为两种情况:当p是g的左子树,cur是p的左子树时,g右单旋,p变黑,g变红:
当p是g的右子树,cur是p的右子树时,g左单旋,p变黑,g变红:
②具象图:
(2)双旋
双旋也就是 在反方向!
这种情况下g、p、cur形成折线,先看cur为红,p为红,g为黑,u为黑的情况:
①抽象图:
当p是g的左子树,cur是p的右子树时,处理方法:p左单旋,g右单旋,cur变黑,g变红:
当p是g的右子树,cur是p的左子树时,处理方法:p右单旋,就变成了情况二(单纯的一边高)
②具象图:
当p是g的左子树,cur是p的右子树时,将p左单旋,g右单旋,cur变黑,g变红:
当p是g的右子树,cur是p的左子树,p右单旋,g左单旋,p变黑,g变红
再看cur为红,p为红,g为黑,u不存在的情况,u不存在的情况更为简单:
当p是g的左子树,cur是p的右子树时, 将p左单旋,g右单旋,cur变黑,g变红
当p是g的右子树,cur是p的左子树,p右单旋就变成了情况二:
3、红黑树 结点 的插入代码
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)
{
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;
}
}
cur = new Node(kv);
cur->_col = RED;// 默认:新插入的结点 为红色节点!
if (kv.first < parent->_kv.first)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;
// 此时已经算是插入上了,如果cur的parent是黑色就此结束,但是红色就需要调整了!
while (parent && parent->_col == RED)
// 这里的 parent && parent->_col == RED 中的 第1个parent就是来判断g是不是root
{
Node* grandfather = parent->_parent;
// 1、变量uncle在grandfather的右边
if (parent == grandfather->_left)
{
//(1)uncle存在&&为红 -------------// 变色
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
//还要判断 此时的grandfather是不是root
// ①g == root只需要让 g再次变黑即可!
// ②g != root 把g赋值给cur 继续往上更新!
cur = grandfather;
parent = cur->_parent;
}
// (2)uncle不存在 || uncle存在 && 为黑色 ----// 变色 + 旋转
else
{
/*parent->_col = BLACK;
grandfather->_col = RED;*/
// 旋转:单旋 、双旋
if (cur == parent->_left)
{
// (1)单旋
// g
// p u
// c
先变色 再旋转!
//parent->_col = BLACK;
//grandfather->_col = RED;
//RS_rotation(grandfather);
// 先旋转 再变色!
RS_rotation(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
// (1)双旋
// g
// p u
// c
先变色 再旋转!
//cur->_col = BLACK;
//grandfather->_col = RED;
//LS_rotation(parent);
//RS_rotation(grandfather);
// 先旋转 再变色!
LS_rotation(parent);
RS_rotation(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;// 因为else这种情况 变色:没有破坏规则 旋转:维持了红黑树的性质 所以完成之后可以直接 退出循环!
}
}
else
// 2、变量uncle在grandfather的左边
{
Node* uncle = grandfather->_left;
// (1)
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else // (2)
{
if (cur == parent->_right)
{
①
//parent->_col = BLACK;
//grandfather->_col = RED;
//LS_rotation(grandfather);
// 先旋转 再变色!
LS_rotation(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
②
//parent->_col = BLACK;
//grandfather->_col = RED;
//RS_rotation(parent);
//LS_rotation(grandfather);
// 先旋转 再变色!
RS_rotation(parent);
LS_rotation(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;// 因为else这种情况 变色:没有破坏规则 旋转:维持了红黑树的性质 所以完成之后可以直接 退出循环!
}
}
}
_root->_col = BLACK;
return true;
}
红黑树 先旋转再变色 or 先变色再旋转 都一样!!!
五、红黑树查找
//查找
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_kv.first < key)//key比当前节点小,就向右查找
{
cur = cur->_right;
}
else if (cur->_kv.first > key)//key比当前节点大,就向左查找
{
cur = cur->_left;
}
else//找到了
{
return cur;
}
}
return nullptr;//空树,直接返回
}
六、检查红黑树是否平衡
检查是否平衡还是要用到递归子函数
(1)需要判断是否满足红黑树的4条性质
(2)计算 最左 / 最右 路径上的黑色节点个数,把它当做参考值:refNum!
递归路径时,需要计算每个路径上的黑色节点个数(blackNum)是否和最左路径上的节点个数相等
public:
bool Is_balance()// 主要就是 判断 这个树 符不符合 那4个规则!
{
if (_root->_col == RED)
{
return false;// 不符合 规则2 :根节点必须是黑色!
}
// 创建 一个 refNum 当做参照物 用来检验是否和blackNum相等 从而得出结论:blackNum思路正确 && 整个树符合规则4、
// refNum 可以是 最左侧/最右侧!
// int refNum = 1; 因为根节点是黑色 为啥初始化的时候 不直接给1?而是前置++
int refNum = 0;
Node* cur = _root;
while (cur)
{
if (cur->_col == BLACK)
{
// refNum++; 因为根节点是黑色 所以要用前置++
++refNum;
}
cur = cur->_left;
}
return _Check(_root, 0,refNum);
}
private:
// 创建一个 blackNum 用来标记 每条路径下 黑色节点的数目!(递归形参!)
bool _Check(Node* root,int blackNum,const int refNum)
{
if (root == nullptr)
{
if (refNum != blackNum)
{
cout << "存在黑色结点不相等的 路径" << endl;
return false;
}
return true;
}
if (root->_col == RED && root->_parent->_col == RED)
{
cout <<root->_kv.first << "存在连续的红色结点!" << endl;
return false;// 不符合规则3: 不能存在连续的红色节点!
}
if (root->_col == BLACK)
{
blackNum++;
}
// 递归遍历整个树!
return _Check(root->_left, blackNum, refNum) &&
_Check(root->_right, blackNum, refNum);
}
七、红黑树的遍历
//遍历
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << endl;
_InOrder(root->_right);
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
八、红黑树的验证
void TestRBTree1()
{
int a[] = { 4,2,6,1,3,5,15,7,16,14,8,3,1,10,6,4,7,14,13 };
RBTree<int, int> t1;
for (auto e : a)
{
if (e == 10)
{
int i = 0;
}
t1.Insert({ e,e });
cout << "Insert:" << e << "->" << t1.Is_balance() << endl;
}
t1.InOrder();
cout << t1.Is_balance() << endl;
}
九、对Insert插入 总结、红黑树的注意事项:
(1)插入 总结
三种情况:
(1)uncle存在且为red
(2)uncle存在且为black
(3)uncle不存在
实际上,情况二 和 情况三 可以合并 为同一种情况!
(2)注意事项
1、对参考值 refNum 的初始化 refNum=0 、refNum必须是前置++
2、为啥要将 refNum 和 blackNum 判断是否相等 放在 if(root是否等于nullptr)里面?
3、双旋的注意事项:
好了,今天的分享就到这里了
如果对你有帮助,记得点赞👍+关注哦!
我的主页还有其他文章,欢迎学习指点。关注我,让我们一起学习,一起成长吧!