RB-Tree, 红黑树(Red Black Tree)
1. 简介
1.1 定义
首先红黑树是一棵二叉搜索树,节点除了二叉树基本元素之外,还包括颜色信息,即节点包含key、left、right、p、color以及数据索引6个域。除此之外还需满足规则123
1.2 定理
-
定理1
红黑树中任意两条路径 P, Q ,P 和 Q 的路径长度存在(长度为内部节点个数)
l e n g t h ( P ) < = 2 l e n g t h ( Q ) length( P ) <= 2length( Q ) length(P)<=2length(Q)
假设路径上的黑色节点为r个,则最短路径为 r 个黑色节点,最长路径为每个红色节点后面跟上一个黑色节点,最短路径长 :r - 1,最长为 ( r - 1) + (r - 2 )+ 1 = 2(r-1)。
所以上式成立,最长路径都小于最小路径的两倍了 。 -
引理1
根据 2 中的定理知道,红黑树高度h ,书节点个数 n
1) h <= 2r;(h 是最长路径)
2 ) n >= 2r - 1(只有两条路径,且都为最短)
3) h <= 2log2(n + 1)( r <= log2(n + 1)
2. 红黑树的操作
红黑树的插入和删除调整本质是
- 插入
将插入引起的多余红色向叔分支转移,如果叔分支红色已满,则向爷爷的叔分支转移。 - 删除
将删除导致的黑色减少从兄弟分支借一个红色节点(是一种颜色上的借)到本侧染黑,如果无法借则向叔公借(就是往上一层的兄弟分支借)。
2.1. 旋转
旋转是红黑树调整中最基本的操纵,包括左旋和右旋,其实旋转本的质是双向链表的操作(断链,连接链)。
- 左旋:
以节点x和其右孩y之间的链为支轴,向左旋转,y替代x,y的左孩子替代y,x替代y的左孩子。使得y成为x的双亲,y的左孩子成为x的右孩子。 - 右旋:
右旋的过程则相反,以x和左孩子y之间的链为轴,是y成为x的双亲,y的右孩子成为x的左孩子。
这里旋转时节点相对于x和y来说逻辑左右关系没有变化,操作过程其实本质在执行链表的断链和结链,一共断开三条链,修复三条链。
- 操作流程:
- 先将x从树中拿掉(直接砍掉放在一边),直接利用y代替x
- 此时x缺乏了孩子,用y的孩子替代原来y在x中的位置,即补充了x缺的孩子。
- 此时y没了孩子,x替代y的孩子。
- TODO 补充图片
以上流程一个萝卜一个坑,必须都填好,y替代x时,x的孩子就空缺了,所以y的孩子替代y,y的孩子没了,那么x再替代y的孩子,就圆满了,类似一个swap的操作过程。
void LeftRotate(RbTree *T, RbNode *x) {
RbNode *y = x->right;
// y 替代 x
if (y == NULL) return;
// 处理p和y
if (x->p == NULL)
T.root = y;
else if(x->p->left == x) {
x->p->left = y;
} else {
x->p->right = y;
}
y->p = x->p;
// 到这里y和p的关系处理完毕,但是x仍然指向该两个节点,需要调整
// y的孩子替代y
x->right = y->left;
if (x->right != NULL) {
x->right->p = x;
}
// x替代y的孩子
x->p = y;
y->left = x;
}
void RightRotate(RbTree *T, RbNode *x) {
RbNode *y = x->left;
// replace x with y
if (x->p == NULL) {
T.root = y;
}
else if (x->p->left == x) {
x->p->left = y;
} else {
x->p->right = y;
}
y->p = x->p;
// replace y with y's child in x
x->left = y->right;
if (x->left != NULL) {
x->left->p = x;
}
// replace y's child with x
y->right = x;
x->p = y;
}
2.2. 插入
红黑树的插入过程同二叉搜索树,但是插入后需要进行调整,插入新节点要上色,一般如果上黑色肯定违反定义中的规则RB rule 33,如果给上红色,则有可能违法规则RB rule 22,也有可能不违反。综合来看插入一个节点优先上红色,如果出现连续的红色则进行改色、旋转的动作进行调整,即红黑树的插入除了常规的二叉树插入操作外,还要进行调整操作。
2.2.1. 插入调整
插入节点z都是红色,如果引起违反RB rule 22,那么其父节p点必须是红色,gp必须为黑色(grandparent,如果gp不是黑色,那么上一轮插入p和gp就不是红黑树了)。按照p和z所处分支情况可分为如下几种情况:
- LL
p在gp左分支,z在p左分支 - LR
p在gp左边,z在p右边 - RL
p在gp右边,z在p右边 - RR
p在gp右边,z在p右边
在以上基础上再对p的兄弟节点,即z的叔节点进行颜色划分,可以为红色或则黑色,那么一共是8种情况:
LLr、LRr、RLr、RRr、LLb、LRb、RLb、RRb。调整方法根据以上的情况调整。只需要了解调整LLr、LRr、LLb、LRb,其他四种情况和这四种镜像对称(即左右对调即可)。
- TODO 以后补充图片
2.2.2. 调整方法
调整方法本质只有两种:着色、旋转,是根据插入节点叔节点的颜色决定的,如果叔节点是红色则着色,如果是黑色则旋转。
1. 着色
此种情况叔节点均为红,那么无法向叔分支转移多余的红色,只可以向祖宗求救(转移)。
- LLr
- LRr
2. 旋转
此时叔节点均为黑
- LLb
- LRb
- 其他
其他四种情况和以上情况镜像对称,只需采取镜像操作即可。
2.2.3. 实现
# define RED 0
# define BLACK 1
void RbTreeInsert(RbTree *T, TreeNode *node) {
if (node == NULL) return;
ket_t key = node->key;
TreeNode *p = NULL;
TreeNode *x = T.root;
while (x != NULL) {
y = x;
if (x.key > key) {
x = x.left;
} else {
x = x.right;
}
}
if (y == NULL) {
T.root = node;
node->color = BLACK; // 根节点为黑色
return;
} else if (y->key >= key) {
y.left = node;
} else {
y.right = node;
}
node->color = RED; // 初始颜色为红
RbTreeInsertFixup(T, node);
}
void RbTreeInsertFixup(RbTree *T, TreeNode *node) {
while (node != NULL && node->p != NULL && node->p->color == RED) {
TreeNode *p = node->p;
TreeNode *gp = node->p->p; // 因为p是红色的,不可能是根节点,所以多一定存在gp。
if (gp->left == p) { // Lxx
if (gp->right != NULL && gp->right->color == RED) { // LLr and LRr
gp->color = RED;
p->color = BLACK;
gp->right->color = BLACK;
node = gp;
} else {
if (p->right == node) { // LRb
LeftRotate(T, p); // turn LRb to LLb
node = p; // parent becomes the node
p = node->p; // node becomes the parent
}
// LLb
gp->color = RED;
p->color = BLACK;
RightRotate(T, gp);
}
} else { // Rxx
if (gp->left != NULL && gp->left->color == RED) { // RLr and RRr
gp->left->color = BLACK;
p->color = BLACK;
gp->color = RED;
node = gp;
} else {
if (p->left == node) { // RLb
RightRotate(T, p); // turn RLb to RRb
node = p; // exchange the node and p
p = node->p;
}
// RRb
gp->color = RED;
node->color = BLACK;
LeftRotate(T, gp);
}
}
}
T.root.color = BLACK;
}
2.3. 删除
红黑树删除节点的过程是基于二叉搜索树的删除过程,根据删除节点的情况具体分析,其实删除的过程从树的结构上看(不考虑卫星数据和key)本质是消失了一个叶节点或者一个只有一个孩子的节点。
- 删除叶节点或者单孩子节点(被删除消失)
从上图看如果删除节点本身就是叶子节点或者只有一个孩子的节点,那么必然符合上述论断。 - 删除内部节点(被移动导致消失)
如果删除内部节点,从上图看其实最后还是将一个前驱或者后继移到该内部节点,从结构上相当于删除了前驱或者后继节点,一定是叶子或者单孩子节点。
所以二叉搜索树的删除本质都是删除了一个叶子节点或只有一个孩子的节点,因为最后树中消失的都是这类节点,如图中的虚线圈出的节点。
2.3.1. 调整分析
根据以上分析知道删除最后本质是消失了一个叶子节点或者单孩子节点(也就是导致了这类节点在树结构上的消失,不是数据的消失),那么树的不平衡只能是这类节点消失引起的。
假设消失的节点是y,替补y的是y的孩子,假设为x。
-
I. y为红色
消失的节点y是红色,无论是被删除还是被移动,消失的红色的节点只可能是叶子节点(如果有孩子,孩子黑色,黑高不一致违反条件,如果孩子红色那么违反连续无连续红色节点条件)。那么红色叶节点消失对红黑树结构无影响,无须调整。 -
II. y为黑色
消失的节点y是黑色,消失后所在分支黑高减一,此时会有节点x填补空缺,如果需要保证整棵树仍然满足红黑树,那么就需要往x所在分支上再加一个黑高(保证黑高不变)。如果x是红色直接加,如果x是黑色,可以分为两种方法,一种是将问题向上面抛,一种是在x侧加上一个黑色节点,即向叔分支借多的红色(一种颜色转移,不是真的借节点)。
总结
只有在树结构中消失的节点(因删除或者移动消失)为黑色时才需要进行调整。如果消失的节点是红色无须调整,见上图分析。
2.3.2. 调整分类
前面分析知道只有消失的节点是黑色时,黑高减一需要调整,调整可根据填充节点x的颜色分为两大类,红色或者黑色。
2.3.2.1. x是红色节点
直接将x变成黑色节点即可增加黑高,填补y消失引起的黑高减一,树平衡。
2.3.2.2. x是黑色
x是黑色节点,那么无法把黑色继续加在该节点上来增加黑高,但可以尝试两种方案,方法一:给父节点p加黑,方法二:在x侧加上一个黑色节点。根据兄弟节点颜色进行分类讨论两种方案可行性,一般给父节点加黑操作复杂一些(涉及递归),所以讨论时优先考虑x侧加黑节点。有如下结论
结论:
兄弟为红时两种方法均不可实施,只可以转为兄弟为黑来进行操作,兄弟为黑时,如果两个黑孩子,则只能给父节点加黑,如果有不变孩子为红,则可以进行一次旋转来对x侧加黑节点,如果不变孩子为黑,则进行两次旋转即可在x侧加黑节点。
假设x的兄弟b,父亲为p。
不变孩子,即旋转后仍为其孩子的孩子,例如左旋则是右孩子,因为旋转后右孩子仍然为其右孩子,右旋则是左孩子,相应的变孩子就是旋转时会丢掉的孩子。
-
兄弟为红(Lr、Rr)
- 方法一:x侧加黑节点
因为在x侧加黑节点只能通过旋转来完成,如果兄弟b为红,那么旋转后兄弟b为祖父,必须继续保持黑,此时兄弟的一个孩子会挂在x的父亲节点p上,这导致b的孩子节点的黑高加一,因为原来该子的父亲是红的,旋转之后虽然祖父不变,但是父亲颜色变成黑了,不可行。 - 方法二:父节点加黑
父节点加黑需要兄弟b移除一个黑高,由于b是红色,移除黑高需要从子树开始,较难实施。因为从黑高的定义来看,都是从根向下的,如果要减则必须b的左右子树同时减黑高,如果左右子树刚好没有多余黑色节点(比如黑色节点在红色节点中间,无法删除,否则出现连续黑),则无法操作。
总结:两种方案均无法实施,只可以转为黑色,而一次旋转既可转换。
- 方法一:x侧加黑节点
-
兄弟为黑,不变孩子为红(LbI、RbI)
- 方法一:
不变孩子为红,变孩子无约束。x侧加黑节点,假设x在左侧,那么b在右侧,在p上进行左旋,然后使得b为p的颜色,p变为黑,此时x侧成功消除影响,b右孩子此时就是不变孩子,此时右孩子是红色,为了消除影响,直接让右孩子变成红色,即可消除旋转导致b右侧黑高减一的影响。 - 方法二:
无法实施,不可以让兄弟变红,因为有孩子为红,且无法对子树进行减一操作,因为可能子树无法减去黑色节点。
总结:不变孩子为红,进行单次旋转即可。
- 方法一:
-
兄弟为黑,不变孩子为黑,变孩子为红(LbII、RbII)
- 方法一:
此时和上面分析类似,不变孩子为黑,那么另一个孩子必须是红,不然就成孩子全黑了。可以通过在b上进行旋转。假设x在左侧,则在b上进行右旋,那么b的左孩子是变孩子,旋转前让b变成红色,b的左孩子变成黑色,旋转后b的左孩子是x的兄弟 b = b l b = bl b=bl。此时x的新兄弟b的不变孩子也就是原来的b是红色的,变成了LbI型的情况,根据以上分析知道在p上进行一次左旋即可。 - 方法二:
无法实施,因为存在红孩子。
- 方法一:
总结 不变孩子为黑,变孩子为红,进行单次旋转转为不变孩子为红的情况,再进一次旋转即可。
-
兄弟为黑,其孩子都为黑(LbIII、RBIII)
- 方法一:x侧加黑节点
假设x在b左侧,因为孩子均为黑,如果在p上左旋让x侧加黑,则p和b交换颜色,然后左旋,会导致兄弟b右侧子树的黑高减一,因为b必须填补p的颜色,导致黑色节点数减一,且无法增加黑色节点,因为b的孩子均为黑,不易操作,且如果子树都是满黑色节点更无法增加。如果在b上进行操作,因为左孩子为黑,旋转后左孩子的黑高会减一,不易操作。对于x在b的右侧情况类似。 - 方法二:父节点加黑
则令x=p,递归向上调整直到遇到红色节点停。如果给父节点增加一层黑色(准确说是祖先节点,因为如果父节点为黑,黑色会增加给祖先),会导致x兄弟分支会多出一个黑高,所以给父节点增加一层黑色需要兄弟需要移除一个黑高,以保证父节点增加黑高时(更确切的说是祖先节点增加黑高时)兄弟分支所有节点黑高不变,因为已经移除了一个黑高,所以路径上方增加一个黑高不会影响自己的分支。方案可行,只要将兄弟b的颜色改为红即可,因为孩子都是黑的,无影响。
总结 只可以使用给父节点加黑的操作,然后可能需要递归,如果p刚好是红则不需要,如果是黑则需要递归往上,令x=p。
- 方法一:x侧加黑节点
2.3.3. 调整
-
兄弟为红
此时根据双重黑节点x在b的左还是右分Lr型和Rr型,分析Lr,另外一种镜像对称。- Lr
图2.4 所以对Lr来分析。y为黑,x是黑,b红色,所以p必须是黑色,且b必须有两个孩子是黑色,因为从p的黑高来分析之前y为黑色,p的黑高为2,那么b是红,必须保证b的黑高为1,所以其必须有两个黑孩子。进行一次左旋,将p变为红,b变为黑,而后 α \alpha α成为x的兄弟节点,令 b = α b=\alpha b=α,那么问题转为b是黑的情况
-
兄弟为黑
x在b的左右分为两种大类,根据不变孩子的颜色分为I、II、III型,不变孩子为红则是I型,不变孩子黑且变孩子红II型,孩子均为黑时III型。分析LbI、LbII、LbIII,另外种RbI、RbII、RbIII对称。-
LbI
图2.5 对于LbI型,只需要在p上进行左旋,然后将p变成黑色,b变成p原先的颜色,然后b不变孩子即右孩子变成黑色即可。图中阴影表示可能是红色,可能是黑色。 -
LbII
图2.6 对于LbII型,只需要在b上进行右旋,然后将b变成红色,b的左孩子变成黑色,图中阴影表示可能是红色,可能是黑色,空白是红色,黑色即黑色,旋转后右边圈出部分其实为LbI型,进一步按LbI型操作即可。 -
LbIII
图2.7 对于LBIII型,将b变成红色,然后令x=p,即使得x向上迁移(本质是尝试对祖先加黑),那么根据新的x可继续按照以上所列类型来处理。例如如果新x是黑色,那么查看兄弟节点,如果新x是红色,立即上色结束。
-
个人总结
对于删除时,如果消失的节点、替代的节点、兄弟节点、不变孩子是红色的都是容易解决掉的,但是如果是黑色的就不好解决。
2.3.4. 实现
注意:本实现和算法导论中的实现不一样,使用了NULL,而非T.nil,如果和算法导论中的fixup保持一致会导致调整时传入了NULL节点则无法调整,本实现采取的方式是零时生成一个nil节点(无意义的哨兵节点,但是可以保证程序继续跑),而算法导论中是通过T.nil哨兵来记录的,在算法导论中的RB-TransPlant当中,如果涉及嫁接,那么会临时将T.nil哨兵的p指到nil的父。也就是说虽然T.nil只有一个,被所有叶节点所指,但是他却可以临时记录最后操作的nil是树中哪个叶节点,因为T.nil->p = leaf;
便可记录,这是算法导论版本的T.nil精巧的地方。
#define RED 0
#define BLACK 1
void RbTreeTransplant(RbTree *T, TreeNode *x, TreeNode *y);
void RbTreeDeleteFixup(RbTree *T, TreeNode *p, TreeNode *x);
void RbTreeDelete(RbTree *T, ket_t key);
void RbTreeRotate(TreeNode *x);
void RbTreeRightRotate(RbTree *T, TreeNode *x) {
if (T == NULL || x == NULL) return;
TreeNode *y = x->left;
if (y == NULL) return;
if (x->p == NULL) {
T.root = x->left;
} else if (x->p->left == x){ // x is left child
x->p->left = y;
} else { // x is right child
x->p->right = y;
}
y->p = x->p;
x->left = y->right;
if (y->right != NULL) {
x->left->p = x;
}
y->right = x;
x->p = y;
}
void RbTreeLeftRotate(RbTree *T, TreeNode * x) {
if (T == NULL || x == NULL) return;
TreeNode *y = x->right;
if (y == NULL) return;
if (x->p == NULL) {
T.root = y;
} else if (x->p->left == x) {
x->p->left = y;
} else {
x->p->right = y;
}
y->p = x->p;
x->right = y->left;
if (y->left != NULL) {
x->right->p = x;
}
y->left = x;
x->p = y;
}
// 把任何一颗子树(包括空)嫁接到一棵树上某个节点上,不可嫁接到空的节点
void RbTreeTransplant(RbTree *T, TreeNode *x, TreeNode *y) {
if (T == NULL || x == NULL) return; // 不可以将子树嫁接到一个空节点或者空树上
if (x->p == NULL) { 嫁接整棵树,其实就是移栽了
T.root = y;
} else if (x->p->left == p) {
x->p->left = y;
} else {
x->p->right = y;
}
if (y != NULL) {
y->p = x->p;
}
}
void RbTreeDelete(RbTree *T, key_t key) {
TreeNode *node = BSTSearch(T, key);
TreeNode *p = NULL; // 如果x是空节点,那么必须要记录删除后其父亲节点
if (node == NULL) return;
color_t disappear_node_color = node->color; // 记录消失节点的颜色,因为默认消失的是删除节点
TreeNode *x = NULL; // 记录替换的节点;
if (node->left == NULL) { // 消失的节点时被删除的,右子树嫁接到自己的位置,即替换节点x是右孩子, 可能是空
RbTreeTransplant(T, node, node->right);
x = node->right;
if (x == NULL) {
p = node->p;
}
} else if (node->right == NULL) { // 消失节点是被删除节点,嫁接左孩子,即x是左孩子,可能是空
RbTreeTransplant(T, node, node->left);
x = node->left;
if (x == NULL) {
p = node->p;
}
} else { // 消失节点是被移动的后继,也就是删除节点是有两个孩子的节点
TreeNode *successor = RbTreeMinium(node->right); // 一定存在,如果不存在那么右孩子为空不会运行到这里
disappear_node_color = successor.color; // 需要重新记录,因为默认消失的是删除节点,但是目前发现不是
x = successor.right; // 替换节点x是y的右孩子,可以为空
if (successor->p != node) { // 后继刚好是删除节点的儿子时,无须移植过程
RbTreeTransplant(T, successor, x);
if (x == NULL) {
p = successor->p;
}
successor->right = node->right;
successor->right->p = successor;
} else {
if (x == NULL) {
p = successor;
}
}
RbTreeTransplant(T,node,successor);
successor->left = node->left;
successor->left->p = successor;
successor->color = node->color;
}
if (disappear_node_color == BLACK) {
RbTreeDeleteFixup(T, p, x);
}
}
void RbTreeDeletFixup(RbTree *T, TreeNode *px, TreeNode *sx) {
if (sx == NULL) { // sx如果是空,则生成临时的nil节点,且让px指向
sx = new TreeNode();
sx->p = px;
sx->color = BLACK;
if(px->left == NULL) { // px只可能有一个null,因为只有px儿子是黑色,才会调整,px之前的儿子是黑色,另外一边必须有节点,否则不满足黑高
px->left = sx;
} else {
px->right = sx;
}
}
// 以上目的是为了防止sx是空,无法找到其父节点,参数px也是据此所额外传递的参数
TreeNode *x = sx;
while (x->p != NULL && x->color == BLACK) {
TreeNode *p = x->p;
if (p->left == x) { // Left
TreeNode *b = p->right;
if (b->color == RED) { // Lr
RbTreeLeftRotate(T, p);
p->color = RED;
b->color = BLACK;
} else {
if ((b->right != NULL && b->right->color == BLACK) && (b- >left != NULL && b->left->color == BLACK) || (b->left == NULL && b->right == NULL)) { // LbIII, 不是空就全黑,或者全空
b->color = RED;
x = p;
} else if (b->right != NULL && b->right->color == BLACK) { // LbII 不变孩子为黑,但是变孩子为红,因为不是两个全黑,否则走上面的
RbTreeRightRotate(T, b);
b->color = RED;
b->p->color = BLACK;
b = b->p; // 转为LbI,走下一轮循环处理最后一种情况后结束
} else {
// LbI
RbTreeLeftRotate(T, p);
b->color = p->color;
p->color = BLACK;
b->right->color = BLACK;
x = T.root; // 结束
}
}
} else {
TreeNode *b = p->left;
if (b->color == RED) { // Rr
RbTreeRightRotate(T, p);
p->color = RED;
b->color = BLACK;
} else {
// RbIII
if ((b->right != NULL && b->right->color == BLACK) && (b- >left != NULL && b->left->color == BLACK) || (b->left == NULL && b->right == NULL)) { // RbIII, 不是空就全黑,或者全空
b->color = RED;
x = p;
} else if (b->left == NULL || b->left->color == BLACK) {
RbTreeLeftRotate(T, b);
b->color = RED; // 着色为红
b = b->p; // 让b上移
b->color = BLACK; // 新b需要着色为黑
} else {
RbTreeRightRotate(T, p);
b->color = p->color; // 改变b的颜色为p的颜色
p->color = BLACK; // p的颜色变黑
b->left->color = BLACK; // b左孩子节点需要变成黑色
x = T.root; // 结束
}
}
}
}
x->color = BLACK;
// 消除用nil节点代替NULL的动作带来的影响。
if (px->left == sx) {
px->left == NULL;
} else {
px->right = NULL;
}
}