什么是红黑树?
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或
Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制。
受下面的性质3和性质4的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是接近平衡的。
红黑树的性质:
1、每个结点不是红色就是黑色
2、树的根节点是黑色的
3、同一条路径中不能出现连续的红色结点
4、每条路径均包含相同数目的黑色结点
红黑树的实现:
结点的定义:
用枚举来限定结点的颜色是红色还是黑色,也可以用 true / false 来区分红黑色。
enum Color
{//枚举,限定红黑树结点的颜色
BLACK,
RED
};
template<class K, class V>
struct RBTreeNode
{
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _parent;
pair<K, V> _kv;
Color _col;
RBTreeNode(const pair<K, V>& kv)
:_right(nullptr)
, _left(nullptr)
, _parent(nullptr)
, _kv(kv)
, _col(RED)//初始化为红色,方便插入
{ }
};
插入 Insert:
假设我们已经找到要插入的位置了,我们设想下面几种情形:
1、插入时新增结点应该设置为什么颜色?
根据红黑树每条路径具有相等数量的黑色结点,如果设置为黑色,相当于我们主动破坏了游戏规则,红黑树中有某条路径的黑色节点的个数比其他路径多,这时我们很难再让红黑树的黑色结点的个数达到平衡,所以插入时新增结点的颜色设置为红色比较合适。
2、假设插入的结点为 cur,parent 为 cur 的父亲节点,grandfather 为parent 的父亲结点,uncle 是 grandfather 的另一个孩子
a.如果 parent 的颜色为黑色,因为我们没有破坏红黑树的游戏规则(没有出现连续的红色结点,且每条路径黑色节点的数量相等),这时候插入可以结束了
b.如果 parent 的颜色为红色 ,我们用简图来表示:
由于 parent 为红色,则 grandfather 也为红色,否则插入前这棵红黑树已经存在连续的红色节点,已经不平衡了,此时 uncle 的颜色可能为红色,可能为黑色,我们可以根据 uncle 的颜色来讨论下一步怎么变色。
1、uncle存在且为红
由于出现连续的红色节点,我们需要 cur 和 parent 中选一个结点变色,但我们不能改变 cur 的颜色,如果把 cur 的颜色改为黑色,这和一开始直接插入黑色结点没有区别,所以我们把 parent 变为黑色,parent 改为黑色之后,每条路径黑色结点的数量不相等,所以把 grandfather 改为红色,uncle 改为黑色。
因为 grandfather 的颜色变为红色了,可能 grandfather 的父亲也是红色结点,又出现连续的红色结点,这也是我们需要考虑的问题,故需要继续向上更新,直到更新后的 parent 为空 或者 parent 的颜色为黑色,更新停止。
cur = grandfather;
parent = cur->_father;
2、uncle不存在或uncle存在且为黑
假设从上面的情况向上更新后,发现 parent 为红色,出现连续的红色结点,uncle 存在且为黑,如果和上面一样,把 parent 变为黑色,grandfather 变为红色,此时每条路径黑色结点的数量不相等了,原本 grandfather 的右子树可能有2个黑色节点,现在变为 1 个了,所以仅靠变色已经无法使红黑树平衡了,需要先旋转,再变色,使红黑树再次平衡。(和 AVL 的旋转规则一样,对旋转规则不熟悉的读者,可以看上一篇 AVL 的实现)。
此时我们需要分类讨论:
a. 如果 uncle 为 grandfather 的右孩子,且 cur 为 parent 的左孩子
b. 如果 uncle 为 grandfather 的右孩子,且 cur 为 parent 的右孩子
c. 如果 uncle 为 grandfather 的左孩子,且 cur 为 parent 的左孩子
d. 如果 uncle 为 grandfather 的左孩子,且 cur 为 parent 的右孩子
uncle 不存在和 uncle 存在且为黑 是一样的处理方式。
变色完之后,已经不需要向上更新了,因为我们当前处理的子树的根已经变为黑色了,向上更新得到的子树也一样符合红黑树的规则,即整棵红黑树已经平衡了,插入可以结束了。
插入的最后,把红黑树的根变为黑色,防止在插入的变色过程中对根的颜色进行修改。
void RotateL(Node* parent)
{//左单旋,要转下来的结点作为parent
Node* subR = parent->_right;//父亲节点的右
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL)//避免空指针的解引用
subRL->_parent = parent;
subR->_left = parent;
Node* ppnode = parent->_parent;
parent->_parent = subR;
if (parent == _root)
{//如果转下来的结点是根
_root = subR;
subR->_parent = nullptr;
}
else
{//如果转下来的结点不是根
if (ppnode->_left == parent)//parent在ppnode的左边
{
ppnode->_left = subR;
}
else//parent在ppnode的右边
{
ppnode->_right = subR;
}
subR->_parent = ppnode;
}
}
void RotateR(Node* parent)
{//右单旋,要转下来的结点作为parent
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
subL->_right = parent;
Node* ppnode = parent->_parent;//先设好ppnode,因为下一句会修改parent的父亲节点,导致ppnode不是我们预想的
parent->_parent = subL;
if (parent == _root)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (ppnode->_left == parent)
{
ppnode->_left = subL;
}
else
{
ppnode->_right = subL;
}
subL->_parent = ppnode;
}
}
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 (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);
if (parent->_kv.first > kv.first)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;
while (parent && parent->_col == RED)
{//当parent不为空,且为红色时进入while
Node* grandfather = parent->_parent;
if (parent == grandfather->_left)
{//父亲在爷爷的左
Node* uncle = grandfather->_right;//叔叔在爷爷的右
if (uncle && uncle->_col == RED)
{
//情况一:叔叔存在且为红
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
//此时该子树的根grandfather的颜色为红,
//grandfather的上半部分可能会出现连续的红色结点,故需要向上更新
cur = grandfather;
parent = cur->_parent;
}
else
{
//叔叔不存在或叔叔存在且为黑
if (cur == parent->_left)
{
// g p
// p u --> c g
//c u
//g右旋
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;//该子树的根p 的颜色变为黑
}
else
{
// g c
// p u --> p g
// c u
//左右双旋
RotateL(parent);
RotateR(grandfather);
grandfather->_col = RED;
cur->_col = BLACK;//该子树的根cur 的颜色变为黑
}
break;//因为子树的根都变为黑了,没有继续向上更新的必要了
//因为再往上走,也不会出现破坏红黑树规则的情况(不会出现连续的红色结点)
}
}
else
{//父亲在爷爷的右
Node* uncle = grandfather->_left;//叔叔在爷爷的左
if (uncle && uncle->_col == RED)
{
uncle->_col = parent->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else
{
if (cur == parent->_left)
{
// g c
//u p --> g p
// c u
//右左双旋
RotateR(parent);
RotateL(grandfather);
grandfather->_col = RED;
cur->_col = BLACK;
}
else
{
// g p
//u p --> g c
// c u
//左单旋
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
_root->_col = BLACK;
return true;
}
判断红黑树是否平衡 IsBalance:
可以根据红黑树的性质,判断红黑树是否平衡:
1、红黑树的根为黑色:直接 if 判断根节点的颜色是否为黑色,判断之前,一定一定要先判断根是否存在,否则会出现空指针的解引用。
2、红黑树不存在连续的红色结点:假设 cur 结点为红色,如果通过判断 cur 结点的孩子的颜色,来判断是否存在连续的红色结点,是比较麻烦的,因为 cur 可能有 2 个孩子,是一对二的情况,反过来,如果直接根据 cur 和 cur 的父亲节点的颜色来判断,是比较容易的,因为 cur 只有 1个父亲节点。
3、每条路径的黑色结点的数量相等:我们可以先算出红黑树的最左边 / 最右边 的路径的黑色结点的数量 refblacknum,把它作为基准值,再去计算其他路径的黑色结点的个数,如果存在某条路径的黑色结点的个数和 refblacknum 不相等,返回 false,如果每条路径的黑色结点的数量都相等,返回 true。
bool Check(Node* cur, int blacknum, int refblacknum)
{
if (cur == nullptr)
{
//一条路径已经走完了
if (blacknum != refblacknum)
{//当前路径黑色结点的个数和基准值不相等
cout << "黑色节点数量不同" << endl;
return false;
}
return true;//当前路径黑色结点的个数和基准值相等
}
if (cur->_col == RED && cur->_parent->_col == RED)
{//出现连续的红色结点
cout << cur->_kv.first << "出现连续的红色结点" << endl;
return false;
}
if (cur->_col == BLACK)
{
blacknum++;
}
return Check(cur->_left, blacknum, refblacknum)
&& Check(cur->_right, blacknum, refblacknum);
}
bool IsBalance()
{
//检查根是否为黑色,记得先检查根是否存在,否则会出现空指针的解引用
if (_root && _root->_col == RED)
{
return false;
}
int refblacknum = 0;
Node* cur = _root;
while (cur)
{
//先计算最左路径的黑色结点,作为基准值
if (cur->_col == BLACK)
refblacknum++;
cur = cur->_left;
}
return Check(_root, 0, refblacknum);
}