红黑树
平衡二叉排序树
二叉排序树+平衡性质
红黑树相比AVL树对于高度的要求更松散。红黑树可以看做松散的AVL树。AVL树通过改变颜色变化一定能变成一颗红黑树。
对于红黑树而言,某高度下最少结点数目low(H) = low(H - 1) + low(H / 2) + 1
红黑树的平衡条件控制了最短边和最长边的关系,所以整体高度的下限仍然控制为log(n)级别,不会退化成链表。
一、红黑树的平衡条件
五个条件
1.每个节点非黑即红
2.根节点是黑色
3.叶节点(NIL)是黑色
NIL即“NULL is legal”(空节点是合法的),所以在红黑树中没有空节点,只有所规定的NIL,NIL的颜色可以看做黑色。
4.如果一个节点是红色,则它的两个子结点都是黑色的
所以两个红色结点是不能在一起的,两个黑色结点可以在一起。
5.从根节点出发到所有叶节点路径上,黑色节点数量相同
由此可以推出:
1.如果一个结点存在黑子结点,那么该结点肯定有两个子结点
2.最长路径最多为最短路径的两倍
【这里只是简单画出路径,其他部分没有补全】
简单的举个例子,最短路径是左边的黑-黑-黑,最长路径一定是黑红相间的,如右边的黑-红-黑-红-黑-红,可以看出最长路径是最短路径的两倍。
二、调整策略
- 插⼊调整站在 祖⽗节点 看,目的是消除“双红”(即两个红色结点连在一起)。
- 删除调整站在 父 节 点 \color{red}{父节点} 父节点 看,目的是消除“双黑”(即双重黑)。
- 插⼊和删除的情况处理⼀共五种
(所以红黑树是一种基础数据结构)误
1.插入调整的情况
注意:插入时我们都是以
红
色
节
点
\color{red}{红色节点}
红色节点的形式插入
因为如果插入新的黑色节点那么肯定会破坏条件5,所以一定会造成失衡,而插入红色节点,只要其父亲节点为黑色就不失衡,父亲节点为红色才失衡,所以插入红色节点,这样选择更好。
故插入调整就是干掉“双红”的情况
插入调整根据叔叔节点的颜色进行分类,分为情况一和情况二
插⼊调整站在 祖⽗节点 看,目的是消除“双红”(即两个红色结点连在一起)。
情况⼀(4):叔叔节点为红色
插入的节点为x,其叔叔节点(uncle)为红色,如下图所示(只是非常大的红黑树中的一部分,可以想象继续补全),因为是一部分,所以要保证调整后该部分各路径上提供的黑色节点个数不变。
这里只列出一种情况,还有三种插入位置不同,同理。
处理办法:1和20修改成⿊⾊,15修改成红⾊(所谓的红⾊上顶)
分析:可以知道
黑 和 红 是等价的。
红 红 黑 黑
这样不断向上递归调整颜色,那么就算到最后根节点变为红色,也就只需要根节点再变为黑色就好。
最后调整后如图所示:
情况⼆(4):叔叔节点为黑色
这里有四种情况,怎么区分呢,站在祖父节点往下看,根据两个红色所在的位置,可以分为LL,LR,RR,RL。
(1)LL类型(RR类似)
下图就是LL类型(即双红的冲突处在祖父节点的左子树的左子树)这里可以看为情况一的红色上顶的操作下又造成了双红的情况,且其叔叔节点为黑色。
处理办法:⼤右(左)旋,20调整成红⾊,15调整成⿊⾊,即可搞定问题
分析:同AVL树调整LL型,我们先以20为旋转的点,进行大右旋,可以得到如图:
对于这种LL的情况,我们分析一下哪些节点的颜色是确定的,哪些节点的颜色是特例。
可以知道除了17这个节点是特例外(即不一定是红色,有没有17都不一定),其他都是通用情况,是确定情况下的颜色。
同理,要保证调整后该部分各路径上提供的黑色节点个数不变,原先旋转前各路径提供的黑色节点个数为2个,所以这旋转之后图中所画的小帽子要给下面每条路径提供的都是1个黑色节点(既可以长成红黑黑,也可以调整为黑红红,所以调整后的红黑树可能不一样),如果题目要求了插入调整的时候,红色节点尽量靠上。那么我们选择进行红黑黑即红色上浮,这里我们进行的是红色下沉操作,(注:代码实现里写的是红色上浮)即将20号节点改为红色,15号节点改为黑色。(17若存在则接在19下面)
(2)LR类型(RL类似)
下图就是LR类型(即双红的冲突处在祖父节点的左子树的右子树)这里可以看为情况一的红色上顶的操作下又造成了双红的情况,且其叔叔节点为黑色。
处理办法:首先对x处进行左旋操作,得到与情况2的(1)LL型开始相同的结果,然后进行同情况2的(1)LL型的大右旋操作即可
分析:
我们也像AVL平衡调整一样先小左旋再大右旋,观察分析可以发现:
对连着的两个红色节点进行左旋/右旋操作不会影响路径上的黑色节点数目
举个例子,如下,拉着两个红色节点旋转,旋转前后依旧满足黑色节点数目相等。所以可以放心地进行小左旋和小右旋操作。
先进行小左旋后,图就变成了下图,就又变成了(1)中的LL型,再进行大右旋,20调整成红⾊,15调整成⿊⾊即可。
总结一下插入调整的情况:
总的分为两大类,根据叔叔节点的颜色来区分,情况一(叔叔结点是红色),就直接红色上顶,改变颜色即可,情况二(叔叔结点时黑色),同AVL调整策略分为LL,RR,LR,RL四种情况,LR或RL 可以先进行小左旋和小右旋后转化为LL或RR类型后再进行大右旋或者大左旋,旋转之后再进行颜色调整,颜色调整时只需要考虑上面三个颜色即可,改成黑红红和红黑黑都可以。
2.删除调整的情况
双黑结点产生的条件
简单分析一下
删除的节点有度为0,度为1,度为2的节点,这里度为2可以先不考虑,因为度为2的节点可以转化为删除度为1或度为0的节点。
对于度为1的节点
我们简单举下例子,可以分为下面的三种情况
一:度为1的节点为红色
二:度为1的节点为黑色,且子节点为黑色
三:度为1的节点为黑色,且子节点为红色
分析:
一:不可能有这种情况
根据性质,红色节点不可能度为1,因为红色结点子结点必为黑色,若只有一个则不能满足条件5,同理得度为1的节点子节点一定为红色。
二:也不可能有这种情况
根据性质,如果度为1的节点为黑色节点且其子节点为黑色,那么肯定破坏了条件5。
三:只有这种情况唯一存在,且红色节点为看的见的叶子结点,即其下面没有子节点
原因:若红色节点下面还有黑色节点,那么肯定会破坏了条件5,因为上面的黑色节点度为1,对于右边的路径只提供一个黑色节点,而左边却提供了两个。
直接删除连接上去肯定会破坏条件5,提供的黑色节点数目变少了,所以在删除后,将删除节点的颜色加到子节点上即可,即子节点变为黑色。
总结:对于度为1的节点的删除,该结点一定为黑色,子结点为红色,删除后将删除节点的唯一子结点变为黑色。
对于度为0的节点
同样举下例子,可以分为两种情况:
一:待删除结点为黑色节点
二:待删除结点为红色节点
删除调整后:
一:删除节点后,为了保证调整后该部分各路径上提供的黑色节点个数不变,如果不改变含该删除节点路径的黑高度,那么树的其它部分的黑高度就必须做出相应的变化来适应它。所以,我们想办法恢复原来含这个节点的路径的黑高度。做法就是:无条件的把删除节点的黑色推到它的子节点上去。(子节点可能是NIL节点)。这样,X就可能具有双重黑色,或同时具有红黑两色,所以这里的NIL具有双重黑属性,即计算路径上的黑色结点数目时,代表有两个黑色节点。这也是双重黑产生的条件。在调整策略旋转过程中,NIL产生的双重黑就向上移动,移动到了我们可以看见的节点上.
所以删除调整就是为了干掉双重黑。
二:度为0的红色结点,直接删除,不影响平衡。
总结:
删除节点,对于度为1和度为0的就是删完之后,把删除节点的颜色加到其子孩子上,然后把子孩子接上去。度为1就是把黑色加到其红色子孩子上(变成了黑色),度为0就是加到NIL上去(此处就产生了双重黑)对于度为2的就是找到前驱,替换上去,然后转化为删除度为0或者度为1的结点。
以下是删除的平衡调整的策略:
根据双重黑节点x的兄弟节点是黑色或红色来分类,以下讨论的三种情况都是兄弟节点为黑色,红色的情况可以转化为黑色,在下面也有说明
删除调整站在父节点看,目的是消除“双黑”(即双重黑)
情况⼀(2):最简单的情况
双重黑节点x的兄弟节点为黑色节点,且兄弟节点的两个子节点也是黑色。(即没有红色)
处理办法:brother 调整为红⾊,x 减少⼀重⿊⾊,father 增加⼀重⿊⾊
分析:
这里我们想要消除双重黑,将双重黑节点减一重黑色,那么为了保证调整后该部分各路径上提供的黑色节点个数不变,所以父节点增加一重黑(即双重黑上移了),那么兄弟节点得减一重黑色(即变为红色),那么这里可不可以变成红色呢,可以的,因为两个子节点都是黑色节点
双重黑向上移动,直至根节点就可以直接将根节点变为普通黑,而满足平衡条件。
情况二(4):
双重黑节点x的兄弟节点为黑色节点,且兄弟节点的两个子节点中有红色节点
站在父节点上,根据其兄弟节点所在位置以及兄弟节点的右子树颜色来区分
兄弟节点在父节点的右子树上则第一个字母为R,兄弟节点的右子树为红色节点,则第二个字母为R,此即为RR类型(这里注意只要右子树为红色就为RR,所以写代码判断时要注意右子树是红色和左子树不是红色的逻辑区别,LL也是这个道理)
其他LL,RL,LR同理。
(1)RR类型(LL类似)
举个例子,这里兄弟节点在右子树,且兄弟节点的右子节点为红色,所以为RR类型
处理办法:father 左(右)旋,由于⽆法确定48的颜⾊,所以38改成⿊⾊,51改成38的颜⾊,x 减少⼀重⿊⾊,72改成⿊⾊。
分析:
先进行一个大左旋,得到如下:
在此情况下我们可以确定哪些节点颜色呢?(图中标蓝的都是确定的)28是双重黑节点是确定的,28的兄弟节点是黑色,所以51也是确定的,又RR类型72的颜色是红色是确定的,72红色下的节点一定是黑色,所以64和85也是确定的,除此之外其他节点的颜色都不确定。
所以这里48的颜色是不确定的,是特例,可能是红色也可能是黑色,为了保险起见,那么38的颜色一定要改为黑色。
为了保证调整后该部分各路径上提供的黑色节点个数不变,原先各路径上提供的黑色节点数都为2,现在38改黑后,左边路径变成了4个,所以28节点从双重黑变为正常黑。那么现在只能改51了,所以51得变为红色,右边路径也得保持不变,所以72得变为黑色。
总结:直接进行一个大左旋(或大右旋),兄弟节点(brother)改为原先的根的颜色,现在根的两个子节点都改为黑色。
(2)RL类型(LR类似)
举个例子,这里兄弟节点在右子树,且兄弟节点的左子节点为红色,所以为RL类型
处理办法:brother 右(左)旋,51变⿊,72变红,转成处理情况二的(1)类型
分析:
先进行一个小右旋,得到如下:
分析一下确定颜色的节点(图中标蓝的都是确定颜色的)因为是RL类型,72的颜色一定是黑色,51的颜色是红色,两个子节点一定是黑色(即48和64一定是黑色),85的颜色一定是黑色(不然就是RR类型)
翻转之后左子树上少了一个黑色节点,为了保证调整后该部分各路径上提供的黑色节点个数不变,所以将51改为黑色,又51改黑之后,右子树又多了,故72改为红色节点(这里两个子节点都是确定性的黑色,所以72可以改为红色)。
简单地说就是原来的根节点和后来的根节点调换颜色。
这样就变成了(1)中的RR的情况,在参照上边的操作调整即可。
情况三:兄弟节点为红色节点
对于兄弟节点为红色节点的情况
举个例子,如下所示:
对兄弟结点进行右旋操作,(双重黑节点在哪就往哪旋),此时左边路径的黑色节点数目少一个,所以为了保证调整后该部分各路径上提供的黑色节点个数不变,改变兄弟结点41为黑色,这时右边路径的黑色节点数又多一个,所以改父节点85为红色,此时虽然双重黑节点依然存在,但此时已经转化为了兄弟节点为黑色的情况,再像上面所写的兄弟节点为黑色的情况讨论调整即可。
三、代码实现
#include <stdio.h>
#include <stdlib.h>
#define K(n) (n->key)
#define C(n) (n->color)
#define L(n) (n->lchild)
#define R(n) (n->rchild)
//红黑树结构定义
typedef struct Node {
int key; //存储的键值
int color; // 存储的颜色,0 red, 1 black, 2 double black
struct Node *lchild, *rchild;
} Node;
Node __NIL; //NIL结点
#define NIL (&__NIL)
//先于主函数执行,初始化NIL节点
__attribute__((constructor))
void init_NIL() {
NIL->key = 0;
NIL->color = 1;
NIL->lchild = NIL->rchild = NIL;
return ;
}
Node *getNewNode(int key) {
Node *p = (Node *)malloc(sizeof(Node));
p->key = key;
p->color = 0; //新生成的节点颜色默认为红色
p->lchild = p->rchild = NIL;
return p;
}
int hasRed(Node *root) { //有没有红色子孩子
return C(L(root)) == 0 || C(R(root)) == 0;
}
//左旋,右旋三步走
Node *left_rotate(Node *root) {
Node *temp = root->rchild;
root->rchild = temp->lchild;
temp->lchild = root;
return temp;
}
Node *right_rotate(Node *root) {
Node *temp = root->lchild;
root->lchild = temp->rchild;
temp->rchild = root;
return temp;
}
//插入的平衡调整
Node *insert_maintain(Node *root) { //传入的结点当做分析所站的祖父节点
if (!hasRed(root)) return root; //如果没得红色子孩子就不会有冲突
int flag = 0;
if (C(L(root)) == 0 && hasRed(L(root))) flag = 1; //双红冲突在左子树中发生
else if (C(R(root)) == 0 && hasRed(R(root))) flag = 2;//双红冲突在右子树中发生
if (flag == 0) return root; //没有冲突
//解决双红冲突
//以下if和else if是插入调整的情况二:叔叔节点为黑色
if (flag == 1 && C(R(root)) == 1) { //冲突在祖父节点的左子树且叔叔节点为黑色
if (C(R(L(root))) == 0) { //左子树的右子树为红色,即LR,需要进行小左旋
root->lchild = left_rotate(root->lchild);
}//变为LL
root = right_rotate(root); //最后都要进行大右旋
} else if (flag == 2 && C(L(root)) == 1) { //冲突在祖父节点的右子树且叔叔节点为黑色
if (C(L(R(root))) == 0) { //右子树的左子树为红色,即RL,需要进行小右旋
root->rchild = right_rotate(root->rchild);
}//变为RR
root = left_rotate(root); //最后都要进行大左旋
}
//最后进行颜色调整,情况二可以红色上浮也可以红色下沉,这里写为红色上浮,可以统一直接写。
//情况一,简单地红色上浮,黑红红变为红黑黑
root->color = 0; //红色上浮
C(L(root)) = C(R(root)) = 1; //子节点为黑黑
return root;
}
//找前驱
Node *predecessor(Node *root) {
Node *temp = root->lchild;
while (temp->rchild != NIL) temp = temp->rchild;
return temp;
}
//删除的平衡调整
Node *erase_maintain(Node *root) {
if (C(L(root)) != 2 && C(R(root)) != 2) return root; //没有双重黑即没有冲突
//有双重黑
//双重黑结点的兄弟结点为红色结点,对应情况三
if (hasRed(root)) {
int flag = 0;
root->color = 0; //旋转前的根节点变为红
if (C(L(root)) == 0) root = right_rotate(root), flag = 1;
else if (C(R(root)) == 0) root = left_rotate(root), flag = 2;
root->color = 1; //旋转后根节点变为黑
//此时双重黑依旧存在,但是其兄弟节点变为了黑色
if (flag == 1) root->rchild = erase_maintain(root->rchild);
else root->lchild = erase_maintain(root->lchild);
return root;
}
//双重黑结点的兄弟结点为黑色结点
if (C(L(root)) == 1) {//L
C(R(root)) = 1; //双重黑改为正常的黑色
if (!hasRed(L(root))) { //最简单的情况,对应情况一
C(root) += 1; //根节点的颜色加一重黑色
C(L(root)) -= 1; //左子树的颜色减一重黑色
return root;
}
//对应情况二
if (C(L(L(root))) != 0) { //LR(左子树的左子树不是红色) 先进行小左旋,并替换颜色
C(L(root)) = 0;//原来的根节点变为红色
root->lchild = left_rotate(root->lchild);
C(L(root)) = 1; //新的根节点变为黑色
}
//LL 最后都要大右旋
C(L(root)) = C(root); //新的根节点改为原来根节点颜色
root = right_rotate(root);
C(L(root)) = C(R(root)) = 1; //两个子节点都强制改为黑色
} else { //R
C(L(root)) = 1;
if (!hasRed(R(root))) { //最简单的情况,对应情况一
C(root) += 1;//根节点的颜色加一重黑色
C(R(root)) -= 1; //右子树的颜色减一重黑色
return root;
}
//对应情况二
if (C(R(R(root))) != 0) { //RL 小右旋,并替换颜色
C(R(root)) = 0;
root->rchild = right_rotate(root->rchild);
C(R(root)) = 1;
}
//RR 最后都要大左旋
C(R(root)) = C(root); //新的根节点改为原来根节点颜色
root = left_rotate(root);
C(L(root)) = C(R(root)) = 1;//两个子节点都强制改为黑色
}
return root;
}
Node *__erase(Node *root, int key) {
if (root == NIL) return root;
if (key < root->key) {
root->lchild = __erase(root->lchild, key);
} else if (key > root->key) {
root->rchild = __erase(root->rchild, key);
} else {
if (root->lchild == NIL || root->rchild == NIL) { //root为度为1或者0的结点
Node *temp = root->lchild == NIL ? root->rchild : root->lchild; //找到唯一子孩子
//无论是度为1还是度为0的结点都要把颜色加到它的子孩子上去
//度为1就是把黑色加到其红色子孩子上,度为0就是加到NIL上去
temp->color += root->color; //此处会产生双重黑
free(root);
return temp;
} else {//度为2的结点
Node *temp = predecessor(root);
root->key = temp->key;
root->lchild = __erase(root->lchild, temp->key);
}
}
return erase_maintain(root); //最后返回调整平衡后的根节点
}
Node *erase(Node *root, int key) {
root = __erase(root, key); //真正的删除操作
root->color = 1; //根节点强制为黑色
return root;
}
Node *__insert(Node *root, int key) { //返回插入调整之后的根节点
if (root == NIL) return getNewNode(key);
if (root->key == key) return root;
if (key < root->key) root->lchild = __insert(root->lchild, key); //左子树中去插入
else root->rchild = __insert(root->rchild, key); //右子树中去插入
return insert_maintain(root); //插入平衡调整
}
//插入操作
Node *insert(Node *root, int key) {
root = __insert(root, key); //真正的插入操作
root->color = 1; //插入调整以后根节点颜色一定要是黑色
return root; //返回插入以后的根节点的地址
}
//删除操作
void clear(Node *root) {
if (root == NIL) return ;
clear(root->lchild);
clear(root->rchild);
free(root);
return ;
}
void output(Node *root) { //输出键值(颜色) | 左子树键值,右子树键值
if (root == NIL) return ;
printf("(%d(%d) | %d, %d)\n", K(root), C(root), K(L(root)), K(R(root)));
output(L(root));
output(R(root));
return ;
}
void in_order(Node *root) { //中序遍历
if (root == NIL) return ;
in_order(root->lchild);
printf("%d ", K(root));
in_order(root->rchild);
return ;
}
int main() {
int op, val;
Node *root = NIL;
while (~scanf("%d%d", &op, &val)) {
switch (op) {
case 1: root = insert(root, val); break;
case 2: root = erase(root, val); break;
}
printf("\n======= red black tree =======\n");
output(root);
in_order(root);
printf("\n");
printf("======= red black tree end =======\n");
}
return 0;
}
插入操作演示:
删除演示: