红黑树
(建议先学完avl树再来看红黑树嗷)
比avl树更抽象?但从实现角度反而更简单一些。在实践中,红黑树的使用更多。
这是一种更松散的控制方式。
红黑树的概念
红⿊树是⼀棵⼆叉搜索树,他的每个结点增加⼀个存储位来表⽰结点的颜⾊,可以是红⾊或者⿊⾊。 通过对任何⼀条从根到叶⼦的路径上各个结点的颜⾊进⾏约束,红⿊树确保++没有⼀条路径会⽐其他路径⻓出2倍++,因⽽是接近平衡的。
红黑树的规则
这就不像avl树规则那么简单了,有4点
- 每个结点不是红⾊就是⿊⾊
- 根结点是⿊⾊的
- 如果⼀个结点是红⾊的,则它的两个孩⼦结点必须是⿊⾊的,也就是说++任意⼀条路径不会有连续的红⾊结点++。
- 对于任意⼀个结点,从该结点到其所有NULL结点的简单路径上,均包含++相同数量的⿊⾊结点++
我们通过看图片来理解规则。对照可以发现,都满足上面4点规则。
(满二叉树)
(全黑的)
这些都是红黑树。
思考⼀下,红⿊树如何确保最⻓路径不超过最短路径的2倍的?
规则的第4点是很强的一点。
注意,这棵树是9条路径。
10的左为一条,15的左右两条,25左右两条,35左右两条,50左右两条。(要走到空)
这就是一个最长路径为最短两倍的树,已经属于极端情况了。
极端场景:假设每条路径有x个黑色结点,最短路径长度就为x,最长路径就为2x
理论上最短就为全黑,最长是一黑一红
但是最长和最短路径不一定存在。
比如全是最短:
比如全是最长:
当然这两种在实践很少出现,只不过也符合红黑树规则。
• 由规则4可知,从根到NULL结点的每条路径都有相同数量的⿊⾊结点,所以极端场景下,最短路径 就就是全是⿊⾊结点的路径,假设最短路径⻓度为bh(blackheight)。
• 由规则2和规则3可知,任意⼀条路径不会有连续的红⾊结点,所以极端场景下,最⻓的路径就是⼀ ⿊⼀红间隔组成,那么最⻓路径的⻓度为2*bh。
• 综合红⿊树的4点规则⽽⾔,理论上的全⿊最短路径和⼀⿊⼀红的最⻓路径并不是在每棵红⿊树都 存在的。假设任意⼀条从根到NULL结点路径的⻓度为x,那么bh<=h<=2*bh。
我们可以发现这几个规则单独看似乎没有什么意义,但是组合起来就很有意义了。
可以说第4点规定了最短,3加4规定了最长,2做了最长的限制。
注意
有些书上是这样画红黑树的:
《算法导论》等书籍上补充了⼀条每个叶⼦结点(NIL)都是⿊⾊的规则。他这⾥所指的叶⼦结点 不是传统的意义上的叶⼦结点,⽽是我们说的空结点 ,有些书籍上也把NIL叫做外部结点。NIL是++为了⽅便准确的标识出所有路径++,《算法导论》在后续讲解实现的细节中也忽略了NIL结点,所以我们知道 ⼀下这个概念即可。
NIL结点都为黑色,仍然满足规则4,给每个路径加一个黑色节点每个路径的黑色节点数量还是相同;同时,也很好满足了第3点,不会让红色结点的孩子不是黑色。
红黑树的 效率
同样插入一堆节点,avl的高度应该是更低的
红黑树的高度没有那么均衡,效率就会低很多吗?也不会
假设N是红⿊树树中结点数量,h最短路径的⻓度,那么 2 h − 1 < = N < 2 2 ∗ h − 1 2^h-1<=N<2^{2*h}-1 2h−1<=N<22∗h−1 ,由此推出 h ≈ l o g N h\approx logN h≈logN,也就是意味着红⿊树增删查改最坏也就是⾛最⻓路径 ,那么时间复杂度还是 O ( l o g N ) O(logN) O(logN)。和avl树一个数量级。
满二叉树才是 O ( l o g N ) O(logN) O(logN),avl树更接近一点。
但是CPU太快了,查找30次和60次没什么大的差别。
红⿊树的表达相对AVL树要抽象⼀些,AVL树通过⾼度差直观的控制了平衡。红⿊树通过4条规则的颜 ⾊约束,间接的实现了近似平衡,他们效率都是同⼀档次 ,但是相对⽽⾔,插⼊相同数量的结点,红 ⿊树的++旋转次数是更少的,因为他对平衡的控制没那么严格++。
补充:
红黑树的实现
我们首先定义一个枚举,枚举也可以方便调试,枚举是一种常量。
节点的定义与之前是类似的。
红黑树的插入
这部分也比较抽象。
红⿊树树插⼊⼀个值的⼤概过程
- 插⼊⼀个值++按⼆叉搜索树规则进⾏插⼊++ ,插⼊后我们只需要观察是否符合红⿊树的4条规则。
- 如果是空树插⼊,新增结点是⿊⾊结点。如果是⾮空树插⼊,新增结点必须红⾊结点,因为⾮空树 插⼊,新增⿊⾊结点就破坏了规则4,++规则4是很难维护的++。
- ⾮空树插⼊后,新增结点必须红⾊结点 ,++如果⽗亲结点是⿊⾊的,则没有违反任何规则++,插⼊结束
- (重点关注)⾮空树插⼊后,新增结点必须红⾊结点,如果⽗亲结点是红⾊的,则++违反规则3++。进⼀步分析,c是 红⾊,p为红,g必为⿊,这三个颜⾊都固定了,关键的变化看u的情况,需要根据u分为以下⼏种 情况分别处理。
说明:下图中假设我们把新增结点标识为c(cur),c的⽗亲标识为p(parent),p的⽗亲标识为 g(grandfather),p的兄弟标识为u(uncle)。
所以红黑树我们也要用三叉链是因为我们要去找亲属关系。这里面真正的变量(不确定的)是叔叔。
因为现在我们是已经违反规则了,也就是说自己c和父亲p是红色是确定的,既然父亲是红色,爷爷g也就必须是黑色的。所以只有叔叔是不确定的。
关键看叔叔
情况1:变⾊
c为红,p为红,g为⿊,++u存在且为红++,则将p和u变⿊,g变红。
无论如何p是必须变黑的,因为cur我们要插入的就是红色节点(刚才说过了插入黑色结点太难搞了)。
但此时这条路径上就多了一个黑色节点了,违背了规则4.
所以我们把叔叔u变黑,再把爷爷g变红,就解决了违背规则4的问题 。++为什么?++我们把叔叔变黑,此时p在的路径和u在的路径都比其他路径多了一个黑色节点,而p和u又都是g的孩子,当我们把g变红,p和u的路径就都少掉了这个共用的黑色节点!
(提前提一嘴叔叔不存在的情况,在后面的情况会具体说明)
如果叔叔不存在为空,我们就不能把p变黑再把g变红这样做,因为:
这样对于18-10-NIL这条路径来说,就只有一个黑色节点,而其他路径都有两个黑色结点。因为g是这条路径的一个结点,我们把g变红,对于这条路径就是单纯减去了一个黑色结点。
那么**这种情况(父亲不存在)要怎么做呢?**旋转+变色
以10为旋转点右单旋,然后再把10变为红色。
(前提、原则:父亲p是必然要变黑的)
旋转后p充当了爷爷g的黑色节点,然后再把旋转下来的g变为红,保证黑色节点数一样。
很巧妙。
回到我们的情况1,也就是叔叔存在且为红,我们发现g为根节点的这棵子树的路径前后的黑色节点数是不变的。
但是问题没有得到彻底的解决,还有几种情况。
-
我们这种情况g原本是黑色没错,但g的父亲原本是什么颜色是不确定的 。如果++原本是红色++,那么我们这种处理将g变为红色后,又出现了连续红色结点的情况,而这种情况是不被允许的。所以还得继续往上处理。再把g当做新的c,继续往上更新。
-
还有一种情况,g就是根节点,那么我们把g变成了红色是不行的,因为规则里根节点必须是黑色。最后我们要再把它变为黑色(不影响规则因为根节点是每个路径共有的)
所以跟学习AVL树时一样,为了代表所有情况,我们画抽象图:
假设现在处理到这种情况了,x是上图这棵树的根。
所以在处理完后x也就是g,变为红了(到上图的中间),然而它的父亲也是红的。所以我们的x就从g变成了新的c,6是p,10是g,15是u。
我们继续还是把p和u变黑再把g变黑,把g变红。
然而,叔叔可能是不存在或者存在且为黑。
这里我们abdef子树不能再以高度来论,而是要以黑色结点个数。
如果c(即x)是新增的(新增结点就是红色),那么abdef就都是空;或者c不是新增,它是下面子树的爷爷,是从黑色变为红色的。
所以分情况讨论:
- 如果c是新增,++那么abdef都是空++,做法就是我们最开始说的最简单的变色
- hb1的情况(hb就是我们前面说的bh,黑色结点的高度,black-height),也就是def为hb1的红黑树的情况。
(理解补充)
为什么def子树黑色结点个数为1,ab子树黑色节点个数为0?在下图中,x也就是原本的爷爷g,原本是黑色的,所以原本这条路径有两个黑色节点(因为p也就是6不能是黑色,否则x更新为红后就结束了不用再往上更新了;p必须是红色所以g必须是黑色否则不符合规则),而原本满足红黑树,根据每条路径黑色节点数相同的原则,def的路径都缺一个黑色节点,所以def子树中各要有一个黑色节点。
所以def是一个黑色节点的红黑树,而这样的红黑树有4种:
而ab必须是红色结点,也++不能为空++,因为为空那3为黑,插入后不会引发向上更新;而如果ab为黑色结点,def的黑色节点就还得增加。
要引发刚才的这一系列情况我们有几种插入位置?ab的各自左右,4个位置。
现在我们在4个其中一个位置插入后,ab变为了黑色,3变为了红色,将c更新到原本的爷爷g也就是3的位置,往上走一层。
同样的做法,将6和15变黑,10变红:
再往上走。
所以这个抽象图中,要么x是新增,要么原本是g变红的。
(我们这里讲的都是叔叔存在且为红的情况)
这里的abdef都是代表各种情况的抽象、组合。而这种组合有256种。
3.hb==2的情况
这种情况def子树各有2个黑色结点,所以ab子树各有1个黑色节点。
含有2个黑色结点的可能子树情况:256+16种
所以def的组合情况有(256+16)* (256+16)* (256+16)=20123648种
a和b为根节点为红色结点(必须这样才能满足变色)的hb==1的树,这里可以看到a和b插入组合也不少
a或者b插入只是要经历两次变色和向上处理才能得到这里的情况,这里的组合情况至少是百亿以上了。
再往下去讨论hb==3这样的细节就已经没有意义了。
无论abdef具体有多复杂,我们的处理方式都不变。都是把p和u变黑,把g变红。
一种hb==2具体情况:
经过两次变色到了有图这种情况(也就是x成为新的c)
上图的右边不正是我们下面这样的抽象图情况吗?
情况2:单旋+变色
c为红,p为红,g为⿊,u不存在或者u存在且为⿊,u不存在,则c⼀定是新增结点;u存在且为⿊,则 c⼀定不是新增,c之前是⿊⾊的,是在c的⼦树中插⼊,符合情况1,变⾊将c从⿊⾊变成红⾊,更新上来的。
**为什么u不存在,则c⼀定是新增结点?**因为如果c不是新增而是变过来的,那它原本是黑色,这条路径有2个黑色结点,而u这条路径只有1个黑色结点,这样黑色结点数量不同,不符合规则。
++所以如果u不存在,那c一定是新增。++
这种情况怎么处理?前面也已经讲过了。就是++先以g爷爷为旋转点右单旋,然后再将p父亲变为黑色,g爷爷变为红色++
**为什么u存在且为⿊,则 c⼀定不是新增?**因为如果c是新增,原本u这条路径有两个黑色结点,c这条只有一个,本来就不符合红黑树规则。
++所以u存在且为黑,那么c一定是从黑色变红的。++
d一定比abef多一个黑色结点,而具体情况太复杂我们说了不讨论
那么,它该怎么变呢?不管我们是变色还是旋转目的都是为了控制黑色结点数量不变。因为如果一条路径黑色结点数量变了,其他路径的也不行了。
(补充细节:可以看到,c变红后,ab的黑色节点数由hb-1变为hb)
不管怎么变,父亲肯定得变黑。但是6变黑后黑色节点数不对应了。
我们会发现10的左子树黑色节点树比10的右子树黑色节点数都多1。做法还是右旋+变色
++6肯定要变黑,然后将10为旋转点右旋,再将10变为红色,这样就满足规则了。++
这和上面的叔叔不存在的做法是一样的,都是让父亲顶替爷爷的位置。
我们可以将hb展现出来感受,这是一个hb为1的情况展现:
但是对于叔叔不存在或者存在且为黑的情况,还是不能完全解决问题,因为有可能是这种折线形,对于这种来说,单旋+变色无法解决问题。
所以我们来看情况3:双旋+变色
情况3:双旋+变色
c为红,p为红,g为⿊,u不存在或者u存在且为⿊,u不存在,则c⼀定是新增结点,u存在且为⿊,则 c⼀定不是新增,c之前是⿊⾊的,是在c的⼦树中插⼊,符合情况1,变⾊将c从⿊⾊变成红⾊,更新上来的。(这里和情况2是一样的)
我们现在探讨的是这种折线形,也就是说c是p的右孩子。
这种情况我们再将g右单旋,p变黑,g变红无法解决问题(可以画图看看),会和 avl树的折线型单旋一样变成镜像的另一种折线形,而且在这里我们也无法解决连续红色结点的问题。
正确做法:
双旋+变色
++先以p左单旋,再以g右单旋,再把c变黑,g变红。++
以p左单旋,我们就将其变为了非折线形(就像在avl树的双旋中先变为一边高一样);然后其实可以说此时既然已是非折线形,就接着走上面的单旋+变色方式:以g右单旋+变色(将此时的根结点变黑,旋转下来的变红)。不同的是上面的是p最后变成根,这里的是双旋后c变成了根。
参考代码:
cpp
enum Colour
{
RED,
BLACK
};
template<class K, class V>
struct RBTreeNode
{
//数据成员
pair<K, V> _kv;
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
Colour _col;
//构造函数
RBTreeNode(const pair<K, V>& kv)//const引用的
:_kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
{}
};
template<class K, class V>
class RBTree
{
using Node = RBTreeNode<K, V>;//typedef的是结点
public:
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->_right;
}
else if (cur->_kv.first > kv.first)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
//这里如果没有parent记录,我们是无法解引用空指针cur找到父亲来链接的
cur = new Node(kv);
cur->_col = RED;
if (kv.first < parent->_kv.first)//是这样做的
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;//至此的插入过程和链接都和以前的二叉搜索树、avl树差不多
//父亲为红色,需要处理。写while因为可能处理好几层
//while(parent->_col == RED)
while (parent && parent->_col == RED)
{
//关键看叔叔
Node* grandfather = parent->_parent;//因为父亲是红色,g肯定是黑
//分两大类情况:叔叔在左、在右
if (parent == grandfather->_left)
{
// g
//p u
Node* uncle = grandfather->_right;
//叔叔存在且为红
if (uncle && uncle->_col == RED)
{
//变色
uncle->_col = BLACK;
parent->_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);
//变成这样:
// g
// c u
//p
RotateR(grandfather);
// c
// p g
// u
cur->_col = BLACK;
grandfather->_col = RED;
}
break;//到这里根为黑所以不用再关注上一层了直接break
}
}
else//另一大类情况------p在g的右边,u在g的左边
{
// g
//u p
Node* uncle = grandfather->_left;
//叔叔存在且为红
if (uncle && uncle->_col == RED)
{
//c在p左右都一样,就变色
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
//往上走
cur = grandfather;//注意是变为g而不是p
parent = cur->_parent;
}
else//叔叔存在且为黑或不存在
{
//都是旋转+变色,非折线形就单旋+变色;折线形双旋+变色。旋转方向与上面的u在右是相反的
if (cur == parent->_right)
{//非折线形------单旋+变色
// g
//u p
// c
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;
}
//右单旋
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
subL->_right = parent;
if (parent == _root)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
Node* pparent = parent->_parent;
subL->_parent = pparent;
if (pparent->_left == parent)
pparent->_left = subL;
else
pparent->_right = subL;
}
parent->_parent = subL;
if (subLR)
subLR->_parent = parent;
}
//左单旋
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* pparent = parent->_parent;
parent->_right = subRL;
subR->_left = parent;
if (_root == parent)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (parent == pparent->_left)
pparent->_left = subR;
else
pparent->_right = subR;
subR->_parent = pparent;
}
parent->_parent = subR;
if (subRL)
{
subRL->_parent = parent;
}
}
private:
Node* _root = nullptr;//给缺省值
};
单旋的代码在学avl时已经写过了,这里直接复制的。
我们再将一些其他的接口补充上:
中序遍历
cpp
void InOrder()
{
_InOrder(_root);
cout << endl;
}
private:
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << endl;
_InOrder(root->_right);
}
高度计算
cpp
int Height()
{
return _Height(_root);
}
private:
int _Height(Node* root)
{
if (root == nullptr)
return 0;
int LeftHeight = _Height(root->_left);
int RightHeight = _Height(root->_right);
return LeftHeight > RightHeight ? LeftHeight + 1 : RightHeight + 1;
}
节点数计算
cpp
int Size()
{
return _Size(_root);
}
private:
int _Size(Node* root)
{
if (root == nullptr)
return 0;
return _Size(root->_left) + _Size(root->_right) + 1;
}
查找
cpp
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (key < cur->_kv.first)
cur = cur->_left;
else if (key > cur->_kv.first)
cur = cur->_right;
else
return cur;
}
return nullptr;
}
测试这棵树
cpp
#include<iostream>
using namespace std;
#include"RBTree.h"
void TestRBTree1()
{
RBTree<int, int> t;
int a[] = { 16,3,7,11,9,26,18,14,15 };
for (auto e : a)
{
t.Insert({e,e});
}
t.InOrder();
}
int main()
{
TestRBTree1();
return 0;
}
红⿊树的验证
这⾥获取最⻓路径和最短路径,检查最⻓路径不超过最短路径的2倍是不可⾏的,因为就算满⾜这个条 件,红⿊树也可能颜⾊不满⾜规则,当前暂时没出问题,后续继续插⼊还是会出问题的。所以我们还 是去检查4点规则,满⾜这4点规则,⼀定能保证最⻓路径不超过最短路径的2倍。
红黑树的规则:
- 每个结点不是红⾊就是⿊⾊
- 根结点是⿊⾊的
- 如果⼀个结点是红⾊的,则它的两个孩⼦结点必须是⿊⾊的,也就是说++任意⼀条路径不会有连续的红⾊结点++。
- 对于任意⼀个结点,从该结点到其所有NULL结点的简单路径上,均包含++相同数量的⿊⾊结点++
检查:
- 规则1枚举颜⾊类型,天然实现保证了颜⾊不是⿊⾊就是红⾊。
- 规则2直接检查根即可
- 规则3前序遍历检查,遇到红⾊结点查孩⼦不太⽅便,因为孩⼦有两个,且不⼀定存在,反过来检查⽗亲的颜⾊就⽅便多了。
- 规则4前序遍历,遍历过程中⽤形参记录跟到当前结点的blackNum(⿊⾊结点数量),前序遍历遇到 ⿊⾊结点就++blackNum,⾛到空就计算出了⼀条路径的⿊⾊结点数量。再任意⼀条路径⿊⾊结点 数量作为参考值,依次⽐较即可。
参考代码:
cpp
bool IsBalance()
{
if (_root == nullptr)
return true;
if (_root->_col == RED)
return false;
int refNum = 0;
Node* cur = _root;
while (cur)
{
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)
{
if (blackNum != refNum)
{
cout << "存在黑色节点的数量不相等的路径" << endl;
return false;
}
return true;
}
if (root->_col == BLACK)
{
blackNum++;
}
if (root->_col == RED && root->_parent->_col == RED)
{
cout << "存在连续的红色结点" << endl;
return false;
}
return Check(root->_left, blackNum, refNum) && Check(root->_right, blackNum, refNum);
}
本文结束