前沿
红黑树学习路线如下:
二叉搜索树 -> AVL树 -> 红黑树
平衡二叉搜索树(AVL树):
红黑树是在AVL树的基础上提出来的。
平衡二叉搜索树又称为AVL树,是一种特殊的二叉排序树。其有以下的性质
1、满足二叉搜索树的所有性质
2、每个节点的左右子树都是平衡二叉搜索树(AVL)
3、每个节点的左右子树高度之差的绝对值不超过1。
将二叉树上结点的左子树高度减去右子树高度的值称为平衡因子BF,故平衡二叉树上的所有结点的平衡因子只可能是-1、0和1。只要二叉树上有一个结点的平衡因子的绝对值大于1,则该二叉树就是不平衡的。
1、已经存在二叉搜索树和AVL树,为什么还需要有红黑树这种数据结构
我们知道ALV树是一种严格按照定义来实现的平衡二叉查找树,所以它查找的效率非常稳定,为O(log n),由于其严格按照左右子树高度差不大于1的规则,插入和删除操作中需要大量且复杂的操作来保持ALV树的平衡(左旋和右旋),因此ALV树适用于大量查询,少量插入和删除的场景中
那么假设现在假设有这样一种场景:大量查询,大量插入和删除,现在使用ALV树就不太合适了,因为ALV树大量的插入和删除会非常耗时间,那么我们是否可以降低ALV树对平衡性的要求从而达到快速的插入和删除呢?
答案肯定是有的,红黑树这种数据结构就应运而生了(因为ALV树是高度平衡的,所以查找起来肯定比红黑树快,但是红黑树在插入和删除方面的性能就远远不是ALV树所能比的了)
2、红黑树概念
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍(最长路径也不会超出最短路径的两倍,因此红黑树的平衡性要求相对宽松,没有AVL树那样严格),从而使搜索树达到一种相对平衡的状态。
3、红黑树性质
红黑树是特殊的二叉搜索树,又名R-B树(RED-BLACK-TREE),由于红黑树是特殊的二叉搜索树,即红黑树具有了二叉搜索树的特性(左子树中节点 < 根节点 < 右子树中的节点),而且红黑树还具有以下特性:
1)每个结点不是红色就是黑色
2)根结点是黑色
3)每个叶子节点都是黑色的(此处的叶子节点为空节点(NIP节点),空节点为黑色存在的意义是避免了后续数路径数错了的误区,如下图1)
4)如果一个结点是红色,那么它的两个孩子节点必须是黑色(没有连续的红色节点)
5)从任意一个结点出发到空的叶子结点经过的黑结点个数相同(每条路径都包含相同数量的黑色节点)
在每次插入新节点时,需要维护上面的5条特性,这样可以保证红黑树中最长路径中节点的个数不会超过最短路径中节点的个数的两倍,从而使红黑树达到相对平衡
思考一:
如下图:如果不带空节点(NIL),我们可能会认为该红黑树仅有5条路径,但是这里计算路径其实应该走到空(NIL),所以正确的应该是有11条路径。 所有我们可以认为这条规则就是为了更好的帮我们区分不同路径的。
思考二:
为什么满足上面5条性质,红黑树就能保证:其最长路径中结点个数不会超过最短路径结点个数的两倍?(其实不带第3条就可以,加不加第4条都不会影响每条路径黑色结点数量是否相等)由于红黑树构建满足上面5条特性,
故极端情况分析(如下图2):
最短路径:全黑
最长路径:一黑一红
由于红黑树每条路径上,黑色节点数目相同(特性5),故极端情况下,最长路径等于最小路径两倍
即:红黑树能保证:其最长路径中结点个数不会超过最短路径结点个数的两倍。
4、红黑树中结点定义
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) //构造新增结点时,都默认是红色
{}
};
思考:在结点的定义中,为什么要将结点的默认颜色给成红色的?
我们来分析一下如果我们插入一个新结点给的是黑色,那它一定会违反上面提到的红黑树的第5条性质——每条路径上黑色结点的数量一致。如图:
由上图可知,当我们在红黑树中插入新节点为黑色时,则插入的当前路径会增加一个黑色结点,但是其它路径中黑色节点数量不变,不满足红黑树中每条路径中黑色节点数相等的性质,故插入黑色节点肯定违法了红黑树的性质。
那如果我们插入结点默认给红色呢?会违反规则吗?插入红色节点时,分析如下:
1、 如果插入一个红色结点,但是它的父亲也是红色:违返红黑树性质,因为红黑树里面不能出现连续的红色结点,那这种情况就需要调整了。
2、但如果它的父亲是黑色:没有违法规则,直接插入。
故插入节点默认必须是红色,因为红色节点不一定违返红黑树的性质。
故我们新插入的结点给成红色。
5、红黑树旋转
此章节为红黑树中节点插入时的前置知识。在红黑树中插入节点时,必须要严格遵守红黑树的性质,当插入节点,向上更新的过程中不满足红黑树性质时,需要采用旋转的策略来调整树的结构。旋转操作分为左旋和右旋,左旋是将某个节点旋转为其右孩子的左孩子,而右旋是节点旋转为其左孩子的右孩子。
5.1 左旋过程及其示例
左旋是将X的右子树绕X逆时针旋转,使得X的右子树成为X的父亲,同时修改相关结点的指向,旋转之后,要求二叉搜索树的属性依然满足。(以X为中心的左旋,就是将X进行左旋)
可以这样理解,旋转的目的是为了让树更加的平衡,既然你要左旋,说明你的右子树比较高,为了降低高度,可以:
1、左子树的根提上去作为整颗树的根,
2、整颗树的根作为左子树
(同理右旋也可以这样理解)
5.2 右旋过程及其示例
右旋是将节点X的左子树绕X顺时针旋转,使得X的左子树成为X的父亲,同时注意修改相关结点的指向 ,旋转之后要求仍然满足二叉搜索树的属性。
5.3 代码实现
//旋转:左边高往右边旋,右边高往左边旋。
void RotateL(Node* parent)
{
++rotateSize;
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)
{
ppnode->_left = subR;
}
else
{
ppnode->_right = subR;
}
subR->_parent = ppnode;
}
}
void RotateR(Node* parent)
{
++rotateSize;
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;
subL->_parent = nullptr;
}
else
{
if (ppnode->_left == parent)
{
ppnode->_left = subL;
}
else
{
ppnode->_right = subL;
}
subL->_parent = ppnode;
}
}
6、红黑树节点插入
明确: 插入的节点必须是红色节点,在" 4、红黑树中节点定义 "已经解释。
由性质四和性质五可知,待插入的红色节点N能否直接插入和其父亲有关,分析如下:
1、无父节点,将待插入节点N变黑,作为根节点
2、插入位置的父亲是黑色,不需要处理,插入结束。
3、插入位置的父亲是红色,出现了连续红色节点,需要处理
如下,以待插入的节点为N节点为例,分析红黑树节点插入时可能出现的情况。
6.1 情况一:无父节点
无父节点,则待插入的节点为根节点,由于性质2,故将节点 N 直接变黑作为根节点即可。
6.2 情况二:待插入节点的父节点为黑色
当父节点是黑色,这种情况下,性质4和性质5没有受到影响,不需要调整,直接插入。
6.3 情况三 :且待插入节点的父节点为红色
当插入节点N的父节点为红色,而插入的节点默认也是红色,此时违法了红黑树中不存在连续红色节点的性质,故肯定需要调整,而具体怎么调整,那得看叔叔(uncle),叔叔为红色节点和黑色节点时调整的方式不一样,具体调整方式请参考下面情况
6.3.1 待插入节点的父亲为红色,且叔叔存在且为红(变色)
待插入节点N的父节点为红色,叔叔也是红色,此时出现了连续的红色节点,需要进行调整,调整方式如下:
1、将待插入节点N的父亲P和叔叔U变黑
2、将待插入节点N的祖父G变黑
3、继续以祖父G为当前节点向上循环判断
具体流程如下图所示
待插入节点N的位置还有下面这三种情况,但是它们都属于待插入节点的父亲为红色,且叔叔为红色这种情况
其中
G -> grandfather (祖父节点)
P -> parent (父节点)
U-> uncle (叔叔节点)
N-> node (当前待插入得节点)
思考
为什么要按照上面方式调整呢?能不能直接将U、P变黑,G、N颜色不变呢?
1、将插入节点N的U,P变黑,将G变红,按这种方式保证了我们不会违背红黑树的性质。
2、不能,将叔叔节点U、父节点P变黑,G、N节点颜色不变(节点颜色为红),虽然保证了没有连续红色节点的出现,满足了性质四。但是这种情况相当于直接增加了黑色节点的数量(原来该子树中黑色节点数目为1,通过这种情况黑色节点数目变为了3),这种情况很危险,在红黑树节点定义中已经谈论过,在这颗子树中虽然每条路径上黑色节点数目相同,但是在整颗红黑树中每条路径上的黑色节点数目肯定不同,不满足性质5,如下图所示。
6.3.2 待插入节点的父亲为红色,且叔叔不存在/为黑色(旋转+变色)
思考:什么情况下才可能出现这种情况呢?
这种情况的出现一般是在6.3.1情况三经过变色处理后,会将g颜色变红
然后,在向上更新的过程中出现的情况。如下图所示。
待插入节点N的父节点为红色,叔叔也是红色,此时出现了连续的红色节点,需要进行调整,调整情况分类如下:
(1) 父亲结点P为祖先结点G的左子树,新插入结点为父结点的左子树( 左左:右单旋 )
解决方法 :
1、以祖父G为旋转中心进行右单旋,
2、父亲节点P和祖父节点G变色,
示例如下图所示
以祖父G为旋转中心进行右单旋,然后父亲节点P和祖父节点G变色,如下图所示
(2) 父亲节点P为祖先节点G的右子树,新插入结点为父节点的右子树 ( 右右:左单旋 )
解决方法:
1、以祖父G为旋转中心进行左单旋,
2、然后父亲节点P和祖父节点G变色,
示例如下图所示
(3) 父节点P为祖先节点的左子树,待插入节点为父节点的右子树( 左右:左右双旋 )
解决方法:
1、以父亲节点P旋转中心进行左单旋
2、以祖父节点G旋转中心进行右单旋 (左左:右旋情况)
3、N,G变色,N由红变为黑,G由黑变为红
示例如下图所示
如下图,新插入结点是126,其父结点125为红色,其叔叔结点为空的黑色结点,而且插入结点N是右结点,父结点P是左结点。
以父亲节点P旋转中心进行左单旋,旋转结果如下图所示
如上图父亲结点P为祖父结点G的左子树,新插入结点为父结点的左子树属于情况一(左左:右旋)故:
以祖父节点G旋转中心进行右单旋,N、G变色,N由红变为黑,G由黑变为红,旋转变色结果如下图所示
(4) 父节点P为祖先节点G的右子树,待插入节点为父节点的左子树( 右左:右左双旋 )
解决方法:
1、以父亲节点P旋转中心进行右单旋
2、以祖父节点G旋转中心进行左单旋 (右右:左单旋情况)
3、N,G变色,N由红变为黑,G由黑变为红
示例如下图所示
思考
旋转的目的是什么?旋转 + 变色最终产生的结果是什么?
1、可以这样理解,旋转的目的是为了让树更加的平衡,既然你要左旋,说明你的右子树比较高,为了降低高度,可以:
(1)左子树的根提上去作为整颗树的根,
(2)整颗树的根作为左子树
(同理右旋也可以这样理解)
故现在就可以理解,为什么
左左(父节点在祖先节点左边,子节点在父节点左边):右旋,在插入节点后,不管以父亲节点还是祖先节点为旋转点,都是左边子树较高,且不符合红黑树特性,故需要右旋 + 变色 。同理(右右:左旋)
左右(父节点在祖先节点的左边,子节点在父节点右变):左右双旋,在插入节点后,以父亲节点为旋转中心点时,右边子树高,以祖先为旋转中心点时,左边子树高,故需要左右双旋 + 变色 。同理(右左:右左双旋)
2、在旋转 + 变色之后,最终我们得到的树就是红黑树,由上面情况一、二、三可知,在红黑树中插入节点时,红黑树中的旋转最多只需要旋转两次就可以保证该树是红黑树。
6.4 代码实现
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv); //默认的节点颜色是红色
_root->_col = BLACK;
return true;
}
//1、找对应位置
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;
}
}
//2、创建节点,插入到对应位置
cur = new Node(kv); // 红色的
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;
//插入节点的父亲parent节点为红色,循环向上更新,讨论叔叔的3种情况
while (parent && parent->_col == RED)
{
//不用判断爷爷grandfather是否存在,因为我们每插入一个节点都需要检查是否满足红黑树的情况,
//如果插入节点cur为红->父亲parent为红,不可能是根节点 ->爷爷必然存在且为黑(这样才会满足红黑树)
Node* grandfather = parent->_parent;
if (parent == grandfather->_left) //父亲parent为grandfather的左边,叔叔uncle为grandfather右边
{
Node* uncle = grandfather->_right;
// 情况一:叔叔存在且为红,
//1、将父亲parent和叔叔uncle变黑,爷爷grandfather变红,
//2、继续向上处理,将grandfather变为cur,即将爷爷当成新增节点,继续向上处理,直到整颗树满足红黑树规则
//向上处理的过程中有三种情况
//1、grandfather的父亲不存在,此时grandfather为根,将grandfather变为黑,可以在外部处理
//2、grandfather的父亲存在且为黑,直接插入
//3、grandfather的父亲存在且为红,继续处理
//Notice:
//1、通过单旋和双旋处理后,肯定不会违返红黑树规则,直接跳出循环即可
//2、如果是通过仅仅改变颜色来处理,还需要继续向上循环处理。(因为通过这种方式处理,根还是红的,所以需要继续向上判断)
if (uncle && uncle->_col == RED)
{
// 变色
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
// 继续往上处理
cur = grandfather;
parent = cur->_parent;
}
else
{
// 情况二:叔叔不存在或者存在且为黑
// 旋转+变色
if (cur == parent->_left) //右单旋,+ 固定变色(p->黑,g->红)
{
//u,g->黑, p,c->红
// g p
// p u —> c g
// c u
RotateR(grandfather); //以g为旋转点
parent->_col = BLACK;
grandfather->_col = RED;
}
else //cur == parent->_right,先以parent为旋转点左旋变为1的情况,然后右旋 + 固定变色(cur->黑,g->红)
{ //双旋为左边高,右边高的情况,如以parent为根该树右边高,所以先左旋,变为2;然后以grandfather为根左边高,右旋,变为3
// g g c
// p u -> c u -> p g (c/u->黑, g/p->红)
// c p u
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
else //父亲parent为grandfather的右边,叔叔uncle为grandfather左边
{
Node* uncle = grandfather->_left;
// 情况一:叔叔存在且为红
if (uncle && uncle->_col == RED)
{
// 变色
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
// 继续往上处理
cur = grandfather;
parent = cur->_parent;
}
else
{
// 情况二:叔叔不存在或者存在且为黑
// 旋转+变色
// g p
// u p -> g c
// c u
if (cur == parent->_right)
{
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
// g g c
// u p -> u c -> g p (c/u->黑, g/p->红)
// c p u
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
//上面处理过程中有可能会把根变成红色,这里统一处理一下,把根置成黑
_root->_col = BLACK;
//插入节点的parent为黑色,直接插入
return true;
}
7、红黑树验证
如何判断它是否满足是一棵红黑树呢?
判断红黑树是否满足上面五条性质即可
主要是下面这三条性质
1、根节点必须时黑色的
2、红黑树中不存在连续的红色节点
3、红黑树的每条路径中的黑色节点数目相同
7.1 验证红黑树更节点必须为黑色
bool IsBalance()
{
if (_root && _root->_col == RED)
return false;
return Check(_root);
}
7.2 验证红黑树中不存在连续的红色节点
(1)如何检查该树中有没有出现连续红色结点呢?
方式一:
遍历这棵树,遇到红色节点判断它的孩子是不是红色节点,如果它的孩子也是红色,则不符合红黑树性质。
方式二:
遍历这棵树,遇到红色节点我们去check它的父亲,如果它的父亲也是红色,则不符合红黑树性质。
这里选择方式二, 因为:
如果是方式一,判断当前节点的孩子是不是红色,当前节点的孩子有5种情况
当前节点不存在左右孩子,当前节点不存在左孩子,当前节点不存在右孩子,当前节点的存在左孩子,当前节点的存在右孩子,故需要判断的情况多,check难度大,实现复杂。
如果方式二,每个孩子对应的父亲是唯一且存在,故这种方式判断难度低,实现简单。
(2)代码实现
bool Check(Node* cur)
{
if (cur == nullptr)
return true;
if (cur->_col == RED && cur->_parent->_col == RED)
{
cout << cur->_kv.first << "存在连续的红色节点" << endl;
return false;
}
return Check(cur->_left)
&& Check(cur->_right);
}
bool IsBalance()
{
if (_root && _root->_col == RED)
return false;
return Check(_root);
}
7.3 验证红黑树的每条路径中的黑色节点数目相同
(1)如何验证红黑树的每条路径中的黑色节点数目相同?
1、先求出一条路径的黑色结点数量,将它作为基准值,
2、然后再递归求出每条路径的黑色结点数量和所求基准值比较,如果存在不相等的情况,就不符合。
(2)代码实现
bool Check(Node* cur, int blackNum, int refBlackNum)
{
if (cur == nullptr)
{
//走到空就是一条路径走完了,比较一下是否相等
if (refBlackNum != blackNum)
{
cout << "黑色节点的数量不相等" << endl;
return false;
}
//cout << blackNum << endl;
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);
}