ACM大牛带你玩转算法与数据结构-课程资料
本笔记属于船说系列课程之一,课程链接:
哔哩哔哩_bilibilihttps://www.bilibili.com/cheese/play/ep66799?csource=private_space_class_null&spm_id_from=333.999.0.0
你也可以选择购买『船说系列课程-年度会员』产品『船票』,畅享一年内无限制学习已上线的所有船说系列课程:船票购买入口https://www.bilibili.com/cheese/pages/packageCourseDetail?productId=598
做题网站OJ:HZOJ - Online Judge
Leetcode :力扣 (LeetCode) 全球极客挚爱的技术成长平台
红黑树
建议看这篇文章时看看二叉排序树和AVL,不然理解不了其中的东西:二叉排序树和AVL树
红黑树是如何构建的,并且他是如何进行像AVL树可以将左右子树的节点进行平衡的,就是基于它的5条性质:
- 每个节点是红色或黑色。
- 根节点是黑色。
- 所有叶子节点都是黑色(这里的叶子节点是指树中所有的空节点)。
- 每个红色节点的两个子节点都是黑色(从每个红色节点到叶子节点的路径上不能有两个连续的红色节点)。
- 从任一节点到其每个叶子节点的所有路径都包含相同数目的黑色节点。
性质1:他对于平衡二叉树种的每个节点多了一个属性,也就是颜色每个节点非黑及红;
性质2:红黑树的最顶端的根节点一定是黑色的,因为如果根节点是红色会将后续的插入和删除操作变得更复杂。
性质3:注意:这里的空节点是上一篇文章中创建AVL树用到的一个虚拟空节点NIL节点,为什么需要用到NIL这个虚拟空节点是一个编程技巧,让我们编程的时候方便进行一些判断和操作,在后续的删除操作会用到对NIL节点进行一些操作为了编程时进行更方便的进行平衡操作。
性质4:红色节点只能接黑色节点,这个性质就是为了保证红黑树的平衡
性质5:这条性质也是为了保证红黑树的平衡,并且在计算时需要加上那个虚拟空节点,虚拟空节点的颜色是黑色
在实现红黑树时,一定要心里铭记这5条性质!!!!
红黑的难点:
红黑树的难点,就是基于二叉排序树的删除和插入操作后的平衡操作。
我的老师教给我的平衡终极大法:
插入调整时站在祖父节点看,什么意思就是当前插入节点如果违反了性质4,因为插入的节点是红色节点,为什么是红色节点在后续会说,那么插入的节点是红色节点,违反了性质4,那么它的父节点那么一定也是红色,然后我们还需要往上找到它父节点的父节点也就是祖父节点,然后进行来调整操作;
删除调整站在父节点看,同理插入调整怎么来进行看的。
插入操作:
问题:插入节点应该是什么颜色?
答:红色,为什么呢?因为如果插入的节点是黑色,那么一定为违反性质5因为在没有插入黑色节点之前每条路径的黑色节点的数量是相同的,插入一个黑色节点那么这条路径就一定会多一个黑色节点,那么就需要进行平衡操作,而插入红色节点如果它的父节点是黑色,那么没有违反性质4和性质5那么就不需要平衡,如果父节点是红色就需要平衡操作,那么平衡的概率是50%,而插入黑色是100%需要调整,所以选择插入红色节点。
插入调整分为两种情况:
情况1:
下面白色就当黑色节点
插入节点是x,它的父节点20是红色的,然后口诀是什么,找到它的祖父节点来看,那么它的祖父节点是18,那么它的叔叔节点是15并且也是红色的。
那么如何进行调整,记住调整一定是围绕的那5条性质并且主要是性质4和性质5,那么直接将祖父节点变为红色,叔叔节点和父节点变为黑色。
通过调整发现,这颗子树已经变得平衡。
情况2:
发生失衡节点是节点10,并它的叔叔节点是黑色的情况,那么找到它的祖父节点,往下看去它和AVL树种LL类型是否很像,那么我们就称这种类型为LL类型:
然后通过确定图中节点的颜色
黑色:18,20,5,16,14
红色:15,10
每条路径的黑色节点都为2个
17节点的颜色是不确定的,因为图中的子树是没有画完全的,它是一颗子树,它现在的祖父节点上面还可能会有节点,17节点下面也可能会有节点,所以17节点的颜色是无法确定的,为什么图中17是红色的,是为了让各位读者看起来这颗子树没有违反性质5
然后确定了每个节点的颜色以后,那么就需要进行平衡操作,如何进行平衡操作:
先对祖父节点进行一个右旋:
然后进行调整颜色,为了每条路劲的黑色节点都为2个,而且让这个子树满足性质4和性质5,将10节点和18节点变为红色,15节点也就是当前的根节点变为黑色,或者将现在的根节点变为红色,10和18节点变为黑色都可以,并且不影响。这里我用的是红色下沉,也就是根为黑,子树为红:
通过调整颜色后,可以发现目前的子树满足性质4和性质5,
那么处理完LL类型,那么LR类型如何去处理呢,那么就是通过AVL中学到的技巧,通过父节点的一个左旋将LR类型变为LL类型来处理就可以了:
那么这里通过25节点一个左旋,就变为了LL类型,就可以通过LL类型方式相同处理:
RR和RL处理,对称过来就可以,就不详细展开说了。
删除操作:
问题:删除节点的颜色会引发红黑树失衡?
答:那么就有6种情况:
- 红,度为0,直接删除没有任何影响,删除后不会违反性质4和性质5,不会影响平衡;
- 黑,度为0,删除度为0的黑色节点就会用到上面提到的NIL虚拟空节点了,删除度为0的黑色节点后,会用一个NIL去代替它的位置对吧,然后将NIL节点的颜色变为双重黑,这里是不是违反了第1条性质节点非黑及红,那么删除调整的难点就在这里,如何进行调整这个双重黑。
- 红,度为1,由于性质5,每条路上的黑色节点数量相同,并且红色节点只能接黑色节点,那么红色节点要么不接节点,要么必须接2个黑色节点,所以红色节点并且度为1的情况不存在;
- 黑,度为1,红色度为1的情况,如果当前节点度为1那么为了维护性质4和5,那么它的子孩子一定是红色,然后直接删除当前节点把它唯一子孩子放在当前节点的位置,并且把它的唯一子孩子改为黑色,这样就删除了度为1的黑色节点;
- 度为2的节点,通过二叉排序树的删除操作,可以将删除度为2的节点变为删除度为0和1的节点;
那么下面的情况就是对于双重黑的处理:
蓝色表示双重黑节点,白色表示黑色节点
注意:但是图中有些节点的颜色是不确定的
情况1:
它的兄弟节点是黑色,并且兄弟节点的子孩子都是黑色,这里的子树是没有违反性质4和性质5的,现在的目的就是为了去除这个双重黑,并且让这颗子树平衡。
那么图中节点的颜色:
双重黑:33
黑色:27,25,28
父节点30的颜色是不确定的。
那么如何进行调整呢,33节点双重黑,就去一重黑,变为黑色节点
那么父节点增加一重黑,如果为红色变为黑色,如果为黑色变为双重黑
兄弟节点去一重黑,变为红色
这里就有人会有疑问那如果当前子树根节点为黑色,那么调整后他为双重黑了,也没有去掉双重黑呀违反了性质1啊。
二叉排序树删除节点是不是递归进行删除的,现在只是在递归到这个子树进行平衡,那么在后续的回溯过程中,当前的父节点他也可能会变为其他节点孩子,那么就进行下次的平衡处理就可以了,最后如果到了整棵树的根节点处了,那么直接给他减去一重黑即可。
情况2:
这里处理的就是RR和LL类型,下面详细解释的是RR类型:
当前情况中,双重黑节点的兄弟节点是黑色,并且他兄弟节点的右节点是红色,那么就称这种情况叫做RR类型;
然后如果兄弟节点的左节点也是红色,那么优先考虑RR类型,如果右节点为黑色那么就是RL类型。同理如果对称过来优先考虑LL类型;
图中现在每条路径的中的黑色节点数量都为2个
然后确定一下当前图中节点的颜色
双重黑:27
黑色:40,56,80
红色:72
不确定:30,36,56,80
RR类型,那么就父节点直接一个左旋:
然后为了满足性质5:
36,30节点的颜色是不确定的,如果36为红色30也为红色就违反了性质4,所以必须把30号节点变为黑色;
27减少一重黑,变为黑色;
那么现在40号节点右子树路劲上每条路都少一个黑色节点来满足性质5,那么就将72变为黑色,那么每条路径上的黑色节点都为3个了:
这里红黑树平衡了,但是最开始每条路径的中的黑色节点数量都为2个,因为要考虑的是整棵树,如果这颗子树平衡后他目前的每条路多了一个黑色节点,那么它上面还有节点的话往下来看,这边子树路径中多了一个黑色节点,而另外的子树路径中刚好少了一个节点,那么就违反了性质5,所以需要将现在的节点40改为红色,保持最开始子树中每条路径的黑色节点数。
然后如果最开始图中的父节点30颜色为黑色,那么每条路劲都有3个黑色节点,那么这里兄弟节点40就变为黑色;
所以这里兄弟变的颜色为之前父节点的颜色。
总结:
当前情况RR类型:
通过父节点的左旋,
将双重黑节点去一重黑,
兄弟节点通过旋转到达父节点的位置,那么新根节点的颜色变为之前根节点的颜色
最后旋转后的根节点的左右孩子都变为黑色。
LL类型同理对称后一样操作。
情况3:
这种情况,就是兄弟节点为黑色节点,并且兄弟节点的左节点为红色节点,那么这种类型就是RL类型:
每条路劲黑色节点数量为2
确定的节点颜色
双重黑:27
黑色:40,35,37,72(如果72是红色那么它一定是RR类型,RR类型优先级高于RL类型)
红色:36
那么RL类型,想办法给他变为RR类型,先对兄弟节点进行一个右旋:
然后为了维护性质5:
36变为黑色
40变为红色
也就是之前的兄弟节点变红,新兄弟节点变黑
OK变为了RR类型情况2的类型,那么直接用情况2进行处理。
然后会发现,36和40这两个节点在情况2中也进行颜色的改变,所以在编程的时候,我们不会在RL变RR类型时进行对颜色的改变,对于上面兄弟节点旋转后的颜色改变,只是为了理解这个过程。
总结情况3:
通过一个兄弟节点的右旋,然后之前的兄弟节点变红,新兄弟节点变黑,就可以得到RR类型,也就是情况2。
情况4:
兄弟节点为红色,那么兄弟节点一定为红色,那么父节点一定为黑色,35,40也一定为黑色。
所以确定颜色的节点:
双重黑:27
黑色:30,35,40
红色:39
每条路劲黑色节点为3个
那么通过父节点一个左旋,变为图中如下情况
然后维护性质5,并且之前每条路劲黑色节点为3个,那么进行调色:
39变为黑色,30变为红色:
然后通过发现,27双重黑节点的兄弟节点为黑色节点了,那么就可以进行情况1,情况2,情况3进行操作判断了。
总结情况4:
也就是最开始的兄弟节点变黑,最开始的父节点变红,然后进行一个父节点的左旋,然后通过新父节点的往下看可以得到情况1,2,3。
代码实现:
通过对于插入和删除的每种情况的分析:
对于这个总结需要自己去捋捋,每个人分析的方式是不一样的。
然后最后是代码实现:
#include <stdio.h> #include <stdlib.h> #include <time.h> #define RED 0 #define BLACK 1 #define DBLACK 2 #define NIL (&__NIL) #define K(n) (n->key) #define L(n) (n->lchild) #define R(n) (n->rchild) #define C(n) (n->color) typedef struct Node { int key, color; // 0 red, 1 black, 2 double black struct Node *lchild, *rchild; } Node; Node __NIL; __attribute__((constructor)) void init_NIL() { NIL->key = -1; NIL->color = BLACK; NIL->lchild = NIL->rchild = NIL; return ; } Node *getNewNode(int key) { Node *p = (Node *)malloc(sizeof(Node)); p->key = key; p->color = RED; p->lchild = p->rchild = NIL; return p; } //判断当前节点的左右孩子是否有红色 bool has_red_node(Node *root) { return root->lchild->color == RED || root->rchild->color == RED; } //左旋操作 Node *left_rotate(Node *root) { Node *new_root = root->rchild; root->rchild = new_root->lchild; new_root->lchild = root; return new_root; } //右旋操作 Node *right_rotate(Node *root) { Node *new_root = root->lchild; root->lchild = new_root->rchild; new_root->rchild = root; return new_root; } //平衡操作 Node *insert_maintain(Node *root) { int flag = 0; //站在祖父节点来看 //判断是在左子树还是右子树失衡 //左子树失衡flag = 1 if (C(L(root)) == RED && has_red_node(L(root))) flag = 1; //右子树失衡flag = 2 if (C(R(root)) == RED && has_red_node(R(root))) flag = 2; if (flag == 0) return root; //如果当前祖父节点的左右孩子都为红色 //那么就是情况1,也就是父节点和叔叔节点都是红色的情况 if (C(L(root)) == RED && C(R(root)) == RED) goto red_up_maintain; //这里就是L类型 if (flag == 1) { //判断是否是LR类型 if (C(R(L(root))) == RED) { L(root) = left_rotate(L(root)); } root = right_rotate(root); } //这里就是R类型 else { //判断是否是RL类型 if (C(L(R(root))) == RED) { R(root) = right_rotate(R(root)); } root = left_rotate(root); } red_up_maintain: //通过旋转或者判断之后 //现在子树最顶上的三个节点进行修改颜色 C(root) = RED; C(L(root)) = C(R(root)) = BLACK; 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); //性质2根节点一定是黑色 root->color = BLACK; 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)) != DBLACK && C(R(root)) != DBLACK) return root; //如果兄弟节点为红色, 情况4 if (has_red_node(root)) { //最开始的父节点变红色 root->color = RED; if (root->lchild->color == RED) { //如果兄弟节点在左,就右旋 root = right_rotate(root); //然后对旋转后双重黑节点的父节点,进行调整 root->rchild = erase_maintain(root->rchild); } else { //对称同理 root = left_rotate(root); root->lchild = erase_maintain(root->lchild); } //最开始的兄弟节点变黑色 root->color = BLACK; return root; } //情况1,兄弟节点的子孩子都为黑 //直接将父节点加一重黑 //父节点的左右孩子减一重黑 if ((root->lchild->color == DBLACK && !has_red_node(root->rchild)) || (root->rchild->color == DBLACK && !has_red_node(root->lchild))) { root->color += 1; root->lchild->color -= 1; root->rchild->color -= 1; return root; } //判断双重黑在那个子树 if (root->rchild->color == DBLACK) { //直接减一重黑 root->rchild->color = BLACK; //情况3 //如果不是LL类型,那么就需要进行对兄弟节点的左旋 //然后不进行节点颜色处理 if (root->lchild->lchild->color != RED) { root->lchild = left_rotate(root->lchild); } //因为旋转后兄弟节点的颜色要变为父节点的颜色 root->lchild->color = root->color; //然后对于父节点的右旋 root = right_rotate(root); } else { //对称同理 root->lchild->color = BLACK; if (root->rchild->rchild->color != RED) { root->rchild = right_rotate(root->rchild); } root->rchild->color = root->color; root = left_rotate(root); } //情况2的统一处理 //通过旋转后那,当前根节点的左右孩子都要变为黑色 root->lchild->color = root->rchild->color = BLACK; 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) { //度为0和1的节点一起处理 //获取到当前节点的子孩子,如果没有子孩子,那么这里就体现处理NIL虚拟空节点的用处了 Node *temp = root->lchild == NIL ? root->rchild : root->lchild; temp->color += root->color; free(root); return temp; } //将删除度为2的节点,变为删除度为0或1的节点进行处理 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 = BLACK; 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", C(root), K(root), K(L(root)), K(R(root)) ); output(root->lchild); output(root->rchild); return ; } int main() { srand(time(0)); #define MAX_N 10 Node *root = NIL; for (int i = 0; i < MAX_N; i++) { int x = rand() % 100; printf("\ninsert %d to red black tree : \n", x); root = insert(root, x); output(root); } int x; while (~scanf("%d", &x)) { printf("\nerase %d from red black tree\n", x); root = erase(root, x); output(root); } return 0; }