目录
引言
在上文中,我们介绍了AVLTree这种特殊的二叉搜索树,有着高度平衡的特性,完美的避免了单支
树的出现,但是实际上,我们大多运用的却不是AVLTree,而是这篇介绍的红黑树RBTree,原因
就在于AVLTree经常在插入元素的时候旋转,而每次旋转,其实都要消耗资源的,RBTree也会旋
转,但不会什么情况都进行旋转,减少了旋转次数,同时也保证了高度平衡,所以,RBTree的出
现和广泛应用是有其重要意义.
上面这棵树,如果是AVLTree则已经旋转;但红黑树则不会旋转.
1.红黑树的概念
红黑树,也是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或
Black.
通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其
他路径长出2倍,因而是接近平衡的.
一棵具体的RBTree具体有以下五点限制
1. 每个结点不是红色就是黑色
2. 根节点是黑色的
3. 如果一个节点是红色的,则它的两个孩子结点是黑色的
4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点 5. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
第一点,不需要多解释,毕竟如果还有其它颜色,那就不叫红黑树了
第二点,根节点是黑色,这是规定
第三点,保证了不可能连续出现红色节点的情况(连续黑色有可能)
那最短路径就是所有节点都是黑色节点,最长路径就是每两个黑色节点之间插入一个红色节点,做到了确保没有一条路径会比其他路径长出2倍
第四,五点,每条路径,包括黑色的空结点,含有相同数目的黑色节点
2.红黑树的模拟实现
2.1 红黑树节点
红黑树的结点,基本和AVLTree完全相同,不过AVLTree结点里面放的是平衡因子,而红黑树结点
放的则是红黑颜色,这里我们采取枚举的方式来区分红黑颜色的不同,实际上也可以采取不同方式
enum Colour {
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;
Colour _col;
//节点构造函数
RBTreeNode(const pair<K, V>& kv)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_kv(kv)
,_col(RED)
{}
};
2.2 插入分析
结点构造完毕后,我们就要考虑插入,也就是如何建一棵红黑树
建一棵红黑树实际和建AVLTree非常类似,当然调整策略会有所不同,不过记住三个原则,建一棵
红黑树实际比建一棵AVLTree树可能还要简单
Principle1.插入节点都为红色
解释:条件4要求红黑树每条路径的黑色节点个数都要相同,假如我们选择插入黑色节点,那就会
同时破坏所有路径,需要对所有路径进行调整;而插入红色节点,则只需要在局部进行调整,因此
我们人为限定,插入的节点都为红色,节点初始化默认为红色
Principle2.根节点为黑色,等效为与根节点相连的两条分路各有一个黑色节点(如图)
解释:根节点为两条路径的重合节点,假如它为黑色,可以等效为每条路径都有一个黑色节点
Principle3.调整的关键在于叔叔节点,调整同时也时刻记住平衡高度
那介绍完三个原则后,我们就可以开始着手建一棵红黑树了
红黑树是一棵节点附带颜色的搜索二叉树,先是一棵搜索二叉树,再是节点附带颜色
因此,建造一棵红黑树的整体结构,前面就是先构建一棵搜索二叉树,再调整节点的颜色
假如刚开始没有节点,则直接让新插入节点成为根节点,并让它的颜色为黑色
假如已经有根节点,则按照搜索二叉树的规则进行建树即可
//假如刚开始没有节点,直接使其成为根即可
//满足规则,根节点必须为黑色
if (_root == nullptr)
{
_root = new Node(kv);
_root->_col = BLACK;
return true;
}
//同样满足二叉树的搜索规则,先找到新节点的正确位置
Node* parent = nullptr;
Node* cur = _root;
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;
建树后,就是调整节点的颜色,使其成为一棵红黑树
约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点
按照原则3,我们将调整节点颜色分为两种情况
第一是叔叔节点存在且为红色
解决方式:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整
性质3要求红黑树是不能存在连续的红色节点的,因此我们要对红黑树进行颜色调整,使其成为红
黑树,由于叔叔节点的存在,所以此时是不需要进行旋转进行高度调整的.
如何进行红黑树的颜色调整呢?我们可以将需要调整的树抽离出来进行思考
进一步观察我们可以发现上面的模型就是我们原则2中对应的模型,该树违反的规则,只是单纯出
现了连续的红色节点,每条路径的黑色节点数目都是不变的(包括空节点为黑色),所以,我们完全
可以按照原则2,对上面的模型进行等效(uncle,parent变黑,grandparent变红)来解决该树颜色调整
问题
但是,这棵树,有可能是整棵树的子树,只是我们抽离出来的一部分
grandparent所对应的节点的父节点,可能是黑色,那就不需要往上调整,但假如是红色,就会出
现连续节点为红色的情况,此时就需要向上继续调整,如下面的例子
我们可以发现17和25又变成了连续红色,需要继续向上调整,模型依旧是原则2所符合的模型
同样按照相同的方法即可完成红黑树的颜色调整
根节点此时为红色,不符合根节点颜色必须为黑色的性质要求,所以还需要把根节点调整回黑色
由于根节点是每条路径的公共节点,所以根据原则2,将根节点调整为黑色,与给每条路径增加一
个黑节点是等效的,依旧满足它是一棵红黑树,至此,完成整棵树的颜色调整
// 情况1:u存在且为红,变色处理,并继续往上处理
if (uncle && uncle->_col == RED)
{
//父亲和叔叔节点都调节为黑色
parent->_col = BLACK;
uncle->_col = BLACK;
//爷爷调节为红色
grandfather->_col = RED;
//往上调节
cur = grandfather;
parent = cur->_parent;
}
第二是叔叔节点不存在或叔叔节点为黑色
解决方式:变色+单旋或双旋
假如叔叔节点不存在,则cur节点一定是新插入的节点,因为假如cur不是新插入的节点,而它又是
一棵红黑树,则p,cur中一定有一个节点为黑色,这时就不满足性质4(每条路径黑色节点个数相同的
要求)
此时直接右单旋即可解决,调整下颜色即可.
假如叔叔节点存在且为黑色,那一定是第一种情况变化而来,即cur节点的颜色最开始一定是黑色
因为按照原则1,插入的新节点都为红色,反推可以得到p一定是红色;由于性质3,不能存在连续
的红色节点,所以cur必定为黑色
现在看到的cur的节点为红色,是由情况1变化而来
此时对于叔叔节点为黑色的情况,我们对连续的红色节点采取不同的策略,先旋转调整高度(原则
3),再改变颜色
为什么需要旋转?
1.连续的红节点导致最长路径不再是红黑相间,最短路径依旧是全黑
2.每条路径的黑色节点相同
3.叔叔节点为黑色
进而导致了此时最长路径超过最短路径的两倍,违背了红黑树的定义,因此需要旋转
如上图中的最左边路径与最右边路径
旋转又分为两种情况,一种是单旋,另一种是双旋,区分的原则是cur在父亲的左边或者右边
具体来讲,单旋的操作和AVLTree完全类似,不过记得旋转完后,还需要调整对应的颜色
parent的颜色变为黑色,grandparent的颜色变为红色
双旋的操作也和AVLTree完全类似,旋转完后,同样需要调整对应的颜色
cur的颜色变为黑色,grandparent的颜色变为红色
else // 情况2+3:u不存在/u存在且为黑,旋转+变色
{
// g
// p u
// c
//右单旋
if (cur == parent->_left)
{
RotateR(grandfather);
grandfather->_col = RED;
parent->_col = BLACK;
}
else
{
// g
// p u
// c
//LR双旋
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
//parent->_col = RED;
grandfather->_col = RED;
}
break;
}
上述全部讨论的是父亲位于爷爷左边的情况,位于右边也是同样进行分析,这里不再赘述,直接放
上插入的源代码
bool Insert(const pair<K, V>& kv)
{
//假如刚开始没有节点,直接使其成为根即可
//满足规则,根节点必须为黑色
if (_root == nullptr)
{
_root = new Node(kv);
_root->_col = BLACK;
return true;
}
//同样满足二叉树的搜索规则,先找到新节点的正确位置
Node* parent = nullptr;
Node* cur = _root;
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)
{
//找爷爷
Node* grandfather = parent->_parent;
//父亲为爷爷的左节点
if (grandfather->_left == parent)
{
//则叔叔是爷爷的右节点
Node* uncle = grandfather->_right;
// 情况1:u存在且为红,变色处理,并继续往上处理
if (uncle && uncle->_col == RED)
{
//父亲和叔叔节点都调节为黑色
parent->_col = BLACK;
uncle->_col = BLACK;
//爷爷调节为红色
grandfather->_col = RED;
//往上调节
cur = grandfather;
parent = cur->_parent;
}
else // 情况2+3:u不存在/u存在且为黑,旋转+变色
{
// g
// p u
// c
//右单旋
if (cur == parent->_left)
{
RotateR(grandfather);
grandfather->_col = RED;
parent->_col = BLACK;
}
else
{
// g
// p u
// c
//LR双旋
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
//parent->_col = RED;
grandfather->_col = RED;
}
break;
}
}
//父亲为爷爷的右节点
else
{
//则叔叔是爷爷的左节点
Node* uncle = grandfather->_left;
// 情况1:u存在且为红,变色处理,并继续往上处理
if (uncle && uncle->_col == RED)
{
//父亲和叔叔节点都调节为黑色
parent->_col = BLACK;
uncle->_col = BLACK;
//爷爷调节为红色
grandfather->_col = RED;
//往上调节
cur = grandfather;
parent = cur->_parent;
}
else // 情况2+3:u不存在/u存在且为黑,旋转+变色
{
// g
// u p
// c
//左单旋
if (cur == parent->_right)
{
RotateL(grandfather);
grandfather->_col = RED;
parent->_col = BLACK;
}
else
{
// g
// u p
// c
//RL双旋
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
//parent->_col = RED;
grandfather->_col = RED;
}
break;
}
}
}
_root->_col = BLACK;
return true;
}
左右单旋的代码和AVLTree的代码实现相同,这里不再重复说明
//左旋
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
//b变成30的右
parent->_right = subRL;
//父节点也需要调整,但subRL可能为空
if (subRL)
subRL->_parent = parent;
//调整时未必是整棵树的调整,所以还需要考虑parent的链接问题,因此需要先记录ppNode
Node* ppNode = parent->_parent;
subR->_left = parent;
parent->_parent = subR;
if (ppNode == nullptr)
{
_root = subR;
_root->_parent = nullptr;
}
else
{
//在调整爷爷节点指向的时候,还需要考虑原来parent是爷爷的左还是右
//subR重新链接回爷爷的左或者右
if (ppNode->_right == parent)
{
ppNode->_right = subR;
}
else
{
ppNode->_left = subR;
}
subR->_parent = ppNode;
}
}
//右旋
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
//b变成60的左
parent->_left = subLR;
//父节点也需要调整,但subRL可能为空
if (subLR)
subLR->_parent = parent;
//调整时未必是整棵树的调整,所以还需要考虑parent的链接问题,因此需要先记录ppNode
Node* ppNode = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (ppNode == nullptr)
{
_root = subL;
_root->_parent = nullptr;
}
else
{
//在调整爷爷节点指向的时候,还需要考虑原来parent是爷爷的左还是右
//subL重新链接回爷爷的左或者右
if (ppNode->_right == parent)
{
ppNode->_right = subL;
}
else
{
ppNode->_left = subL;
}
subL->_parent = ppNode;
}
}
2.3 测试
检验一棵树是否是红黑树,不能按照路径长度来进行判断,最长路径是最短路径的两倍,并不能说
明它是一棵红黑树
而是要从5条性质出发,逐一进行验证
1. 每个结点不是红色就是黑色
2. 根节点是黑色的
3. 如果一个节点是红色的,则它的两个孩子结点是黑色的
4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点 5. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
第一点,不需要检验
第二点,首先直接进行判断根节点是否为黑色
bool IsRBTree()
{
//假如根节点存在,但颜色不是黑色,则不是红黑树
if (_root && _root->_col == RED)
{
cout << "根节点颜色是红色" << endl;
return false;
}
...
}
第三点和第四点,走一个深度遍历,每遇到一个黑色节点,则blacknum进行加一
bool _Check(Node* root, int blackNum, int benchmark)
{
//假如到空节点(叶子节点),说明已经走完一条路径,可以开始判断
if (root == nullptr)
{
//假如统计出的黑色节点个数和参考黑色节点个数不同,则一定不是红黑树
if (blackNum != benchmark)
{
cout << "某条路径黑色节点的数量不相等" << endl;
return false;
}
return true;
}
//递归遇到黑色节点时,则blackNum可以加1
if (root->_col == BLACK)
blackNum++;
//假如连续存在两个红色节点,则也不是红黑树,注意还需要判断父节点是否存在
if (root->_col == RED && root->_parent && root->_parent->_col == RED)
{
cout << "存在连续的红色节点" << endl;
return false;
}
//递归判断是否是红黑树,左子树和右子树都为红黑树,则为红黑树
return _Check(root->_left, blackNum, benchmark)
&& _Check(root->_right, blackNum, benchmark);
}
结合上面的代码,即可得到完整的测试红黑树代码
bool IsRBTree()
{
//假如根节点存在,但颜色不是黑色,则不是红黑树
if (_root && _root->_col == RED)
{
cout << "根节点颜色是红色" << endl;
return false;
}
//随便选一条路径作为黑色节点参考点
int benchmark = 0;
Node* cur = _root;
while (cur)
{
if (cur->_col == BLACK)
benchmark++;
cur = cur->_left;
}
// 连续红色节点
return _Check(_root, 0, benchmark);
}
3.建树测试
void TestRBTree1()
{
int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
//int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
RBTree<int, int> t1;
for (auto e : a)
{
/* if (e == 14)
{
int x = 0;
}*/
t1.Insert(make_pair(e, e));
//cout << e << "插入:" << t1.IsBalance() << endl;
}
t1.Inorder();
cout << t1.IsRBTree() << endl;
}
void Test_RBTree2()
{
srand(time(0));
const size_t N = 5000000;
RBTree<int, int> t;
for (size_t i = 0; i < N; ++i)
{
size_t x = rand() + i;
t.Insert(make_pair(x, x));
//cout << t.IsBalance() << endl;
}
//t.Inorder();
cout << t.IsRBTree() << endl;
cout << t.Height() << endl;
}