目录
红黑树的概念
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的(AVL树是严格平衡因子)。
红黑树的性质
1.每个节点不是红色就是黑色
2.根节点是黑色的
3.如果一个节点是红色的,则它的两个孩子节点是黑色的(即不存在连续的红色节点)
4.对于每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点(每条路径都存在相同数量的黑色节点)
5.每个叶子节点都是黑色的(此处的叶子节点指的是空节点,NIL)
前4条规则就可以保证最长路径<=最短路径*2,因为最短的就是全是黑节点的路径,最长的就是一黑一红间隔的路径,在最极端的条件下仍能满足最长路径<=最短路径*2,我们不得不感慨,发明红黑树的前辈真是一个条件组合大师啊!
红黑树节点的定义
enum Color
{
BLACK,
RED
};
template<class K,class V>
struct RBTreeNode
{
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
Color _color;
pair<K, V> _kv;
RBTreeNode(const pair<K, V>& kv)
: _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _color(RED)
{}
};
在RBTreeNode的构造函数中,我们默认节点为红色,这是为什么呢?
反过来想,如果我们默认节点是黑色,当插入这个节点后,会违反性质3,把其他所有路径都得罪了, 后果还是很严重的;如果默认节点是红色,可能会违反性质4,后果轻一些。
红黑树的插入操作
红黑树是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树的插入可分为两步:
1.按照二叉搜索的树规则插入新节点
2. 新节点插入后,检测红黑树的性质是否造到破坏
由于新节点的默认颜色是红色,因此,如果其双亲节点是黑色,没有违反红黑树任何性质,则不需要调整;但是当新插入节点的双亲节点是红色,就会存在连续的红节点,违反性质3,此时需要分情况讨论:
为了方面描述,我们做一些约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点。
其实,有很多因素是确定的,cur是红色,p也是红色,g是黑色,唯一的变量是u的颜色以及u是否存在。
情况1:cur为红,p为红,g为黑,u存在且为红
注:a/b/c/d/e每条路径有x个黑色节点,x>=0。
cur和p均为红,违反了性质三,能否将p直接改为黑?
不可以,如果p直接改为黑,那么p所在路径将会导致违反性质4
g是否可以不变红?
不可以,因为g所在的树,可能是整棵树的子树,不变红,这棵子树路径黑色节点数量都+1,破坏规则4
解决方式:将p、u改为黑,g改为红,如果g不是根,然后g当做cur,继续向上调整;如果g是根,再把g变黑。
g当做cur,继续向上调整还要分情况:
1.g的父亲是黑色的,就结束了
2.g的父亲是红色的,还要继续处理
情况二: cur为红,p为红,g为黑,u不存在/u存在且为黑
说明:u的情况有两种
1.如果u节点不存在,则cur一定是新插入节点,如果cur不是新插入节点,则cur和p一定有一个节点颜色是黑色,就不满足性质4。
2.如果u节点存在,则其一定是黑色的,那么cur原来颜色一定是黑色的,现在看到红色是因为cur的子树在调整过程中将cur节点的颜色由黑色改成红色。
解决方式:p为g的左孩子,cur为p的左孩子,则进行右单旋;相反,p为g的右孩子,cur为p的右孩子,则进行左单旋转;p、g变色--p变黑,g变红。
情况三:cur为红,p为红,g为黑,u不存在/u存在且为黑
p为g的左孩子,cur为p的右孩子,则针对p做左单旋转;相反, p为g的右孩子,cur为p的左孩子,则针对p做右单旋转,则转换成了情况2。
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)
{
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);
cur->_color = RED;
if (cur->_kv.first < parent->_kv.first)
{
parent->_left = cur;
cur->_parent = parent;
}
else
{
parent->_right = cur;
cur->_parent = parent;
}
while (parent && parent->_color == RED)
{
Node* parent = cur->_parent;
Node* grandfather = parent->_parent;
if (parent == grandfather->_left)
{
Node* uncle = grandfather->_right;
if (uncle && uncle->_color == RED)
{
//叔叔存在且为红
parent->_color = BLACK;
uncle->_color = BLACK;
grandfather->_color = RED;
cur = grandfather;
parent = cur->_parent;
}
else
{
//叔叔不存在,或者存在且为黑
if (cur == parent->_left)
{
// g
// p u
// c
//单旋
RotateR(grandfather);
parent->_color = BLACK;
grandfather->_color = RED;
}
else
{
// g
// p u
// c
//左右双旋
RotateL(parent);
RotateR(grandfather);
cur->_color = BLACK;
grandfather->_color = RED;
}
break;
}
}
else
{
Node* uncle = grandfather->_left;
if (uncle && uncle->_color == RED)
{
//叔叔存在且为红
parent->_color = BLACK;
uncle->_color = BLACK;
grandfather->_color = RED;
//继续往上处理
cur = grandfather;
parent = cur->_parent;
}
else
{
//叔叔不存在,或者存在且为黑
if (cur == parent->_right)
{
// g
// u p
// c
//
RotateL(grandfather);
parent->_color = BLACK;
grandfather->_color = RED;
}
else
{
// g
// u p
// c
//
RotateR(parent);
RotateL(grandfather);
cur->_color = BLACK;
grandfather->_color = RED;
}
break;
}
cur = grandfather;
}
}
_root->_color = BLACK;
return true;
}
红黑树的验证
为了验证我们建立的树是红黑树,可以依次检查红黑树的几条性质,如果全部符合,那么就可以认为这棵树是红黑树:
1.根节点是黑色节点:这条性质很容易检查。
2.如果一个节点是红色的,则它的两个孩子节点是黑色的(即不存在连续的红色节点):这条性质在检查时,有两种思路:
1)如果遇到红色节点,去检查它的孩子节点是不是红色,但是还要检查它的左右两个孩子,比较麻烦;
2)如果遇到红色节点,去检查它的父亲节点是不是红色,由于父亲节点是唯一的,这样检查比较简单。
所以我们采用方式2)检查这条性质。
3.对于每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点(每条路径都存在相同数量的黑色节点)
为了测试性质3是否成立,可以记录下来每条路径黑色节点的数量。为了求出每条路径黑色节点的数量,我们可以使用深度优先遍历(DFS),每个节点记录一个值:根到当前节点路径中黑色节点的数量,递归时的形参就可以解决这个问题。我们可以任意计算一条路径作为参考值,比如,最左路径。
cpp
bool IsBalance()
{
int refNum = 0;
Node* cur = _root;
while (cur)
{
if (cur->_color == BLACK)
{
refNum++;
}
cur = cur->_left;
}
return Check(_root,0, refNum);
}
private:
bool Check(Node* root,int BlackNum,const int ref)
{
if (root == nullptr)
{
//cout << BlackNum << endl;
if (BlackNum != ref)
{
cout << "存在黑色节点的数量不相等的路径" << endl;
return false;
}
return true;
}
if (root->_color == RED && root->_parent->_color == RED)
{
cout << root->_kv.first << "->及其父亲节点构成连续的红色" << endl;
return false;
}
if (root->_color == BLACK)
{
BlackNum++;
}
return Check(root->_left, BlackNum,ref) && Check(root->_right, BlackNum,ref);
}
*******如果在验证是否为红黑树时报错,可以通过下面的步骤检查:********
1.先看是插入谁导致出现的问题
2.打条件断点,画出插入前的树
3.单步跟踪,对比图,分析细节原因
红黑树与AVL树的比较
红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O(log_2 N),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。