红黑树
从 234 树 到 红黑树:https://blog.youkuaiyun.com/asdfsadfasdfsa/article/details/86500552
定义
2-3-4 树和红黑树是完全等价的,由于绝大多数编程语言直接实现2-3-4树会非常繁琐,所以一般是通过实现红黑树来实现替代2-3-4树,而红黑树本也同样保证在O(lgn)的时间内完成查找、插入和删除操作。
红黑树是每个节点都带有颜色属性的平衡二叉查找树 ,颜色为红色或黑色。除了二叉查找树一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:
(1) 节点是要么红色或要么是黑色。
(2) 根一定是黑色节点。
(3) 每个叶子结点都带有两个空的黑色结点(称之为NIL节点,它又被称为黑哨兵)。
(4) 每个红色节点的两个子节点都是黑色(或者说从每个叶子到根的所有路径上不能有两个连续的红色节点)。
(5) 从任一节点到它所能到达得叶子节点的所有简单路径都包含相同数目的黑色节点。
这些性质保证了根节点到任意叶子节点的路径长度,最多相差一半(因为路径上的黑色节点相等,差别只是不能相邻的红色节点个数),所以红黑树是一个基本平衡的二叉搜索树,它没有AVL树那么绝对平衡,但是同样的关键字组成的红黑树相比AVL旋转操作要少,而且删除操作也比 AVL 树效率更高,实际应用效果也比 AVL 树更出众。当然红黑树的具体实现也复杂的多。
黑哨兵节点的作用:
红黑树的这5个性质中,第3点是比较难理解的,但它却非常有必要。我们看上面这张图,如果不使用黑哨兵,它完全满足红黑树性质,根结点5到两个叶结点1和叶结点9路径上的黑色结点数都为3个,且没有连续红色节点。
但如果加入黑哨兵后,叶结点的个数变为 8 个黑哨兵,根结点5到这8个叶结点路径上的黑高度就不一样了,所以它并不是一棵红黑树。NIL节点的存在还可以使得红黑树在代码实现方面得到简化,在具体实现过程中我们只需要 1个NIL节点即可。
为了简化代码和减少不必要的开销,在具体的实现中我们定义一个伪根节点 ROOT 且只定义一个 NIL 节点。伪根节点的左子支永远指向 NIL 节点,NIL 节点的左右子支又指向它自身。伪根节点的右子支才表示真正的红黑树。( jdk 中 treemap 的实现,貌似没有用到黑哨兵节点)
代码:
enum RBTColor{RED, BLACK};
template <class T>
class RBTNode{
public:
RBTColor color; // 颜色
T key; // 关键字(键值)
RBTNode *left; // 左孩子
RBTNode *right; // 右孩子
RBTNode *parent; // 父结点
RBTNode(T value = 0, RBTColor c = BLACK, RBTNode *l = nullptr, RBTNode *r = nullptr, RBTNode *p = nullptr)
:key(value),color(c),left(l),right(r),parent(p) {}
};
template <class T>
class RBTree {
private:
RBTNode<T> *mRoot; // 根结点
RBTNode<T> *NIL;
int size;
public:
RBTree();
~RBTree();
RBTColor getRootColor(){
return mRoot->color;
};
int Compare(T x, T y){
if (x > y)
return 1;
else if (x < y)
return -1;
else
return 0;
}
// 前序遍历"红黑树"
void preOrder();
// 中序遍历"红黑树"
void inOrder();
// 后序遍历"红黑树"
void postOrder();
// (递归实现)查找"红黑树"中键值为key的节点
RBTNode<T>* search(T key);
// 查找最小结点:返回最小结点的键值。
T minimum();
// 查找最大结点:返回最大结点的键值。
T maximum();
// 找结点(x)的后继结点。即,查找"红黑树中数据值大于该结点"的"最小结点"。
RBTNode<T>* successor(RBTNode<T> *x);
// 找结点(x)的前驱结点。即,查找"红黑树中数据值小于该结点"的"最大结点"。
RBTNode<T>* predecessor(RBTNode<T> *x);
// 将结点(key为节点键值)插入到红黑树中
bool insert(T key);
// 删除结点(key为节点键值)
bool remove(T key);
// 销毁红黑树
void destroy();
// 打印红黑树
void print();
// 查找最小结点:返回tree为根结点的红黑树的最小结点。
RBTNode<T>* minimum(RBTNode<T>* tree);
// 查找最大结点:返回tree为根结点的红黑树的最大结点。
RBTNode<T>* maximum(RBTNode<T>* tree);
private:
// 前序遍历"红黑树"
void preOrder(RBTNode<T>* tree) const;
// 中序遍历"红黑树"
void inOrder(RBTNode<T>* tree) const;
// 后序遍历"红黑树"
void postOrder(RBTNode<T>* tree) const;
// 左旋
void leftRotate(RBTNode<T>* x);
// 右旋
void rightRotate(RBTNode<T>* y);
// 删除函数
void remove(RBTNode<T>* &root, RBTNode<T> *node);
// 销毁红黑树
void destroy(RBTNode<T>* &tree);
// 打印红黑树
void print(RBTNode<T>* tree, T key, int direction);
#define rb_parent(r) ((r)->parent)
#define rb_color(r) ((r)->color)
#define rb_is_red(r) ((r)->color==RED)
#define rb_is_black(r) ((r)->color==BLACK)
#define rb_set_black(r) do { (r)->color = BLACK; } while (0)
#define rb_set_red(r) do { (r)->color = RED; } while (0)
#define rb_set_parent(r,p) do { (r)->parent = (p); } while (0)
#define rb_set_color(r,c) do { (r)->color = (c); } while (0)
};
/*
* 构造函数
*/
template <class T>
RBTree<T>::RBTree()
{
size = 0;
mRoot = new RBTNode<T>;
NIL = new RBTNode<T>(0, BLACK, nullptr, nullptr, nullptr);
NIL->left = NIL;
NIL->right = NIL;
mRoot->left = NIL;
mRoot->right = NIL;
mRoot->parent = mRoot;
}
234 树 和 红黑树的等价关系
如果一棵树满足红黑树,把红结点收缩到其父结点(红结点是和父结点一体的,对应于 234 树的结点),就变成了2-3-4树,所有红色节点都与其父节点构成 3 或 4 节点,其它节点为 2 节点。图中 NIL 节点未画出。
所以红黑树的每一类型操作都与 2-3-4 树一一对应。黑色节点的个数(或者说位置)对应 2-3-4 树中的节点个数(或者说位置),这样可以很好的理解性质 3(从每个叶子到根的所有路径上不能有两个连续的红色节点)和性质5(从任一节点到它所能到达得叶子节点的所有简单路径都包含相同数目的黑色节点)以及根节点到任意叶子节点的路径长度,最多相差一半。
同时我们还需要明白的是,一颗红黑树对应唯一形态的 2-3-4 树,但是一颗 2-3-4 树可以对应多种形态的红黑树(主要是 3 节点可以对应两种不同的红黑树形态),上图中的 2-3-4 树还可以对应下图中的红黑树。我们在后面红黑树的删除操作中会利用这种情况。
红黑树中旋转的定义
为每种书中对旋转的定义不一致,所以我们有必要在这里特此说明一下。以某一个节点为轴,它的左子枝顺时针旋转,作为新子树的根,我们称之为顺时针旋转(clockwise)或者右旋转。同理,以某一个节点为轴,它的右子枝逆针旋转,作为新子树的根,我们称之为逆时针旋转(anticlockwise)或者左旋转。
/*
* 对红黑树的节点(x)进行左旋转
*
* 左旋示意图(对节点x进行左旋):
* px px
* / /
* x y
* / \ --(左旋)--> / \
* lx y x ry
* / \ / \
* ly ry lx ly
*
*
*/
template <class T>
void RBTree<T>::leftRotate(RBTNode<T>* X)
{
RBTNode<T> *P = X->parent; // 得到 x 的父节点 P
RBTNode<T> *XR = X->right; // 得到 x 的右子树(将替换 x 的位置) XR
if(P->left == X){ // 判断 x 在 px 的左还是右
P->left = XR; // x 的子树 XR 替换 X
}else{
P->right = XR;
}
XR->parent = P; // 换 父
X->right = XR->left; // 将 XR 的左子数给 X
if(XR->left != NIL){
XR->left->parent = X; // 换 父
}
XR->left = X; // 将 X 变成 XR 的左子树,完成旋转
X->parent = XR;
}
/*
* 对红黑树的节点(y)进行右旋转
*
* 右旋示意图(对节点y进行左旋):
* py py
* / /
* y x
* / \ --(右旋)--> / \ #
* x ry lx y
* / \ / \ #
* lx rx rx ry
*
*/
template <class T>
void RBTree<T>::rightRotate(RBTNode<T>* X)
{
//顺时针旋转(右旋),参数表示轴节点
RBTNode<T>* P = X->parent;
RBTNode<T>* XL = X->left;
if(P->left == X){
P->left = XL;
}else{
P->right = XL;
}
XL->parent = P;
X->left = XL->right;
if(XL->right != NIL){
XL->right->parent = X;
}
XL->right = X;
X->parent = XL;
}
红黑树的插入操作
(1)如果红黑树中已存在待插入的值,那么插入操作失败,否则一定是在叶子节点进行插入操作,执行步骤 2 。
(2)当我们插入一个新节点后,我们会把该节点涂红(涂红操作,从2-3-4树的的角度看来,向 key 结点插入,如 2 -> 3),由于插入操作可能破坏了红黑树的平衡性(已经是 4 树了,再插入需要分解),所以我们需要不断回溯,进行调整。调整过程就是颜色变换和旋转操作,而这些操作都可以从2-3-4树来理解。考虑到回溯的情况,从2-3-4树的角度,我们可以把 X 节点看成向上层进位的 key。
// insert
// 首先找到插入的点,若值存在则返回 false,否则找到它
RBTNode<T> *P = mRoot; // 父节点,初始为 伪父
RBTNode<T> *X = mRoot->right; // 得到实际的根节点
int r = 0;
while(X != NIL){ // 弹出则找到了位置
r = Compare(key, X->key);
P = X; // 得到最后一个有效节点作为父
if(r > 0){
X = X->right;
}else
if(r < 0){
X = X->left;
}else{
return false;//元素已存在,插入失败
}
}
RBTNode<T> *G; // 祖父节点
RBTNode<T> *U; // 叔节点
X = new RBTNode<T>(key, RED, NIL, NIL, P); //创建新节点,涂红
...
插入新节点时,我们可能会遇到以下几种情况:
- 黑父
- 插入后直接涂红,如果父亲节点是个黑色,插入结束。换成 234 树表示该点最多为 3 树,至少可以插入一个;
- 绿色箭头表示插入的位置,上图中的虚线表示可以有该节点,也可以没有该节点,如果有,一定是红色。当然还有可能在对称的情况,即在右子支插入,操作方式都是一样的,由于不涉及到旋转操作,所以代码的实现方式也一样,不在赘述;
- 这个操作可以从2-3-4树来理解,相当于2-3-4树中待插入的叶子节点是个 2 节点(对应黑父没有孩子节点)或者3节点(黑父有孩子节点,孩子节点的颜色一定是红色)。在回溯调整的过程中也会遇到这个情况,回溯时X 表示的是下一层向上进位的 key,到这个时候就不需要继续回溯了(可容纳,没达到 4 树无需分解)
代码:
if(P->color == BLACK) // 最简单的插入,无需判断其他,推出即可
break;
...
- 红父黑叔
这种情况还有对应的镜像情况,即 P 为 G 的右子支情况:
- 这种情况不会在叶子节点出现,但是会出现在回溯调整的过程中。这种情况相当于 2-3-4 树中,容纳进位的父节点为 3 节点,还有空间可以容纳 key,所以到此就不用继续回溯了。
代码:
P = X->parent;
if(P->color == RED){ //红父
G = P->parent; // 得到祖父
if(P == G->left){ // 得到叔
U = G->right;
}else{
U = G->left;
}
...
if(U->color == BLACK){ // 黑叔
if(G->left == P){
if(P->left == X){
rightRotate(G); // 旋转
P->color = BLACK; // 涂色
G->color = RED;
}else{
leftRotate(P);
rightRotate(G);
X->color = BLACK;
G->color = RED;
}
}else{
if(P->right == X){
leftRotate(G);
P->color = BLACK;
G->color = RED;
}else{
rightRotate(P);
leftRotate(G);
X->color = BLACK;
G->color = RED;
}
}
break;
}
- 红父红叔
-
这种情况相当于 2-3-4 树中,向上进位的父节点为 4 节点,所以先分裂(对应P和B的颜色变换)然后再插入 X,然后继续回溯,把 G 看成向更上一层进位的节点(即把 G 看成新的 X);
-
这种情况还有对应的镜像情况,即 P 为 G 的右子支情况,但在具体的代码实现过程中,因为不涉及到旋转操作,所以不用区分
代码:
while(true){
P = X->parent;
//红父
if(P->color == RED){
G = P->parent;
if(P == G->left){
U = G->right;
}else{
U = G->left;
}
//红叔
if(U->color == RED){
P->color = BLACK;
U->color = BLACK;
G->color = RED;
X = G; // 当前结点变为祖父节点,继续向上回溯
}
...
}
size++; // 结点加一
mRoot->right->color = BLACK; // 有可能向上层进位,根节点涂黑
return true;
}
红黑树的删除操作
删除操作可以概括为以下几个步骤:
(1)查找要删除的值所在的节点,如果不存在,删除失败,否则执行步骤2
(2)如果要删除的节点不是叶子节点,用要删除节点的后继节点替换(只进行数据替换即可,颜色不变,此时也不需要调整结构),然后删除后继节点。
代码:
bool RBTree<T>::remove(T key)
{
RBTNode<T> *X = mRoot->right;
X->color = RED; // 删除时,根先涂红,1.防止继续向上回溯 2.只有根节点时也方便删除
RBTNode<T> *P;
RBTNode<T> *B; // 兄弟节点
while(X != NIL){
int r = Compare(key, X->key);
if(r > 0){
X = X->right;
}else
if(r < 0){
X = X->left;
}else{
break;
}
}
if(X == NIL){ //没有找到需要删除的节点
mRoot->right->color = BLACK; // 根节点变回黑色
return false;
}
size--; // 若为叶子节点,跳过;否则,将最小值与当前结点替换,删除最小值节点
if(X->left != NIL && X->right != NIL){
RBTNode<T> *tmp = minimum(X->right);
X->key = tmp->key;
X = tmp;
}
P = X->parent; // 得到当前结点的 父
...
}
那么真正需要删除的节点有以下几种可能性:
- 要删除的节点为红色
- 该操作对应 234 数的非 2 节点,可以直接删除。
- 要删除的节点为黑色,且有一个孩子结点,这个孩子必为红色
- 在 234 树中,只有一个孩子,说明 key 没有填满,因此只能是红色,即使分裂后也不会出现单子树这样不平衡的情况。
代码:
// 删除当前结点的操作,此时表示初始状态或者处理后的状态达到可以直接删除的要求
P = X->parent;
RBTNode<int> *deleteNode = X;
// 来到这里有 后继节点 和 初始的单子树
if(X->right != NIL){ // X 是右子树,需要嫁接
if(X == P->left){
P->left = X->right; // 嫁接
}else{
P->right = X->right;
}
X->right->parent = P; // 换 父
// X->color = BLACK; // X 变为黑
mRoot->right->color = BLACK;
delete deleteNode; // 删除
return true;
}
else if(X->left != NIL){ // X 有左子树,需要嫁接 :是左子树 或者 后继结点
if(X == P->left){
P->left = X->left; // 嫁接
}else{
P->right = X->left;
}
X->left->parent = P;
// X->color = BLACK;
mRoot->right->color = BLACK;
delete deleteNode;
return true;
}else{ // X 为叶子结点
if(X == P->left){ // 将 P 的待删除结点 X 设为 NIL
P->left = NIL;
}else{
P->right = NIL;
}
if(X->color == RED){ // 若 X 为 RED,则可直接删除
mRoot->right->color = BLACK;
delete deleteNode;
return true;
}else{ // 若 X 为 BLACK,则需要继续处理
X = NIL;
}
}
...
- 要删除的结点为黑色,孩子节点都为 NIL
这时,我们删除这个黑色节点后需要进行调整,在图中 X 总表示下一层的节点,一开始 X 表示 NIL 节点(回溯过程中 X 会不断向上层迭代)。需要调整的情况又可以分为以下几种。
a. 黑兄红侄(兄弟结点是多树,可上移,父节点可下移至 X 处)
- 上述两种情况大致对应 2-3-4 树删除操作中兄弟节点为 3 节点或 4 节点,父节点 key 下移,兄弟节点 key 上移动,但不完全一致;
- 由于 X 为黑结点,则与父节点是无关的。若删除 X,必然导致该树的叶子节点不等高。因此,需要旋转;
- 上述的旋转的作用为:B 结点是多树,则将 P 结点修正到 X 结点处,将 B 结点的后继上移至父节点处
代码:
if(BL->color == RED || BR->color == RED){ // 存在红侄
if(X == P->right){ // X 在 P 的右边
if(BL->color == RED){ // 上述第一种情况
rightRotate(P);
BL->color = BLACK;
B->color = P->color;
P->color = BLACK;
}else{ // 上述第二种情况
leftRotate(B);
rightRotate(P);
BR->color = P->color;
P->color = BLACK;
}
}
...
break;
}
这种情况还有对应的镜像情况,即 P 为 G 的右子支情况:
代码:
if(B->color == BLACK){ //黑兄
RBTNode<T> *BL = B->left; //左侄子
RBTNode<T> *BR = B->right; //右侄子
if(BL->color == RED || BR->color == RED){ // 存在红侄
if(X == P->left){ // X 在 P 的左边
if(BR->color == RED){ // 上述第一种情况
leftRotate(P);
BR->color = BLACK;
B->color = P->color;
P->color = BLACK;
}else{ // 上述第二种情况
rightRotate(B);
leftRotate(P);
BL->color = P->color;
P->color = BLACK;
}
}
}
...
break;
}
b. 黑兄黑侄红父(兄弟、父亲和自己在同一结点(234 树))
- 上述两种情况都对应 2-3-4 树删除操作中兄弟节点为 2 节点,父节点至少是个 3 节点,父节点 key 下移与兄弟节点合并。
代码:
if(B->color == BLACK){ //黑兄
...
if(BL->color == BLACK && BR->color == BLACK){ // 只有黑侄
if(P->color == RED){ //黑侄红父
P->color = BLACK;
B->color = RED;
break; //不需要继续向上回溯
}
}
...
}
c. 黑兄黑侄黑父(属于分裂过的情形,全为二节点)
- 此时,在 234 树中,为全为 2 节点的情况。此时旋转是无用的,删除会导致叶子高度失衡,需要不断向上合并;
- 对应 2-3-4 树删除操作中兄弟节点为 2 节点,父亲节点也为 2 节点,父节点 key 下移与兄弟节点合并,已父节点看成新的 X,继续回溯;
代码:
...
if(B->color == BLACK){ //黑兄
...
if(BL->color == BLACK && BR->color == BLACK){ // 只有黑侄
if(P->color == BLACK){ //黑侄黑父
B->color = RED;
X = P;
P = X->parent;
}
}
...
}
d. 红兄(黑侄黑父)
按照 2-3-4 树删除操作的原理,我们这里应该检测黑侄 R (第二幅图中是 L)的两个孩子节点是否存在红色节点(对应 2-3-4 树,是否是2节点),但这样做使用的局部变量也会增多,代码实现起来也会变得非常复杂。我们这里做了一个技巧性处理,以P为轴进行旋转,它原理就是前面 2-3-4 树和红黑树的等价关系中讲到的:一颗 2-3-4 对应的红黑树形态并不唯一。
上面的两种红兄情况,旋转后对应的还是同一颗 2-3-4 树(只是 B 和 P 组成的 3 节点在红黑树的两种不同形态而已),但此时 X 的兄弟节点和侄子节点发生变化,现在 X 的兄弟节点就变成了 R(第二幅图中是 L ),我们正需检查要 R(第二幅图中是 L)的两个孩子节点的颜色。实际上,此时我们又回到上面讨论过的了黑兄的情况。
代码:
...
if(B->color == RED) { //红兄,变换一下红黑树的形状,继续判断
if(B == P->right){
leftRotate(P);
}else{
rightRotate(P);
}
B->color = BLACK;
P->color = RED;
//X节点的P节点没有发生变化,但兄弟节点发生变化
}
234 树中,为全为 2 节点的情况。此时旋转是无用的,删除会导致叶子高度失衡,需要不断向上合并;
- 对应 2-3-4 树删除操作中兄弟节点为 2 节点,父亲节点也为 2 节点,父节点 key 下移与兄弟节点合并,已父节点看成新的 X,继续回溯;
代码:
...
if(B->color == BLACK){ //黑兄
...
if(BL->color == BLACK && BR->color == BLACK){ // 只有黑侄
if(P->color == BLACK){ //黑侄黑父
B->color = RED;
X = P;
P = X->parent;
}
}
...
}
d. 红兄(黑侄黑父)
按照 2-3-4 树删除操作的原理,我们这里应该检测黑侄 R (第二幅图中是 L)的两个孩子节点是否存在红色节点(对应 2-3-4 树,是否是2节点),但这样做使用的局部变量也会增多,代码实现起来也会变得非常复杂。我们这里做了一个技巧性处理,以P为轴进行旋转,它原理就是前面 2-3-4 树和红黑树的等价关系中讲到的:一颗 2-3-4 对应的红黑树形态并不唯一。
上面的两种红兄情况,旋转后对应的还是同一颗 2-3-4 树(只是 B 和 P 组成的 3 节点在红黑树的两种不同形态而已),但此时 X 的兄弟节点和侄子节点发生变化,现在 X 的兄弟节点就变成了 R(第二幅图中是 L ),我们正需检查要 R(第二幅图中是 L)的两个孩子节点的颜色。实际上,此时我们又回到上面讨论过的了黑兄的情况。
代码:
...
if(B->color == RED) { //红兄,变换一下红黑树的形状,继续判断
if(B == P->right){
leftRotate(P);
}else{
rightRotate(P);
}
B->color = BLACK;
P->color = RED;
//X节点的P节点没有发生变化,但兄弟节点发生变化
}