红黑树
[1. 思考:红黑树如何确保最长路径不超过最短路径的2倍的?](#1. 思考:红黑树如何确保最长路径不超过最短路径的2倍的?)
[2. 红黑树的效率](#2. 红黑树的效率)
[1. 整体结构](#1. 整体结构)
[2. 红黑树的插入](#2. 红黑树的插入)
[2.1 插入流程](#2.1 插入流程)
[4. 红黑树的验证](#4. 红黑树的验证)
[5. 红黑树的删除](#5. 红黑树的删除)
一、红黑树的概念
红黑树是一颗平衡二叉搜索树 ,它的每个节点增加一个存储位来存储该节点的颜色,可以是红色或黑色。通过对任意从根到叶子节点路径上节点颜色的限制,确保没有一条路径会比其它路径长2倍,达到平衡
二、红黑树的规则
- 每个节点不是红色就是黑色
- 根节点是黑色的
- 如果一个节点是红色的,那么它的两个孩子是黑色
- 对于任意节点,该节点到所有NULL节点的简单路径上,均包含相同数量的黑色节点
注意: 在《算法导论》等书中,补充了每个叶子结点(NIL)都是黑色的规则,此叶子结点指我们常说的空节点,有些书也把它们叫做外部节点
1. 思考:红黑树如何确保最长路径不超过最短路径的2倍的?
由规则4可知,一棵树不可能存在连续的红色节点,那我们可以知道:最短的路径就是全黑,长度为bh,最长的路径就是一黑一红交替出现,长度为2bh,因此,最极端的情况,最长的路径是最短的路径的两倍
2. 红黑树的效率
假设N为红黑树中节点的数量,h为最短路径长度,那么2^h-1<=N<=2^(2h)-1 ,因此h是在logN量级的 ,因此其增删查改的效率是O(logN) ,红黑树相比于AVL树要抽象一些,但是红黑树对平衡的控制没有AVL树那么严格,旋转次数也更少

三、红黑树的实现
1. 整体结构
红黑树与AVL树相同,也是由三叉链构成,它还有一个颜色的成员变量,我们可以用一个枚举来实现
cpp
enum Colour
{
RED, BLACK
};
template<typename K, typename V>
struct RBTreeNode
{
pair<K, V> _kv;
RBTreeNode<K, V>* _parent;
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
Colour _col;
RBTreeNode(const pair<K,V>& _kv)
:_kv(_kv), _parent(nullptr), _left(nullptr), _right(nullptr)
{}
};
template<typename K, typename V>
class RBTree
{
typedef RBTreeNode<K, V> Node;
public:
private:
Node* _root;
};
2. 红黑树的插入
2.1 插入流程
- 首先我们也是要按照二叉搜索树的规则来进行插入,之后我们再看是否满足4条规则
- 如果树为空树,那插入的节点必然为 黑色的根节点 ;如果树不是空树,那么插入节点 必然为红色节点 ,
因为如果新增黑色节点,那必然会导致一路径黑色节点数量加一,而其他路径黑色节点数量不变,难以维护 - 非空树插入后,新增节点必须是红色节点,如果父亲节点是黑色节点,则没有违反规则,插入结束。
- 非空树插入后,新增节点必须是红色节点,如果父亲节点是红色节点,则违反规则,要进行处理 。
进一步分析,下面将当前节点称为c(cur),c的父亲为p(parent),p的父亲为g(grandfather),p的兄弟为u(uncle)。如果c,p为红,那么g必然为黑,接下来就要看u的情况了。
情况一:变色
c红,p红,g黑,u存在且为红,则将p,u变黑,g变红,再把g当做新的c继续向上更新。直到遇到父亲为黑为止
p,u均为红,g为黑,把p,u变黑,则左右两边都增加一个黑色节点,依旧符合条件,但是从根经过g的路径多了一个黑色节点,因此g要变红,随后继续向上更新。如果g的父亲为红,还需要继续处理;如果g的父亲为黑,则处理结束

上图只是其中的一种情况,实际中是有大量的情况的
下图将以上类似的处理进行了抽象表达,d/e/f代表每条路径拥有hb个黑色结点的子树,a/b代表每条路径拥有hb-1个黑色结点的根为红的子树,hb>=0。

我们给hb具体的数值,来看看这个过程的具体情况。
场景1 :hb=0,这种情况比较简单:

场景2 :hb=1,这种情况的数量一下子就激增了很多

场景3 :hb=2,这种情况的数量激增到了一个恐怖的数字

因此,我们在实际中并不需要去考虑其具体情况,只需要一个抽象的情况即可完成分析。
情况二:单旋+变色
c红,p红,g黑,u不存在或存在且为黑;若u不存在,则c一定是新增节点;若u存在且为黑,则c一定不是新增节点,是从下面更新上来的。
- 若u不存在,假设c不是新增节点,则c原本是黑,从下面更新上来变成红,那么c所在路径黑节点数量必然会比u所在路径多,假设不成立。
- 若u存在且为黑,假设c是新增节点,那么c祖先p在c侧的子树一定是空,因为这样c才能插入进来,而p另一侧至少有u这样一个黑,则u所在路径必然比c所在路径黑色节点多一个,假设不成立。
分析:
首先,无论是那种情况,p都必须得变黑,u不存在或存在且为黑都不能通过单纯的变色解决问题,必须进行旋转+变色
1.p为g左,c为p左
g
p u
c
处理方式:先以g为旋转点右单旋,在把p变黑,g变红即可,且不需要继续向上更新,因为旋转后新根p已经为黑且符合规则,更新结束。
2.p为g右,c为p右
g
u p
c
处理方式:先以g为旋转点进行左单旋,再把p变黑,g变红即可。
以上两种情况使红黑树依旧保持规则,p为黑,成为了子树的新根,不会产生"红红",原本根为黑,修改后根依旧为黑,且黑色节点数量不变。
下面以1和u不存在为示例:

以hb=1为具体情况:

情况三:双旋+变色
c红,p红,g黑,u不存在或存在且为黑,若u不存在,c一定是新增节点;若u存在且为黑,c一定不是新增节点,此推理与情况二一致。解决想这种连续红色的问题,都必须得是p变黑才行。
1.p为g左,c为p右
g
p c
u
处理方式:先以p为旋转点进行左单旋,再以g为旋转点进行右单旋(以g左右双旋),再把c变黑,g变红即可
2.p为g右,c为p左
g
u p
c
处理方式:先以p为旋转点进行右单旋,再以g为旋转点进行左单旋(以g右左双旋),再把c变黑,g变红即可
这两种情况与上面单旋的类似,也是保持了红黑树的结构。
下面以1和u不存在为示例:

以hb=1为具体情况:

代码实现
这里旋转的逻辑与AVL树完全一样,可参考AVL树
cpp
bool insert(const pair<K,V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
_root->_col = BLACK;
return true;
}
else
{
//找插入位置
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (kv.first < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
else if (kv.first > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else return false;
}
//找到了
cur = new Node(kv);
cur->_parent = parent;
cur->_col = RED;
if (kv.first > parent->_kv.first)
{
parent->_right = cur;
}
else if (kv.first < parent->_kv.first)
{
parent->_left = cur;
}
else assert(false);
//调整
while (parent && parent->_col == RED)
{
//parent为红,parent必然不为根,grandfather为黑
Node* grandfather = parent->_parent;
Node* uncle;
if (grandfather->_left == parent) uncle = grandfather->_right;
else uncle = grandfather->_left;
//判断uncle情况
if (uncle && uncle->_col == RED)
{
parent->_col = BLACK;
uncle->_col = BLACK;
grandfather->_col = RED;
}
else
{
if (grandfather->_left == parent)
{
if (parent->_left == cur)
{
// g
// p
// c
//右单旋+变色
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
cur->_col = RED;
}
else
{
// g
// p
// c
//左右双旋+变色
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
parent->_col = RED;
}
}
else
{
if (parent->_right == cur)
{
// g
// p
// c
//左单旋+变色
RotateL(grandfather);
parent->_col = BLACK;
cur->_col = RED;
grandfather->_col = RED;
}
else
{
// g
// p
// c
//右左双旋+变色
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
parent->_col = RED;
grandfather->_col = RED;
}
}
break;
}
cur = grandfather;
parent = cur->_parent;
}
_root->_col = BLACK;
return true;
}
}
3.红黑树的查找
红黑树的查找与二叉搜索树一样,效率为O(logN)
cpp
Node* find(const K& k)
{
Node* cur = _root;
while (cur)
{
if (k > cur->_kv.first)
cur = cur->_right;
else if (k < cur->_kv.first)
cur = cur->_left;
else return cur;
}
return nullptr;
}
4. 红黑树的验证
想要验证一棵树是否为红黑树,只需要检查它是否满足红黑树四条规则即可
- 颜色非红即黑,非黑即红,
我们的实现正好保证了这一点。 - 根节点为黑,直接检查即可
。 - 红的孩子为黑,不存在相邻红节点,
这个遇到红节点检查左右孩子非常不方便,我们反过来检查父亲节点就方便多了。 - 任意路径有相同数量黑节点,
我们可以先走任意路径,记录黑节点数量为refnum,然后再进行前序遍历,用一个形参num记录当前遇到黑色节点的数量,形参num出了函数作用域就会被销毁,前序遍历,遇到黑就++,走到空就是一条路径的黑色节点数量,这样我们就能得到每一条路径黑色节点的数量。
cpp
private:
bool _IsBalanceTree(Node* root,int num,const int refnum)
{
if (root == nullptr)
{
if (num != refnum) return false;
return true;
}
if (root->_col == RED && root->_parent->_col == RED) return false;
if (root->_col == BLACK) num++;
return _IsBalanceTree(root->_left,num,refnum) && _IsBalanceTree(root->_right,num,refnum);
}
public:
bool IsBalanceTree()
{
if (_root->_col == RED) return false;
Node* cur = _root;
int refnum = 0;
//以最左路线为参考
while (cur)
{
if (cur->_col == BLACK) refnum++;
cur = cur->_left;
}
return _IsBalanceTree(_root,0,refnum);
}

5. 红黑树的删除
红黑树的删除步骤及其复杂,可参考《算法导论》或《STL源码剖析》进行学习。