文章目录
除了AVL树,红黑树也是被广泛使用的平衡二叉树。两者都解决了二叉搜索树的平衡问题。
关于AVL树,之前博客有介绍: AVL树
红黑树的概念
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或 Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路 径会比其他路径长出俩倍(最长路径不会超过最短路径的二倍)。因而是接近平衡的。
比如下图就是一棵红黑树
红黑树的性质
所谓红黑树,不仅仅是一个二叉搜索树,还要必须满足以下规则:
- 每个节点不是黑色就是红色
- 根节点是黑色
- 如果节点为红色,则它的两个孩子结点是黑色的。(不能出现连续的红色)
- 任意节点到NULL(树尾端)的任何路径,所包含黑色节点的个数必须相同。
- 每个叶子结点都是黑色的(此处的叶子结点指定是空结点)。
对于第四点规则:任意节点到NULL(树尾端)的任何路径,所包含黑色节点的个数必须相同。
比如上图从根节点到任意NULL节点,所包含的黑色节点都是3个。
根据红黑树的性质可以得出一颗红黑树的最短路径全部是由黑色结点构成
而最长路径是一黑一红结点构成的路径(因为要满足红黑第4点性质)
所以对于一颗红黑树来说,最长路径不会超过最短路径的二倍
红黑树结点定义
结点直接定义为K,V模型,
cpp
enum Color
{
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;//存储键值对
Color _color; //使用枚举值定义结点的颜色
RBTreeNode(const pair<K,V>& kv)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_kv(kv)
,_color(RED)
{}
};
这里再结点定义的时候,默认颜色初始化为红色。也就是说,当插入一个新节点时,默认是红色的。这是为什么?
- 如果新插入结点是黑色的,那么一定会破坏规则4,插入新结点的这条路径黑色结点个数就会比其他路径多一个。这时就需要对红黑树进行调整。
- 如果插入结点是红色的,如果父结点是红色的,就会出现连续红色结点破坏了规则3。但如果父结点是黑色的,插入后就仍然满足红黑树的性质,无需进行调整。
所以在构造结点的时候默认初始化为红色更为合理。
红黑树的插入
红黑树是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树的插入可分为两步:
- 按照二叉树的规则插入新节点
cpp
bool insert(const pair<K,V>& kv)
{
//空树直接做为根结点
if (_root == nullptr)
{
_root = new Node(kv);
_root->_color = BLACK;
return true;
}
//确定插入的位置
Node* cur = _root;
Node* parent = nullptr;
while (cur != nullptr)
{
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);
if (kv.first < parent->_kv.first)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;
return true;
}
- 检测新节点插入后,红黑树的性质是否遭到破坏
因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何 性质,则不需要调整;但当新插入节点的双亲节点颜色为红色时,就违反了性质三不能有连 在一起的红色节点,此时需要对红黑树分情况来讨论:
约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点
1. 情况一:cur为红,p为红,g为黑,u存在且为红
cur新节点肯定为红,p也只能为红,p如果为黑无需处理。cur和p都为红,g只能为黑。如果此时g为红说明插入前就不是红黑树。
只用看u节点是否存在和其颜色(对应的是三种情况)。 如果u存在且为红
比如对下图插入新节点-1(其中c、d、e是任意一种包含一个黑色节点的红黑子树)
插入新节点cur后,违反了红黑树的性质3。出现了相同的红色节点。并且其叔叔节点u是红色,就需要对其进行颜色调整。
将p节点和u节点变为黑色,再将g节点变为红色。如下图:
此时g节点的父结点是红色,仍然破坏性质3,出现了连续的红色节点。(如果g节点的父结点是黑色的,就无需处理)还需要继续向上调整。
让g节点当做cur,继续向上调整。根据u节点的情况进行调整。下图u节点正好是红色,对应得是情况1,如果出现其他情况,分别对应得是下面分析得情况2和情况3进行处理。
注意: 插入在0或2的左右,处理情况都是一样的。因为u节点都是红色。只需进行颜色调整即可。
当新插入节数的叔叔存在且为数色的时候,解决方式:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整。
2. 情况二: cur为红,p为红,g为黑,u存在且为黑
当u存在时且为黑,这种情况一定是由情况1继续向上调整造成的因为cur新节点肯定为红,p也只能为红,p如果为黑无需处理。cur和p都为红,g只能为黑。如果此时u存在且为黑
只能说明插入前就不是一棵红黑树。
当出现u存在且为黑的情况,一定是由情况一继续向上调整造成的,并且cur之前是黑色的(只用cur是黑色的才能说明其是红黑树),cur子树经过情况1调整之后由黑色变为了红色。
比如下面这种情况。
对下面这棵红黑树插入新节点5(插入再7或者10 的 左或右对应的都是情况1,这里以7的左为例)
插入新节点,引发情况1,进行颜色调整,如下。
此时cur为红,p为红,g为黑,u存在且为黑。需要对其进行旋转处理。
p是g的左,cur是p的左,进行右单旋处理。如下:
右单旋结束后,还需要对其进行颜色调整。p变黑,g变红即可。如下:
相反,如果u存在且为黑。p是g的右子树,cur是g的右子树,则需要进行左单旋+变色处理。(插入再16或19 的左或右对应的都是这种情况)
当情况1调整完后,p是g的左子树,cur是p的右子树时,这种情况就需要左右双旋处理了
比如下面这种情况:
对下面这课红黑树插入新节点
对应的是情况1,进行变色调整
颜色调整完之后此时,p是g的左子树,cur是p的右子树时,这种情况就需要左右双旋处理了(先左旋,再右旋)
以p为轴点进行左单旋(不用变色)
再以g为轴点进行右单旋
最后,将cur变黑,g变红即可
相反,如果u存在且为黑,且p是g的右子树,cur是p的左子树时,这种情况就需要右左双旋处理了。
比如下面红黑树插入新节点36之后,引发情况1进行变色。
此时 p是g的右子树,cur是p的左子树时,先以p为轴点进行右单旋,再以g为轴点左单旋,最后也是将cur变黑,g变红即可。
总结:
当u存在且为黑时,这种情况一定是由情况一引发的。当情况一进行颜色调整完之后,对应的四种情况分别是:
- p是g的右子树,cur是p的右子树。左单旋 + p变黑,g变红
- p是g的左子树,cur是p的左子树。右单旋 + p变黑,g变红
- p是g的右子树,cur是p的左子树。右左双旋 + cur变黑,g变红。
- p是g的左子树,cur是p的右子树。左右双旋 + cur变黑,g变红。
3. 情况三: cur为红,p为红,g为黑,u不存在
当u不存在时对应的是下面这种情况。cur一定是新插入节点,因为cur如果不是新节点,则cur和p节点一定有一个是黑色的(性质3)。cur和p有一个为黑时,不满足性质4,只能说明cur就是新插入节点。
cur是新插入节点,对应的也是四种情况。
- 当u不存在,p是g的左子树,cur是p的左子树。 右单旋 + p变黑,g变红。
- 当u不存在,p是g的右子树,cur是p的右子树。 左单旋 + p变黑,g变红。
- 当u不存在,p是g的左子树,cur是p的右子树。 左右双旋 + cur变黑,g变红。
- 当u不存在,p是g的右子树,cur是p的左子树。 右左双旋 + cur变黑,g变红。
代码实现(左旋和右旋的代码和AVL树的一样)
cpp
bool insert(const pair<K,V>& kv)
{
//空树直接做为根结点
if (_root == nullptr)
{
_root = new Node(kv);
_root->_color = BLACK;
return true;
}
//1、 确定插入的位置
Node* cur = _root;
Node* parent = nullptr;
while (cur != nullptr)
{
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);
if (kv.first < parent->_kv.first)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;
//3、若插入结点的父结点是红色的,则需要对红黑树进行调整
while (parent != nullptr && parent->_color == RED)
{
Node* grandfahter = parent->_parent; //parent为红色,grandfahter一定存在
if (grandfahter->_left == parent) //parent是grandfather左孩子的情况
{
Node* uncle = grandfahter->_right;//uncle若存在,一定是其右孩子
if (uncle != nullptr && uncle->_color == RED)//情况一:u存在且为红
{
//颜色调整
parent->_color = BLACK;
uncle->_color = BLACK;
grandfahter->_color = RED;
//继续向上调整
cur = grandfahter;
parent = cur->_parent;
}
else //情况2+3(u不存在/u存在且为黑)
{
//cur是parent的左
/* g
* p u
* c
*/
if (cur == parent->_left)
{
//右旋
RotateR(grandfahter);
//更新颜色
parent->_color = BLACK;
grandfahter->_color = RED;
}
else//cur是parent的右
{
/* g
* p u
* c
*/
//左右双旋(先以p为旋点左旋,在以g为旋点右旋)
RotateL(parent);
RotateR(grandfahter);
// cur变黑,g变红
cur->_color = BLACK;
grandfahter->_color = RED;
}
break;
}
}
else //parent是grandfather的右孩子
{
Node* uncle = grandfahter->_left; //uncle若存在一定是其左孩子
if (uncle != nullptr && uncle->_color == RED)//u存在且为红
{
//颜色调整
parent->_color = BLACK;
uncle->_color = BLACK;
grandfahter->_color = RED;
//继续向上调整
cur = grandfahter;
parent = cur->_parent;
}
else//u不存在/u存在为黑
{
//cur是parent的右
/* g
* u p
* c
*/
if (cur == parent->_right)
{
//左旋
RotateL(grandfahter);
parent->_color = BLACK;
grandfahter->_color = RED;
}
else
{
//cur是parent的左
/* g
* u p
* c
*/
//右左双旋(先以p为轴点右旋,再以g为轴点左旋)
RotateR(parent);
RotateL(grandfahter);
// cur变黑,g变红
cur->_color = BLACK;
grandfahter->_color = RED;
}
break;
}
}
}
//根节点一定为黑
_root->_color = BLACK;
return true;
}
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* parent_parent = parent->_parent;
//让subRL结点作为parent结点的右子树 更新完之后处理subRL_parent;
parent->_right = subRL;
if (subRL != nullptr)
{
subRL->_parent = parent;
}
//让parnet做为subR的左子树 更新完之后处理parent的_parent
subR->_left = parent;
parent->_parent = subR;
//subR做为这颗最小不平衡子树的根节点
if (parent_parent == nullptr)
{
_root = subR;
_root->_parent = nullptr;
}
else
{
if (parent_parent->_left == parent)
{
parent_parent->_left = subR;
}
else
{
parent_parent->_right = subR;
}
subR->_parent = parent_parent;
}
}
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
Node* parent_parent = parent->_parent;
//让subLR节点做为parent节点的左子树 更新完之后处理subLR的_parent;
parent->_left = subLR;
if (subLR != nullptr)
{
subLR->_parent = parent;
}
//让parent节点做为subL的右子树 更新完之后处理parent的_parent
subL->_right = parent;
parent->_parent = subL;
//让这颗最小不平衡子树的parent节点做为subL的右子树
if (parent_parent == nullptr)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (parent_parent->_left == parent)
{
parent_parent->_left = subL;
}
else
{
parent_parent->_right = subL;
}
subL->_parent = parent_parent;
}
}
红黑树的验证
红黑树的检测分为两步:
- 检测其是否满足二叉搜索树(中序遍历是否为有序序列)
cpp
void Inorder()
{
_Inorder(_root);
}
void _Inorder(Node* root)
{
if (root == nullptr)
{
return;
}
_Inorder(root->_left);
cout << root->_kv.first << ' ';
_Inorder(root->_right);
}
- 检测其是否满足红黑树的性质
- 检查根节点是否是黑色
- 检查每条路径的黑色节点个数是否相等
- 检查是否出现连续的红色节点
cpp
bool IsRBtree()
{
//根节点是红色肯定不是红黑树
if (_root->_color == RED)
{
cout << "根节点是红色" << endl;
return false;
}
//红黑树每条路径的黑色节点个数都相等,给定一个基准值进行判断每条路径的黑色节点个树是否相等
int reference_value = 0;
Node* cur = _root;
while (cur != nullptr)
{
if (cur->_color == BLACK)
{
reference_value++;
}
cur = cur->_left;
}
//检查每条路径的黑色节点个数是否相等,并检查是否出现连续的红色节点
return _Check(_root, 0, reference_value);
}
bool _Check(Node* root, int blacknum, int reference_value)
{
if (root == nullptr)
{
if (reference_value != blacknum)
{
cout << "某条路径黑色节点的数量不相等" << endl;
return false;
}
else
{
//cout << "黑色节点个数相等" << endl;
return true;
}
}
if (root->_color == BLACK)
{
blacknum++;
}
if (root->_color == RED && root->_parent != nullptr && root->_parent->_color == RED)//root节点的父结点为红色则出现连续的红色节点
{
cout << "出现连续的红色节点" << endl;
return false;
}
return _Check(root->_left, blacknum, reference_value)
&& _Check(root->_right, blacknum, reference_value);
}
参考源码
- gitee 红黑树
- 菜鸟一枚,写的不好的地方请各位大佬多多包涵,手下留情。