二叉树
二叉树每个节点最多有两个子节点。
特殊二叉树有:满二叉树(除叶子节点外每个节点都有两个子节点)、完全二叉树(除最下层外其他层节点全满,且最下层叶子节点靠左排列)
二叉树的存储方式有两种:一种是基于链式存储(通过指针),另一种是基于数组(左子节点为2i,右子节点为2i+1,父节点为i/2,根节点为1)
二叉树的遍历方式有四种:前序遍历(中左右)、中序遍历(左中右)、后续遍历(左右中)、按层遍历(BFS),时间复杂度都为O(n)
二叉查找树(二叉排序树)
树中任意节点,其左子树节点值都小于该节点,而右子树节点值都大于该节点。它的操作:
查:从根节点开始,小于递归左子树,大于递归右子树直到找到为止
增:从根节点开始,若小于且无左子树直接添加,否则递归左子树;大于且无右子树直接添加,否则递归右子树
删:若待删除节点无子节点,直接删除;若待删除节点只有一个子节点,只需让父节点直接指向子节点;若待删除节点有两个子节点,需要找到右子树的最小节点,把它替换到待删除节点位置后删除。优化方法:标记要删除的节点但不真正执行删除操作(省时费内存)
支持重复数据
方法一:通过链表或动态数组,把值相同的数据存储在同一个节点上
方法二:把新插入的数据当作大于这个节点的值来处理(查询时找到值后递归右子树直到叶子节点,删除也同理)
时间复杂度
增删改查都为O(logn),但是在特殊情况下(树退化为链)时复杂度为O(n)
红黑树
为避免二叉树在频繁更新中出现树高远大于logn而导致效率降低的情况,发明了平衡二叉树
平衡二叉树种类很多,如AVL树(任何节点的左右子树高度相差不超过1),SB树(以任意节点作为树的数量不能少于任意一个侄儿节点),红黑树等
关于红黑树的要求:
- 根节点为黑,叶子节点为黑且空(NIL节点)
- 若节点为红,那它两个子节点、父节点必为黑
- 从根出发到所有叶路径上黑色节点数量相同
- 新节点初始为红(可调整)
2,3两点能保证树的平衡性,使得根到叶子结点的最长路径小于最短路径的2倍(例如路径上黑色节点数为2,最短路径为黑黑nil,最长路径为黑红黑红nil)
维护平衡的三种操作:左旋、右旋、改变颜色
插入删除调整就像玩魔方一样,遇到哪种情况,就按相应规则进行旋转、改色操作。
插入调整
插入操作同二叉查找树一致,但需回溯进行插入调整(目的是为了去除连续红节点),每种情况优先度依次降低:
情况一:没有红色子节点 || 没有红色孙子节点:无需调整
情况二:左、右子节点红且孙子节点存在红:左右子节点改黑,自身改红
情况三:左子节点、左子节点的左子节点红,右子节点黑(LL型):自身右旋,自身改黑,左右子节点改红
情况四:左子节点、左子节点的右子节点红,右子节点黑(LR型):左子节点左旋,自身右旋,自身改黑,左右子节点改红
//等价于左子节点左旋后,执行情况三操作
情况五:右子节点、右子节点的右子节点红,左子节点黑(RR型):自身左旋,自身改黑,左右子节点改红
(同情况三相反,不再画图)
情况六:右子节点、右子节点的左子节点红,左子节点黑(RL型):右子节点右旋,自身左旋,自身改黑,左右子节点改红
//等价于右子节点右旋后,执行情况五操作(同情况四相反,不再画图)
删除
//假设红为0,黑为1,双重黑为2
在二叉查找树删除操作的基础上判断待删除节点的度进行相应操作,最后再回溯进行删除调整(目的是去除双重黑):
情况一:待删除节点为红且度为0:直接删除
情况二:待删除节点为红且度为1:删除并把子节点提上来
情况三:待删除节点为黑且度为0:删除并在该位置挂一个nil节点(双重黑节点)
情况四:待删除节点为黑且度为1:删除并把子节点提上来,若子节点为红则改黑,若子节点为黑则为双重黑
//总结来说前四种情况提上来的子节点颜色为+=带删除节点颜色
情况五:待删除节点度为2:同前继节点交换,然后从左子树中删除前继节点
删除调整
红(0)代表红,黑(1)代表黑,灰(2)代表双重黑,每种情况优先度依次降低:
情况一:子节点不存在双重黑:无需调整
情况二:子节点一个双重黑,一个红:先自身变红,若左节点红则右旋,右节点红则左旋,最后自身变黑
情况三:子节点一个双重黑,一个黑,且黑节点无红色子节点:自身颜色+1,子节点颜色-1
情况四:左子节点黑,右子节点双重黑,左子节点的左子节点红(LL型):右子节点变黑,大右旋,左节点颜色=右节点
情况五:左子节点黑,右子节点双重黑,左子节点的右子节点红(LR型):左子节点变红,左子节点左旋,左子节点变黑,右子节点变黑,大右旋,左节点颜色=右节点
情况六:右子节点黑,左子节点双重黑,右子节点的右子节点红(RR型):左子节点变黑,大左旋,右节点颜色=左节点
(同情况四相反,不再画图)
情况七:右子节点黑,左子节点双重黑,右子节点的左子节点红(RL型):右子节点变红,右子节点左旋,右子节点变黑,左子节点变黑,大左旋,右节点颜色=左节点
(同情况五相反,不再画图)
红黑树 VS AVL树
AVL树高度平衡,查找效率非常高,但每次插入删除操作比较耗时
红黑树只是近似平衡,所以维护成本上比 AVL 树低(插入删除操作很多只需改改颜色),对于工厂级应用,更倾向于性能稳定的红黑树
代码
#include <stdio.h>
#include <stdlib.h>
//节点信息
typedef struct Node {
int key, color;
struct Node *lchild, *rchild;
} Node;
//宏定义颜色
#define RED_COLOR 0
#define BLACK_COLOR 1
#define DOUBLE_COLOR 2
//定义NIL节点
Node __NIL;
#define NIL (&__NIL)
//constructor参数让系统执行main()函数之前调用init_NIL()函数
//与此类似的,destructor参数让系统在main()函数退出后调用函数
//init_NIL()用于初始化NIL节点
__attribute__((constructor))
void init_NIL() {
NIL->key = 0;
NIL->color = BLACK_COLOR;
NIL->lchild = NIL->rchild = NIL;
}
//初始化节点
Node *getNewNode(int key) {
Node *p = (Node *)malloc(sizeof(Node));
p->key = key;
p->color = RED_COLOR;
p->lchild = p->rchild = NIL;
return p;
}
//销毁红黑树
void clear(Node *node) {
if(node == NIL) return;
clear(node->lchild);
clear(node->rchild);
free(node);
return;
}
//判断是否有红色子孩子
bool has_red_child(Node *p) {
return (p->lchild->color == RED_COLOR || p->rchild->color == RED_COLOR);
}
//查找节点前驱(左子树中最大值)
Node *predecessor(Node *p) {
Node *temp = p->lchild;
while(temp->rchild != NIL) {
temp = temp->rchild;
}
return temp;
}
//左旋
Node *left_rotate(Node *p) {
Node *temp = p->rchild;
p->rchild = temp->lchild;
temp->lchild = p;
return temp;
}
//右旋
Node *right_rotate(Node *p) {
Node *temp = p->lchild;
p->lchild = temp->rchild;
temp->rchild = p;
return temp;
}
/*******************
以下为插入流程
********************/
//step3:插入调整
Node *insert_maintain(Node *p) {
//情况一:没有红色子孩子:直接返回
if (!has_red_child(p)) return p;
//情况二:左、右子节点红且孙子节点存在红:左右子节点改黑,自身改红
if (p->lchild->color == RED_COLOR && p->rchild->color == RED_COLOR) {
p->color = RED_COLOR;
p->lchild->color = p->rchild->color = BLACK_COLOR;
return p;
}
//若左子节点红且左子节点的儿子节点红
if (has_red_child(p->lchild) && p->lchild->color == RED_COLOR) {
//情况四:左子节点、左子节点的右子节点红,右子节点黑(LR型):左子节点左旋,自身右旋,自身改黑,左右子节点改红
if (p->lchild->rchild->color == RED_COLOR) {
//小左旋
p->lchild = left_rotate(p->lchild);
}
//情况三:左子节点、左子节点的左子节点红,右子节点黑(LL型):自身右旋,自身改黑,左右子节点改红
//再大右旋
p = right_rotate(p);
}
//若右子节点红且右子节点的儿子节点红
else if (has_red_child(p->rchild) && p->rchild->color == RED_COLOR) {
//情况六:右子节点、右子节点的左子节点红,左子节点黑(RL型):右子节点右旋,自身左旋,自身改黑,左右子节点改红
if (p->rchild->lchild->color == RED_COLOR) {
//先小右旋
p->rchild = right_rotate(p->rchild);
}
//情况五:右子节点、右子节点的右子节点红,左子节点黑(RR型):自身左旋,自身改黑,左右子节点改红
//再大左旋
p = left_rotate(p);
} else {
goto insert_end;
}
//最后改色
p->color = BLACK_COLOR;
p->lchild->color = p->rchild->color = RED_COLOR;
insert_end:
return p;
}
//step2:插入操作(在二叉排序树找到合适的位置进行插入,然后回溯进行插入调整)
Node *__insert(Node *p, int key) {
if(p == NIL) return getNewNode(key);
if(p->key == key) {
return p;
} else if(p->key < key) {
p->rchild = __insert(p->rchild, key);
} else {
p->lchild = __insert(p->lchild, key);
}
//返回调整后的p
return insert_maintain(p);
}
//step1:完整插入流程(最后将根节点调整为黑)并返回根节点
Node *insert(Node *root, int key) {
root = __insert(root, key);
root->color = BLACK_COLOR;
return root;
}
/*******************
以下为删除流程
********************/
//step3:删除调整
Node *erase_maintain(Node *p) {
//情况一:子节点不存在双重黑:无需调整
if(p->lchild->color != DOUBLE_COLOR && p->rchild->color != DOUBLE_COLOR) {
return p;
}
//情况二:子节点一个双重黑,一个红:先自身变红,若左节点红则右旋,右节点红则左旋,最后自身变黑
if (has_red_child(p)) {
Node *temp = p;
p->color = RED_COLOR;
if (p->lchild->color == RED_COLOR) {
p = right_rotate(p);
} else {
p = left_rotate(p);
}
p->color = BLACK_COLOR;
if(temp == p->lchild) {
p->lchild = erase_maintain(temp);
} else {
p->rchild = erase_maintain(temp)
}
}
else if (p->lchild->color == BLACK_COLOR) {
//情况三:子节点一个双重黑,一个黑,且黑节点无红色子节点:自身颜色+1,子节点颜色-1
if (!has_red_child(p->lchild)) {
goto erase_end;
}
//情况五:左子节点黑,右子节点双重黑,左子节点的右子节点红(LR型):左子节点变红,左子节点左旋,左子节点变黑,右子节点变黑,大右旋,左节点颜色=右节点
if(p->lchild->lchild->color != RED_COLOR) {
p->lchild->color = RED_COLOR;
p->lchild = left_rotate(p->lchild);
p->lchild->color = BLACK_COLOR;
}
//情况四:左子节点黑,右子节点双重黑,左子节点的左子节点红(LL型):右子节点变黑,大右旋,左节点颜色=右节点
p->rchild->color -= 1;
p = right_rotate(p);
p->lchild->color = p->rchild->color;
}
else {
//情况三:子节点一个双重黑,一个黑,且黑节点无红色子节点:自身颜色+1,子节点颜色-1
if (!has_red_child(p->rchild)) {
goto erase_end;
}
//情况七:右子节点黑,左子节点双重黑,右子节点的左子节点红(RL型):右子节点变红,右子节点左旋,右子节点变黑,左子节点变黑,大左旋,右节点颜色=左节点
if(p->rchild->rchild->color != RED_COLOR) {
p->rchild->color = RED_COLOR;
p->rchild = right_rotate(p->rchild);
p->rchild->color = BLACK_COLOR;
}
//情况六:右子节点黑,左子节点双重黑,右子节点的右子节点红(RR型):左子节点变黑,大左旋,右节点颜色=左节点
p->lchild->color -= 1;
p = left_rotate(p);
p->rchild->color = p->lchild->color;
}
return p;
erase_end:
p->color += 1;
p->lchild->color -= 1;
p->rchild->color -= 1;
return p;
}
//step2:删除操作(在二叉排序树找到合适的位置进行删除,然后回溯进行删除调整)
Node *__erase(Node *p, int key) {
if (p == NIL) return p;
if (key < p->key) {
p->lchild = __erase(p->lchild, key);
} else if (key > p->key) {
p->rchild = __erase(p->rchild, key);
} else {
//待删除节点度为0或1
if (p->lchild == NIL || p->rchild == NIL) {
Node *q = (p->lchild == NIL? p->rchild: p->lchild);
q->color += p->color;
free(p);
return q;
//待删除节点度为2
} else {
//同前继节点交换,然后从左子树中删除前继节点
Node *temp = predecessor(p);
p->key = temp->key;
p->lchild = __erase(p->lchild, temp->key);
}
}
//回溯进行删除调整
return erase_maintain(p);
}
//step1:完整删除流程(最后将根节点调整为黑)并返回根节点
Node *erase(Node *root, int key) {
root = __erase(root, val);
root->color = BLACK_COLOR;
return root;
}
//输出
void output(Node *p) {
if (p == NIL) return;
printf("(%d | %d, %d, %d)\n",
p->color,
p->key,
p->lchild->key,
p->rchild->key
);
output(p->lchild);
output(p->rchild);
return;
}
int main(int argc, char const *argv[]) {
Node *root = NIL;
int op, val;
while (~scanf("%d%d", &op, &val)) {
switch(op) {
case 1: {
root = insert(root, val);
break;
}
case 2: {
root = erase(root, val);
break;
}
}
output(root);
}
return 0;
}