目录
概念:
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或 Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路 径会比其他路径长出俩倍,因而是接近平衡的。
红黑树的性质/规则:
- 每个结点不是红色就是黑色
- 根节点是黑色的
- 如果一个节点是红色的,则它的两个孩子结点是黑色的 ,不存在两个连续的红色节点
- 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均 包含相同数目的黑色结点,每条路径都应该存在相同数量的黑色节点
- 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
从规则性质可以看出,红黑树最短的路径是全是黑节点的路径,而红黑树最长的路径则是一黑一红相互交替的路径,而最短路径和最长路径之间,最长路径是最短路径的2倍或是2倍不到。
同时如上图所示,NIL其实是红黑树的最后一个节点,这是为了方便红黑树最后统计路径的长度而特地创造出来的一种空的叶子节点,对应上第五点规则,但是大部分时期是不会把NIL计入树中,只是当作一个计数器。
红黑树的节点内部结构:
enum Colour
{
RED,
BLACK
};
template<class K, class V>//使用KV模型的红黑树
struct RBTreeNode
{
RBTreeNode<K, V>* _left;//左节点指针
RBTreeNode<K, V>* _right;//右节点指针
RBTreeNode<K, V>* _parent;//父节点指针,为了方便红黑树之后的旋转操作
pair<K, V> _kv;//存储KV模型的kv
Colour _col;//表明节点是红黑二色的指针
RBTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _col(RED)
{}
};
红黑树的基本结构:
template<class K, class V>
class RBTree
{
typedef RBTreeNode<K, V> Node;
public:
private:
Node* _root = nullptr;
};
关于红黑树的插入节点问题:
因为红黑树是搜索树的一种,所以可以利用搜索树的插入代码,其次红黑树最关键的一个问题:最新插入的节点是红节点还是黑节点问题。
因为红黑树的每一个节点都必须要有颜色,且红黑树的节点颜色必须遵循红黑树的规则,尤其是上面罗列出来的第三点规则和第四点规则:
- 如果一个节点是红色的,则它的两个孩子结点是黑色的 ,不存在两个连续的红色节点
- 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点,每条路径都应该存在相同数量的黑色节点
不存在两个连续的红色节点,每个路径都应该存在相同数量的黑色节点,通过插入的节点颜色不同,可以分析出,两种不同的情况:
- 插入红色节点遵循了每条路径上黑色节点的数量相同,但是打破了不能存在连续的红色节点的规则
- 插入黑色节点准寻了不能存在两个连续的红色节点,但是打破了每条路径上黑色节点数量相同的规则
不论,插入哪一种颜色必然都会破坏这两个重要规则之间的一个,所以只能按照破坏规则后,那个规则被破坏后,造成的严重性更大来判断。
最后得出,只能插入红色节点,因为入股破坏了“每条路径上的黑色节点数量相同” ,那么修复这个规则将变得十分的困难,因为每条路径上的黑色节点都要被修改,而破坏红色节点不能连续,则可以通过更改插入红色节点的这一条路径,一路从这条路径往上修改颜色,最长需要修改根节点的整个左子树或者右子树,最短可能不需要往上进行节点颜色的修改。
插入红节点的情况:
抽象图:
cur 表示 插入的新节点,p表示父节点,g表示祖父节点,u表示叔叔节点,其中的abcde表示的是各类节点的连接情况 ,其中abcde这些连接路径上,又有x个黑色节点。
在以上的抽象图可以看出:
叔叔节点是一个关键点,因为在父节点是黑节点时,不需要进行节点颜色的修改
而当父节点是红节点时,叔叔节点可以是红色、黑色、不存在,这三种状况,且如果要往上进行节点的颜色调整,也一定需要调整叔叔节点的颜色,而叔叔节点因为这三种情况,也会导致调整的方略会有所不同。
情况一 :cur为红,p为红,g为黑,u存在且为红
情况1详细分析,可以有3种情况,分别是x==0,x==1,x==2 分别表示abcde上黑色节点的数量x的不同变化
x==0:
如果x==0则表示abcde上这几几个路径上没有一个黑色节点,也因为不能破坏 “不能有连续的红色节点” ,所以导致cur就是新增加的红色节点,所以abcde这些路径不存在,而只需要将p和u进行颜色的修改,以及祖父节点的修改即可
且这种修改是一个循环交替的过程,需要修改完祖父节点后,继续往祖父节点上方继续判断和修改,而如果祖父节点是根节点,那么祖父节点必须是黑色的!
x==1:
如果x==1 则表示cde 三条路径的情况是上面四条路径之一,而ab两条路径则是只有一个红色节点,且新增加的节点就在a或者b路径的下方,且插入的是红色节点。
x==2:
x==2的情况并不好把握,所以一般及计算在内,但是可以在抽象图中进行表示。
情况二:u不存在 ,p是红节点,g是黑节点
如果叔叔节点不存在,那么为了维持“每条路径上的黑色节点数量一定要相同” ,那么abcde这些路径情况将会没有,且p不能是黑色,因为p是黑色,而u不存在会导致规则被破坏,而cur也不能是黑色,只能是红色,同时新增加的节点只能存在于cur节点的左节点或者右节点。
于是,这种情况们需要使用搜索树中的右单旋方式进行处理解决,将p变黑,g变红后,进行右单旋,如上图所示情况。
情况三:u存在且是黑色的
如果u存在且是黑色的,那么以x==1为例,因为cde三条路径上面都会有一个黑色节点,所以cur也必须是黑色的,那么a b两条路径只能是红色节点且只有一个红色节点,因为如果是ab路径上存在黑色节点,那么会破坏“每个路径上的黑色节点数量相同”这个规则。
所以最后插入的节点是在 红色节点a或者红色节点b 下方,而cur也只能是黑色,c路径上是下面mnpq的任意一种。
而因为叔叔u节点是黑色的,且叔叔节点下方的 两条路径 d e 只能是空的或者全红的,因为在当前的图来看,如果叔叔是黑色,且x==1,而cur是黑色节点,那么如果d e路径上只要有一个节点是黑色的,那么就会破坏“每个路径上的黑色节点数量相同”这个规则,所以de路径要么是空,要么是红色节点存在。
而又如上图所示,在进行更新节点颜色后,会导致“每个路径上的黑色节点数量相同”这个规则被破坏,所以需要进行旋转,进行右单旋使得每个路径上的黑色节点个数恢复一致性。
编辑代码:
if (_root == nullptr)
{//如果刚开始没有节点,插入的节点就是根节点!
_root = new Node(kv);
_root->_col = BLACK;
return true;
}
//寻找插入的位置,因为是KV模型所以需要找KV模型的frist进行查找位置
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_kv.first < kv.first)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_kv.first > kv.first)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
//进行节点的插入,插入的节点是红色节点,且在最后进行父亲节点的链接
cur = new Node(kv);
cur->_col = RED; // 新增节点给红色
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;//进行父亲节点的链接以方便之后的往上更新节点情况
// parent的颜色是黑色也结束
while (parent && parent->_col == RED)
{
// 关键看叔叔
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;
// 继续往上处理
cur = grandfather;
parent = cur->_parent;
}
else // 叔叔不存在,或者存在且为黑
{
if (cur == parent->_left)//在父亲节点是祖父的左节点,插入节点是父亲左节点
{//这种就是单旋
// g
// p u
// c
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
// g
// p u
// c
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
else//父亲节点是祖父节点的右子树情况
{
Node* uncle = grandfather->_left;
// 叔叔存在且为红,-》变色即可
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
// 继续往上处理
cur = grandfather;
parent = cur->_parent;
}
else // 叔叔不存在,或者存在且为黑
{
// 情况二:叔叔不存在或者存在且为黑
// 旋转+变色
// g
// u p
// c
if (cur == parent->_right)
{//父亲节点是祖父节点的右节点,插入节点是父亲节点的右节点这种就是单璇
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
// g
// u p
// c
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
_root->_col = BLACK;//无论如何根节点都得是黑色节点!
return true;
}
叔叔不存在且为黑的代码分析:
g是祖父节点,u是叔叔节点,p是父亲节点,c是新增节点,那么这种情况下,就是单旋处理,而下图就是双旋处理。
而双旋转就得以父亲进行左单璇,然后在以cur进行右单旋(以下图为例)
void RotateR(Node* parent)//右单旋
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
subL->_right = parent;
Node* ppNode = parent->_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;
}
}
void RotateL(Node* 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;
_root->_parent = nullptr;
}
else
{
if (ppNode->_right == parent)
{
ppNode->_right = subR;
}
else
{
ppNode->_left = subR;
}
subR->_parent = ppNode;
}
}
检查红黑树是否满足规则:
查找红黑树是否满足规则其实是重点查找红黑树是否满足以下两个规则:
- 如果一个节点是红色的,则它的两个孩子结点是黑色的 ,不存在两个连续的红色节点
- 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点,每条路径都应该存在相同数量的黑色节点
而对于两个规则的检查,其实可以利用不能存在两个红色节点连续存在的特点,检查每一个路径的黑色节点数量是否相同 ,因为不能存在两个连续的红色节点,那么每一个红色节点的父亲节点就只能是黑色节点。
而对于路径上黑色姐的数量,可以先查询一条路径上面的黑色节点数量,这条路径是最左或者最右路径。
bool IsBalance()
{
if (_root->_col == RED)//如果根节点是红色的就报错!
{
return false;
}
int refNum = 0;//参考路径上的黑色节点数量计数器
Node* cur = _root;
while (cur)//通过while遍历参考路径,并记录参考路径上的黑色节点数量
{
if (cur->_col == BLACK)
{
++refNum;
}
cur = cur->_left;//以最左路径为参考路径
}
return Check(_root, 0, refNum);
}
private:
bool Check(Node* root, int blackNum, const int refNum)
{
if (root == nullptr)//节点是空表示是根节点或者遍历到了空节点该路径结束
{
//cout << blackNum << endl;
if (refNum != blackNum)
{//判断是否和参考路径上的黑色节点数量一样,当路径走到头了才进行比较
cout << "存在黑色节点的数量不相等的路径" << endl;
return false;
}
return true;
}
//如果当前节点是红色节点,且它的父节点也是红色节点那么就报错!
if (root->_col == RED && root->_parent->_col == RED)
{
cout << root->_kv.first << "存在连续的红色节点" << endl;
return false;
}
if (root->_col == BLACK)
{//如果当前节点是黑节点,那么就更新当前路径上的黑色节点数量
blackNum++;
}
return Check(root->_left, blackNum, refNum)
&& Check(root->_right, blackNum, refNum);
//使用形参拷贝进行递归,拷贝的是当前路径上的黑节点数量!
}