数据结构与算法--轻松玩转红黑树

二叉树

​ 二叉树每个节点最多有两个子节点。

特殊二叉树有:满二叉树(除叶子节点外每个节点都有两个子节点)、完全二叉树(除最下层外其他层节点全满,且最下层叶子节点靠左排列)

​ 二叉树的存储方式有两种:一种是基于链式存储(通过指针),另一种是基于数组(左子节点为2i,右子节点为2i+1,父节点为i/2,根节点为1)

​ 二叉树的遍历方式有四种:前序遍历(中左右)、中序遍历(左中右)、后续遍历(左右中)、按层遍历(BFS),时间复杂度都为O(n)

二叉查找树(二叉排序树)

​ 树中任意节点,其左子树节点值都小于该节点,而右子树节点值都大于该节点。它的操作:

​ 查:从根节点开始,小于递归左子树,大于递归右子树直到找到为止

​ 增:从根节点开始,若小于且无左子树直接添加,否则递归左子树;大于且无右子树直接添加,否则递归右子树

​ 删:若待删除节点无子节点,直接删除;若待删除节点只有一个子节点,只需让父节点直接指向子节点;若待删除节点有两个子节点,需要找到右子树的最小节点,把它替换到待删除节点位置后删除。优化方法:标记要删除的节点但不真正执行删除操作(省时费内存)

支持重复数据

​ 方法一:通过链表或动态数组,把值相同的数据存储在同一个节点上

​ 方法二:把新插入的数据当作大于这个节点的值来处理(查询时找到值后递归右子树直到叶子节点,删除也同理)

在这里插入图片描述

时间复杂度

​ 增删改查都为O(logn),但是在特殊情况下(树退化为链)时复杂度为O(n)

红黑树

​ 为避免二叉树在频繁更新中出现树高远大于logn而导致效率降低的情况,发明了平衡二叉树

​ 平衡二叉树种类很多,如AVL树(任何节点的左右子树高度相差不超过1),SB树(以任意节点作为树的数量不能少于任意一个侄儿节点),红黑树

​ 关于红黑树的要求:

  1. 根节点为黑,叶子节点为黑且空(NIL节点)
  2. 若节点为红,那它两个子节点、父节点必为黑
  3. 从根出发到所有叶路径上黑色节点数量相同
  4. 新节点初始为红(可调整)

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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值