【算法导论】红黑树详解之一(插入)

本文地址:http://blog.csdn.net/cyp331203/article/details/42677833

作者:苦_咖啡

欢迎转载,但转载请注明出处,否则将追究相应责任,谢谢!。

红黑树是建立在二叉查找树的基础之上的,关于二叉查找树可以参看【算法导论】二叉搜索树的插入和删除【算法导论】二叉树的前中后序非递归遍历实现。对于高度为h的二叉查找树而言,它的SEARCH、INSERT、DELETE、MINIMUM、MAXIMUM等操作的时间复杂度均为O(h)。所以在二叉查找树的高度较高时,上述操作会比较费时,而红黑树就可以解决这种问题。红黑树是许多平衡搜索树中的一种,可以保证在最坏的情况下基本动态集合操作的时间复杂度为O(logn),n为节点个数。

一、什么是红黑树

红黑树是一颗二叉查找树,所不同的是,它的每个节点上增加了一个存储位来表示节点的颜色,可以是RED或BLACK。红黑树的每个节点包含5个属性:color、key、left、right、和p(父节点)。如果一个节点没有子节点或父节点,在二叉查找树中,相应的指针就会指向NULL(空),而这里就是红黑树与二叉查找树的第二个不同之处:

红黑树中并没有任何一个节点的左子节点、右子节点或者父节点会指向NULL(空),取而代之的是使用一个nil的哨兵节点来放在原来为NULL的位置,类似于【算法导论】10.2不带哨兵节点和带哨兵节点的双向链表文中带哨兵节点的双向链表。在红黑树中,哨兵节点nil是所有一个与树种普通节点有相同属性的对象。它的color属性为BLACK,而它的其他属性可以任意设置,一般时没有什么意义。nil用来替换二叉查找树中原本为NULL的指针的指向,同时root节点的父指针指向nil。我们将除nil节点的其他节点视为内部节点,在红黑树中,我们主要关注内部节点。

红黑树结构图:

红黑树的五条性质:

红黑树,通过对任何一条从根到叶子的简单路径上各个节点的颜色进行约束,红黑树可以保证没有一条路径会比其他路径长出2倍以上,所以称之为平衡。为了保持平衡,红黑树要满足以下五条性质:

1、每个节点不是红色就是黑色的。

2、根结点是黑色的。

3、每个叶节点(nil)是黑色的,实际上nil只有一个。

4、如果一个节点是红色的,则它的两个子节点都是黑色的。

5、对每个节点,从该节点到其所有后代叶结点的简单路径上,均包含相同数目的黑色节点。

对于上面的第5点,又有一个新的定义,黑高:从某个节点x出发(不含该节点)到达一个叶结点的任意一条简单路径上的黑色节点个数称之为该节点的黑高注意黑高是将nil计算在内的

将上图的黑高标识出来:

红黑树如何保证其性能O(lgn)?

证明:红黑树的高度为O(lgn)

实际上,一颗有n个内部节点的红黑树的高度至多为2lg(n+1)。我们设一个节点x的黑高为hb(x)。

先来证明以任一节点x为根的子树中至少包含2^(hb(x)-1) -1个内部节点,使用归纳法:

①如果x的高度为0,那么x是叶结点(nil),且以x为根结点的子树至少包含2^(1-1) -1=0个内部节点,符合结论。

②假设x的高度为k时(k>=1),以x为根节点的子树至少包含2^(hb(x)-1)-1个内部节点。

③对于x的高度为k+1的红黑树,我们考虑它的两个子树,它的两个子树的高度为k,那么满足②号条件,两个子树至少包含了2^(hb(x)-1)-1个内部节点,所以以x为根的树至少包含了(2^(hb(x)-1)-1)+(2^(hb(x)-1)-1)+1=2^(hb(x))-1个内部节点,因此得证。

对于高度为h的红黑树,根据性质4我们不难发现在每一条路径上,至少有一半以上的黑色节点,否则必定有两个红色节点会相邻。所以树的黑高至少为h/2。所以根据上面已证明的结论,有n>=2(h/2 - 1)-1,所以有:h<=2lg(n+1)。得证。

根据这个证明,我们由二叉搜索树的字典操作的时间复杂度为O(h),而红黑树的高度为O(lgn)可知,红黑树的字典操作的时间复杂度应为O(lgn)。所以红黑树有比二叉搜索树更优的性能。

红黑树的单分支情况

这里需要注意的是,由于红黑树的性质的限制,对于某些情况的单分支是不可能出现的。

1、可能出现的单分支:

因为只有在上面的两种情况下才有可能在单分支的情况下,保持上面的第五条性质(注意nil节点)。而下面的几种情况,都是不能保证第五条性质的单分支情况。

2、不可能出现的单分支情况:

因为上述四中但分支情况下,不能保证性质5。例如前两种,必定会让红色节点->叶子节点(nil)线路的黑色节点数比红色节点->黑色节点线路的黑色节点数少一。所以,我们容易发现,红黑树种只可能有双分支或黑上红下的单分支情况

二、左旋和右旋操作

旋转操作是后序很多红黑树操作必不可少的部分,旋转操作可以保持二叉搜索树性质的搜索树局部操作。

左旋和右旋(都是针对上面的那个节点):

通过上图,我们很容易发现,旋转之后,还是可以保持二叉搜索树的性质。但是却不能保证红黑树的性质,例如右图,如果是21节点是单分支的情况(a节点=nil),就一定不会满足第五条性质。

我们可以比较容易的给出左旋和右旋的代码:

左旋:

/*
	 * 以x为支点进行左旋
	 */
	void left_rotate(node* x) {
		if (x == nil_node) {
			return;
		}

		node* y = x->r_child;

		x->r_child = y->l_child; //让y的左节点接到x的右节点位置,这样能满足二叉搜索树的性质

		if (y->l_child != nil_node) { //这一句判断不能少
			y->l_child->parent = x; //让左节点的父指针指向x
		}

		y->parent = x->parent;

		if (x->parent == nil_node) { //x原本为根结点的情况
			this->root = y;
		} else if (x == x->parent->r_child) {
			y->parent->r_child = y;
		} else {
			y->parent->l_child = y;
		}
		//处理左节点
		y->l_child = x;
		x->parent = y;
	}

右旋:

	/*
	 * 以x为支点进行右旋
	 */
	void right_rotate(node* x) {
		if (x == nil_node) {
			return;
		}

		node* y = x->l_child;

		x->l_child = y->r_child;
		x->l_child->parent = x;

		y->parent = x->parent;

		if (x->parent == nil_node) { //x原本为根结点的情况
			this->root = y;
		} else if (x == x->parent->l_child) {
			y->parent->l_child = y;
		} else {
			y->parent->r_child = y;
		}

		y->r_child = x;
		x->parent = y;
	}

我们容易发现,旋转操作,不论是左旋还是右旋,时间复杂度都是O(1)。

三、红黑树的插入操作

之前有提到过,二叉搜索树的字典操作的时间复杂度为O(h),而红黑树的操作却可以在O(lgn)_内完成。为了做到这一点,我们肯定需要在插入之后进行一些节点的调整,让其满足红黑树的性质。所以在完成插入之后,还需要对树进行调整,对节点重新着色,并旋转。

红黑树的节点插入可以分为插入过程和调整过程;其中插入过程与二叉搜索树差不太多。只是注意搜索树中为NULL的地方,现在被替换成nil哨兵节点,其次插入的节点我们总是将其着色为红色。

插入的代码:

	void insert(int k) {
		node* n = new node(k);
		node* y = nil_node;
		node* x = root;
		while (x != nil_node) { //寻找合适的插入位置,这里主要是满足二叉搜索树的性质
			y = x; //y来记录x的位置
			if (x->key > k) {
				x = x->l_child;
			} else {
				x = x->r_child;
			}
		}

		n->parent = y; //找到了合适的插入位置,y为这个位置的父节点

		if (y == nil_node) { //为空树的情况
			root = n;
		} else if (y->key > n->key) { //左节点
			y->l_child = n;
		} else {
			y->r_child = n; //右节点
		}

		//处理nil
		n->l_child = nil_node;
		n->r_child = nil_node;

		//处理插入节点的颜色
		n->color = RED;

		//调整操作
		rb_insert_fixup(n);
	}

下面是重头戏,我们来看一看,rb_insert_fixup这个调整函数如何实现。

要调整,我们首先得明白要调整什么,即我们之前的插入操作,会破坏红黑树的哪些性质?我们再来看看这5条性质:

1、每个节点不是红色就是黑色的。

2、根结点是黑色的。

3、每个叶节点(nil)是黑色的,实际上nil只有一个。

4、如果一个节点是红色的,则它的两个子节点都是黑色的。

5、对每个节点,从该节点到其所有后代叶结点的简单路径上,均包含相同数目的黑色节点。

其中1、3性质肯定是满足的,而对于5性质,由于我们插入的节点都染红色了,所以不存在多出黑色节点的情况,所以5性质也可以满足。可能出现问题的就是2、4性质,对于2号性质,如果插入的是一颗空树(只有nil节点),那么根结点就是红的,就不满足2性质。如果插入的节点的父节点是红色,而我们插入的也是一个红色的节点,那么就不满足4性质了。

 处理性质2:

对于2号性质,其实很好解决,我们只需要在调整函数最后加上一句root->color=BLACK即可,将根结点染黑。

 处理性质4:

而对于4号性质,出现问题的是父节点为红色的情况,这时待调整节点和父节点都为红,不满足4性质,于是我们需要调整的判断条件就是父节点如果为红,就需要继续调整。这里实际上就要分三种情况,首先都已经默认z的父节点z->p的颜色为红色,循环条件为while(z->color==RED):

情况一、z的叔节点y是红色,其中z为要调整的节点(下同):

‘       由于z的父节点z->p和叔节点都是红色,所以z的祖父节点z->p->p的颜色一定是黑色,这是因为插入之前是一颗“完好”的红黑树。

对于这种情况,处理办法是:将z的父节点和叔节点都染黑,而将z的祖父节点染红,这样做了之后,我们仔细思考一下,发现其实这样对当前树的走父节点或走叔节点的路径的黑节点数没有影响。然后这时我们再将z点指向z的祖父节点位置,然后继续进入循环;

情况二、z的叔节点y是黑色的且z是一个右孩子

这里的做法就是将二情况转换成三情况,当然就要用到旋转,但是我们又不希望z的位置发现变化,所以这里先让z=z->p,然后再以z为支点进行左旋,因为左旋会让z下降一级,所以实际上z还是指向的原来那一层的节点,z->p->p的位置还是没有变。

情况三、z的叔节点y是黑色的且z是一个左孩子

这时实际上是z和z->p都是红色,z->p->p和z.->p->right都是黑色,我们想做的就是让z和z->p之间多个黑节点,这样就能满足性质4,但是同时我们又希望保持性质5。于是我们这样处理:

z->p染黑,z->p->p染红,然后以z->p->p为支点右旋,这样就搞定了,如图:

经过上面分别对性质2和性质4三种情况处理的分析,不难给出实现代码:

	/*
	 * 修复颜色,因为插入的点,我们总是将其先染成红色,所以如果父节点为黑色则不需要修复,如果父节点为红色,则需要修复
	 * 修复颜色分三种情况:
	 * ①当前插入点的父亲为红色,且祖父节点的另一个节点为也为红色,且父节点为祖父节点的左子节点
	 * ②当前插入点的父节点为红色,且祖父节点的另一个节点为黑色,且本节点为父结点的右子节点
	 * ③当前插入点的父节点为红色,且祖父节点的另一个节点为黑色,且本节点为父结点的左子节点
	 */
	void rb_insert_fixup(node* z) {
		while (z->parent->color == RED) {
			if (z->parent == z->parent->parent->l_child) { //如果z的父节点是祖父节点的左子节点
				node* y = z->parent->parent->r_child;
				if (y->color == RED) {									//情况1		处理性质4
					z->parent->color = BLACK;							//情况1
					z->parent->parent->r_child->color = BLACK;			//情况1
					z->parent->parent->color = RED;						//情况1
					z = z->parent->parent;								//情况1
				} else if (z == z->parent->r_child) {					//情况2
					z = z->parent;										//情况2
					left_rotate(z);										//情况2
				} else {												//情况3
					z->parent->color = BLACK;							//情况3
					z->parent->parent->color = RED;						//情况3
					right_rotate(z->parent->parent);					//情况3
				}
			} else { //当父节点是祖父节点的右孩子的情况,与上面差别在于将所有的left操作和节点改成对应的right的
				node* y = z->parent->parent->l_child;
				if (y->color == RED) {
					z->parent->color = BLACK;
					z->parent->parent->l_child->color = BLACK;
					z->parent->parent->color = RED;
					z = z->parent->parent;
				} else if (z->parent == z->parent->parent->l_child) {
					z = z->parent;
					right_rotate(z);
				} else {
					z->parent->color = BLACK;
					z->parent->parent->color = RED;
					left_rotate(z->parent->parent);
				}
			}
		}
		root->color = BLACK;													//处理性质2
	}

四、插入操作的算法分析

通过观察,不难发现,由于有n个节点的红黑树的高度为O(lgn),因此插入过程的时间复杂度为O(lgn);而对于调整过程rb_insert_fixup而言,只有情况1让z上升两层,有可能让while一直发生,但是由于树高的限制,时间复杂度也只可能在O(lgn),而对于情况2和情况3,程序所做的旋转不会超过两次。因为只要执行到情况2或者3,while循环就将要结束了。所以整个节点插入的时间复杂度为O(lgn)。

下一篇将是红黑树的删除节点,敬请关注。

时间: 2024-10-06 01:24:08

【算法导论】红黑树详解之一(插入)的相关文章

算法导论学习---红黑树详解之插入(C语言实现)

前面我们学习二叉搜索树的时候发现在一些情况下其高度不是很均匀,甚至有时候会退化成一条长链,所以我们引用一些"平衡"的二叉搜索树.红黑树就是一种"平衡"的二叉搜索树,它通过在每个结点附加颜色位和路径上的一些约束条件可以保证在最坏的情况下基本动态集合操作的时间复杂度为O(nlgn).下面会总结红黑树的性质,然后分析红黑树的插入操作,并给出一份完整代码. 先给出红黑树的结点定义: #define RED 1 #define BLACK 0 ///红黑树结点定义,与普通的二

算法导论-------------红黑树

红黑树是一种二叉查找树,但在每个结点上增加了一个存储位表示结点的颜色,可以是RED或者BLACK.通过对任何一条从根到叶子的路径上各个着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是接近平衡的.本章主要介绍了红黑树的性质.左右旋转.插入和删除.重点分析了在红黑树中插入和删除元素的过程,分情况进行详细讨论.一棵高度为h的二叉查找树可以实现任何一种基本的动态集合操作,如SEARCH.PREDECESSOR.SUCCESSOR.MIMMUM.MAXMUM.INSERT.DELETE等

算法导论 红黑树 学习 删除(四)

版权声明:本文为博主原创文章,未经博主允许不得转载.技术博客 http://blog.csdn.net/stecdeng 技术交流群 群号码:324164944 欢迎c c++ windows驱动爱好者 服务器程序员沟通交流 学习算法 还是建议看看算法导论 算法导论第三版 如果不看数学推导 仅看伪代码 难度还是适中 本系列只是记录我的学习心得 和伪代码转化代码的过程 深入学习 还是建议大家看看算法书籍 教程更加系统. 本文参考算法导论第13章节 红黑树 代码由本人写成 转载请标明出处 先看看不做

数据结构-红黑树详解

介绍: 红黑树(Red Black Tree) 是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组. 它是在1972年由Rudolf Bayer发明的,当时被称为平衡二叉B树(symmetric binary B-trees).后来,在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的"红黑树". 红黑树和AVL树类似,都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能. 它虽然

红黑树详解

前言 红黑树的起源,自然是二叉查找树了,这种树结构从根节点开始,左子节点小于它,右子节点大于它.每个节点都符合这个特性,所以易于查找,是一种很好的数据结构.但是它有一个问题,就是容易偏向某一侧,这样就像一个链表结构了,失去了树结构的优点,查找时间会变坏. 所以我们都希望树结构都是矮矮胖胖的,像这样: 而不是像这样: 在这种需求下,平衡树(AVL)的概念就应运而生了. 红黑树就是一种平衡树,它可以保证二叉树基本符合矮矮胖胖的结构,但是理解红黑树之前,必须先了解另一种树,叫2-3树,红黑树背后的逻辑

算法导论 红黑树 学习 旋转(二)

学习算法 还是建议看看算法导论 算法导论第三版 如果不看数学推导 仅看伪代码 难度还是适中 本系列只是记录我的学习心得 和伪代码转化代码的过程 深入学习 还是建议大家看看算法书籍 教程更加系统. 本文参考算法导论第13章节 红黑树 代码由本人写成 转载请标明出处 红黑树是一个带颜色的二叉树 有以下5点性能 1 每个节点或者红色或者黑色 2 根节点黑色 3 每个叶子节点(nil)为黑色 4 如果一个节点是红色的则它的两个子节点都是黑色 5 每个节点 该节点到子孙节点的路径上 黑色节点数目相同 如图

[[算法导论]红黑树速记

红黑树的性质: 1.每个结点要么是红色要么是黑色的. 2.根结点是黑色的. 3.所有叶结点(nil)是黑色的. 4.每个红色结点的两个孩子都是黑色的. 5.每个结点到其后代叶结点的简单路径上均包含相同数目的黑色结点. INSERT操作按二叉搜索树的方法插入新结点. INSERT-FIXUP(三种情况): 插入后新结点(z)为红色,当z.p==z.p.p.left时. 循环条件:父结点为红色. 情况1:叔结点(z.p.p.right)为红色. 父结点和叔结点改为黑色,祖父结点改为红色并成为新的z结

红黑树详解 原理 史上最强 精华

未经授权,不得私自转载,否则追究法律责任 联系作者[email protected]取得授权 转载请注明作者和出处 网上很多红黑树的讲解都没有分析清楚插入删除各种情况是怎么来的,他们大多把分析图画的很复杂,其实这些情况都是极其简单的,我这里重点推导各种情况是怎么来的,不解释各种情况怎么调整,因为结构很简单调整很容易,且网上很多.红黑树的精髓是明白各种情况是怎么来的,弄清楚这个才是真正掌握红黑树. 五个性质: 1.节点要么红,要么黑 2.根节点为黑 3.NIL(叶节点)为黑,这条没什么卵用 4.红

算法导论—红黑树

华电北风吹 天津大学认知计算与应用重点实验室 日期:2015/9/9 红黑树是对二叉树的一种平衡扩展.红黑树采用开放的数据扩张策略,并且对于诸如插入.查询,删除有Θ(lg n)的时间复杂度,因此也是一种应用比较广泛的数据结构. 一.红黑树的节点 节点属性:关键字key,节点颜色,左孩子指针,右孩子指针,父节点指针,卫星数据. 虚拟节点-NIL:对于红黑树中所有节点如果没有父节点或者缺少某个子节点,则把对应的指针指向同一个NIL. 二.红黑树的性质 1.每个节点的颜色是红色或黑色. 2.根节点是黑