算法导论 第13章 红黑树

二叉查找树的基本操作包括搜索、插入、删除、取最大和最小值等都能够在O(h)时间复杂度内实现,因此能在期望时间O(lgn)下实现,但是二叉查找树的平衡性在这些操作中并没有得到维护,因此其高度可能会变得很高,当其高度较高时,而二叉查找树的性能就未必比链表好了,所以二叉查找树的集合操作是期望时间O(lgn),最坏情况下为O(n)。

红黑树也是一种二叉查找树,它拥有二叉查找树的性质,同时红黑树还有其它一些特殊性质,这使得红黑树的动态集合基本操作在最坏情况下也为O(lgn),红黑树通过给节点增加颜色和其它限制条件,使得红黑树中的某一节点到叶节点的任一路径不会比其它路径长两倍,因此可以保持树的平衡性。红黑树中节点包括五个域:颜色、关键字、左节点、右节点和父节点,并将没有子节点或父节点的指针指向一个 nil 哨兵节点,这个nil节点称着叶节点,也是外节点,其它包含关键字的节点叫着内节点。之所以要增加这样一个为空的外节点,是为了方便红黑树上的一些边界操作,特别是在删除节点的时候,这个nil节点就很有用了。

红黑树的五个性质:

1、每个节点或者是红的,或者是黑的。

2、根节点是黑的。

3、叶节点(nil节点)是黑的。

4、如果一个节点是红的,则其孩子节点都是黑的。

5、对于任意节点,从该节点到其子孙节点的所有路径上包含相同数目的黑节点。

从某个节点出发到达叶节点的任意一条路径上,黑节点的个数称为该节点的黑高度bh(x)。红黑树的黑高度就定义为根节点的黑高度。红黑树在执行一些搜索,取最大最小值,取前驱后继节点等操作没有修改红黑树,因此跟二叉查找树上的操作相同,但是对于插入和删除操作,红黑树由于被修改了,必须执行一些额外的操作来维护其红黑树的性质,实际上这些额外的操作也不会改变插入和删除操作的时间复杂度,仍然为O(lgn)。

为了维护红黑树的性质,需要执行一些红黑树特有的操作:旋转,包括左旋和右旋。

左旋:对节点x做左旋,必须保证它的右孩子y 不为nil节点,旋转以x到y之间的支轴进行,让y取代x的位置成为该子树新的根,而x成为y的左孩子,y原来的左孩子就成为x的右孩子。时间复杂度为O(1)。

右旋:与左旋相反,对节点x做右旋,必须保证它的左孩子y不为nil节点,旋转以x到y的支轴进行,让y取代x的位置成为该子树新的根,而x成为y的右孩子,y原来的右孩子就成为x的左孩子。时间复杂度为O(1)。

在对红黑树执行插入和删除之后,需要执行额外的维护操作,这些操作就是建立在旋转之上的。下面分析一下在红黑树中插入和删除节点之后可能出现的问题,及解决思路:

1、插入节点

向红黑树中插入一个新的节点,为了不增加黑节点的个数(从而影响黑高度),我们将新插入的节点涂为红色,这样插入之后它可能会违反性质2和4,在未付性质2和4的同时还得注意不能违反性质5,主要考虑这三个性质即可。因此维护的主要任务就是通过修改红黑树中节点的结构和颜色让根节点为黑色,同时让插入的红节点的父节点变成黑色(如果不是黑色)。

2、删除节点

从红黑树中删除一个节点之后,如果这个节点是红的,则红黑性质没有被破坏,因为被删除的是红节点,所以不是根节点,性质2没问题,同时黑节点没有变,所以性质5没问题,因为被删除节点的父节点是黑,所以无论替补上去的节点是什么颜色都不会破坏性质4,性质1和3没有被影响,所以所有性质都没有破坏。如果被删除的节点是黑色的,则节点的黑高度变化了,性质5被破坏了,维护的主要任务就是通过修改节点的结构和颜色从而增加该子树一个黑高度。

在插入或者删除节点之后,维护红黑树的性质的时候,都是只有一种情形可以直接通过变换来保持红黑树的性质,其它情形就必须通过变换转化到这一种可以直接解决的出口情形。至于具体的解决过程请见下面的代码注释。

/*
 *	算法导论 第十三章 红黑树
 */

#include <iostream>
using namespace std;

//定义颜色
enum Color
{
	BLACK = 0,
	RED = 1
};
//定义红黑树节点
typedef struct RBTNode
{
	Color color;
	int key;
	RBTNode *left, *right, *parent;

	RBTNode()
	{
		this->color = Color::RED;
		this->key = 0;
		this->left = this->right = this->parent = NULL;
	}

	RBTNode(Color c, int k, RBTNode* l, RBTNode* r, RBTNode* p)
	{
		this->color = c;
		this->key = k;
		this->left = l;
		this->parent = p;
		this->right = r;
	}

}RBTNode, RBTree;

//定义一个哨兵nilNode,以方便处理边界问题
//特别是在删除红黑树元素时很有用
// 如果删除节点没有孩子,nilNode作为其孩子处理起来就方便多了
RBTNode* nilNode = NULL;

/*
 *	中序遍历
 *	递归
 */
void inOrderTreeWalkRecursion(RBTree* tree)
{
	if (tree && tree != nilNode)
	{
		inOrderTreeWalkRecursion(tree->left);
		cout<<tree->key<<" ";
		inOrderTreeWalkRecursion(tree->right);
	}
}

/*
 *	查找二叉排序树中的最小元素
 *	即为最左元素
 *	时间复杂度为O(lgn)
 */
RBTNode* rbTreeMinimum(RBTree* tree)
{
	if (! tree || tree == nilNode)
		return NULL;

	while (tree->left != nilNode)
	{
		tree = tree->left;
	}

	return tree;
}

/*
 *	求二叉排序树中指定节点node的中序遍历后继
 *	如果node右子树不为空,则后继则为node右子树中的最小节点
 *	否则node右子树为空,必须向上回溯找到第一个节点:该节点为其父节点的左孩子
 *	后继即为其父节点,如果不存在这样的节点,说明node为最右节点,后继为空
 *	时间复杂度为O(lgn)
 */
RBTNode* rbTreeSuccessor(RBTNode* node)
{
	if (! node || node == nilNode)
		return NULL;

	if (node->right != nilNode)
	{
		return rbTreeMinimum(node->right);
	}

	RBTNode* temp = node->parent;
	while (temp != nilNode && node == temp->right)
	{
		node = temp;
		temp = node->parent;
	}
	return temp;
}

/*
 *	红黑树节点左旋
 */
void leftRotate(RBTree* &tree, RBTNode* node)
{
	//如果node为空或者其右节点为空,就返回
	if (! node || node == nilNode || node->right == nilNode)
		return;
	//让node右节点的左孩子成为node的右孩子
	RBTNode* temp = node->right;
	node->right = temp->left;
	if (temp->left != nilNode)
	{
		temp->left->parent = node;
	}
	//让node的右孩子取代node的位置
	temp->parent = node->parent;
	if (node->parent == nilNode)
	{//说明node是根节点,修改根节点的指针
		tree = temp;
	} else {
		if (node == node->parent->left)
		{
			node->parent->left = temp;
		} else {
			node->parent->right = temp;
		}
	}
	//让node成为其右孩子的左孩子
	temp->left = node;
	node->parent = temp;
}

/*
 *	红黑树节点右旋
 */
void rightRotate(RBTree* &tree, RBTNode* node)
{
	//如果node为空或者其左节点为空,就返回
	if (! node || node == nilNode || node->left == nilNode)
		return;
	//让node左节点的右孩子成为node的左孩子
	RBTNode* temp = node->left;
	node->left = temp->right;
	if (temp->right != nilNode)
	{
		temp->right->parent = node;
	}
	//让node的右左孩子取代node的位置
	temp->parent = node->parent;
	if (node->parent == nilNode)
	{//说明node是根节点,修改根节点的指针
		tree = temp;
	} else {
		if (node == node->parent->left)
		{
			node->parent->left = temp;
		} else {
			node->parent->right = temp;
		}
	}
	//让node成为其左孩子的右孩子
	temp->right = node;
	node->parent = temp;
}

/*
 *	红黑树插入元素后,对树进行调整以保持红黑树的性质
 *	主要耗时在将违规的位置上升,因为高度为O(lgn),所以其时间复杂度依然为O(lgn)
 *	而旋转一共不超过2次,每次时间复杂度为O(1)
 *	所以总的时间复杂度为O(lgn)
 */
void rbInsertFixup(RBTree* &tree, RBTNode* node)
{
	if (! tree || ! node)
		return;

	//	因为新插入的node颜色为红,只可能违反性质2(根节点为黑色)和性质4(红节点的孩子节点只能为黑)
	//	所以如果node的父节点不是nilNode,则只有在其父节点也为红的时候才违反红黑树的性质,需要调整
	//	否则不用调整
	while (node->parent->color == Color::RED)
	{	//主要考虑node的叔叔节点
		RBTNode* uncleNode = NULL;
		if(node->parent == node->parent->parent->left)
		{
			uncleNode = node->parent->parent->right;

			if (uncleNode->color == Color::RED)
			{
				// 情形1:node的叔叔节点为红,则将违规位置上移
				// 因为node的父节点和叔叔节点均为红,则node的祖父节点必然为黑
				// 因此可以将node的父节点和叔叔节点均变成黑,而祖父节点变成红
				// 这样依然保持了整体的黑高度没变,保持了性质5(任意节点到其子孙节点的路径上黑节点个数相同)
				node->parent->color = Color::BLACK;
				uncleNode->color = Color::BLACK;
				node->parent->parent->color = Color::RED;
				node = node->parent->parent;
			} else {
				//	情形2:node的叔叔节点为黑

				// 情形2.1:node为其父节点的右孩子,将情形2.1转化成2.2以便处理,因为node和其父节点都是红的,所以演父节点旋转没有<span style="white-space:pre">				</span>   影响
				if (node == node->parent->right)
				{
					node = node->parent;
					leftRotate(tree, node);//从2.1变成了2.2
				}

				//	情形2.2:node为其父节点的左孩子,这种情形为出口情形
				//	因为父节点为红,叔叔节点为黑,则祖父节点一定为黑,所以将父节点变为黑
				//	祖父节点变为红,再沿祖父节点右旋则黑高度没变,同时node的父节点变黑了
				//	达到主要目的:让node的父节点变成黑色,以保持性质4
				// 在这个过程中又不能改变黑高度,因为要保持性质5
				node->parent->color = Color::BLACK;
				node->parent->parent->color = Color::RED;
				rightRotate(tree, node->parent->parent);
			}

		} else {
			//叔叔节点的位置相反,处理方法同上,只是旋转时候的方向相反
			uncleNode = node->parent->parent->left;

			if (uncleNode->color == Color::RED)
			{
				// 情形1:node的叔叔节点为红,则将违规位置上移
				node->parent->color = Color::BLACK;
				uncleNode->color = Color::BLACK;
				node->parent->parent->color = Color::RED;
				node = node->parent->parent;
			} else {
				//	情形2:node的叔叔节点为黑

				// 情形2.1:node为其父节点的左孩子
				if (node == node->parent->left)
				{
					node = node->parent;
					rightRotate(tree, node);//从2.1变成了2.2
				}

				//	情形2.2:node为其父节点的右孩子,这种情形为出口情形
				node->parent->color = Color::BLACK;
				node->parent->parent->color = Color::RED;
				leftRotate(tree, node->parent->parent);
			}
		}

	}

	//保持性质2,让根节点为黑
	tree->color = Color::BLACK;
}

/*
 *	插入元素
 *	搜索插入点时间为O(lgn)
 *	维护红黑树的性质时间为O(lgn)
 *	所以总的时间复杂度为O(lgn)
 */
void rbInsert(RBTree* &tree, RBTNode* node)
{
	if (! tree || ! node)
		return;

	RBTNode* posNode = nilNode;
	RBTNode* t = tree;
	while (t != nilNode)
	{
		posNode = t;
		if (node->key < t->key)
		{
			t = t->left;
		} else {
			t = t->right;
		}
	}

	node->parent = posNode;

	if (posNode == nilNode)
	{
		tree = node;
	} else {
		if (node->key < posNode->key)
		{
			posNode->left = node;
		} else {
			posNode->right = node;
		}
	}
	//不同于二叉排序树的地方
	node->left = node->right = nilNode;
	node->color = Color::RED;//插入红节点以保证黑高度不变
	//维护红黑树性质
	rbInsertFixup(tree, node);
}

/*
 *	删除红黑树节点后维护红黑树的性质
 *	主要耗时仍然是将违规位置上升,时间复杂度为O(lgn)
 *	旋转最多3次,共O(1)
 *	所以时间复杂度为O(lgn)
 */
void rbDeleteFixup(RBTree* &tree, RBTNode* node)
{
	//如果node是根节点,则整个树的黑高度都减一,没有影响
	//如果node的颜色是红色,则直接涂黑就可以了
	// 否则就要维护了,红黑树节点删除之后,维护性质主要与node的兄弟节点有关
	while (node != tree && node->color == Color::BLACK)
	{
		//当前节点node为非根黑节点,考虑其兄弟节点
		RBTNode* brotherNode = NULL;
		if (node == node->parent->left)
		{
			brotherNode = node->parent->right;
			//情形1:兄弟节点为红,将其转换成兄弟节点为黑的情形2
			//应为node节点为黑,兄弟节点为红,则其父节点必然为黑
			// 且兄弟节点的子节点必然为黑,因此可以将父节点变红,兄弟节点变黑
			// 然后沿父节点左旋,则黑高度没有被影响,同时node的新的兄弟节点变成了原兄弟节点的左黑孩子了
			// 因此node的兄弟节点变为黑节点了
			if (brotherNode->color == Color::RED)
			{
				node->parent->color = Color::RED;
				brotherNode->color = Color::BLACK;
				leftRotate(tree, node->parent);
				brotherNode = node->parent->right;
			}

			//情形2:兄弟节点为黑

			if (brotherNode->right->color == Color::BLACK && brotherNode->left->color == Color::BLACK)
			{
				//情形2.1:兄弟节点的右孩子为黑,且其左孩子为黑
				//这时node为黑,其兄弟节点和两个孩子节点也均为黑
				//因为此时node子树比其兄弟节点的子树黑高度小1,所以可以把兄弟节点变红,从而二者黑高度相同
				// 从而让node的父节点子树整个黑高度降低1(违反规则5),让node的父节点变成新的node,使违规的位置上升
				// 继续循环
				brotherNode->color = Color::RED;
				node = node->parent;

			} else {
				if (brotherNode->right->color == Color::BLACK) {
					//情形2.2:兄弟节点的右孩子为黑,且其左孩子为红,将这种情形转换成2.3
					//因为兄弟节点为黑,其左孩子为红,右孩子为黑,因此可以将兄弟节点的左孩子变黑
					//兄弟节点变红,然后沿兄弟节点右旋,从而进入情形2.3,且为改变黑高度
					brotherNode->left->color = Color::BLACK;
					brotherNode->color = Color::RED;
					rightRotate(tree, brotherNode);
					brotherNode = node->parent->right;
				}

				//情形2.3:兄弟节点的右孩子为红,左孩子任意,这时出口情形,经过以下变换,算法结束
				// 因为兄弟节点为黑,但其右孩子为红,可以把兄弟节点变成父节点的颜色,把其右孩子和node父节点变黑
				// 然后沿node父节点左旋,则node的兄弟节点的位置仍然是黑色(为brother的右孩子),该子树的黑高度不变
				// 而node节点所在子树因为增加了node父节点这个黑节点而黑高度增加1,所以两边的黑高度相同了
				// (原来node子树比其兄弟子树黑高度少1),从而保持了性质5
				brotherNode->color = node->parent->color;
				node->parent->color = Color::BLACK;
				brotherNode->right->color = Color::BLACK;
				leftRotate(tree, node->parent);

				//问题已解决,让node指向根节点,从而退出循环
				node = tree;
			}

		} else {//兄弟节点的位置相反,原理同上
			brotherNode = node->parent->left;
			//情形1:兄弟节点为红,将其转换成兄弟节点为黑的情形2
			if (brotherNode->color == Color::RED)
			{
				node->parent->color = Color::RED;
				brotherNode->color = Color::BLACK;
				rightRotate(tree, node->parent);
				brotherNode = node->parent->left;
			}

			//情形2:兄弟节点为黑

			if (brotherNode->left->color == Color::BLACK && brotherNode->right->color == Color::BLACK)
			{
				//情形2.1:兄弟节点的左孩子为黑,且其右孩子为黑
				brotherNode->color = Color::RED;
				node = node->parent;
			} else {
				if (brotherNode->left->color == Color::BLACK) {
					//情形2.2:兄弟节点的左孩子为黑,且其右孩子为红,将这种情形转换成2.3
					brotherNode->right->color = Color::BLACK;
					brotherNode->color = Color::RED;
					leftRotate(tree, brotherNode);
					brotherNode = node->parent->left;
				}

				//情形2.3:兄弟节点的左孩子为红,右孩子任意,这时出口情形,经过以下变换,算法结束
				brotherNode->color = node->parent->color;
				node->parent->color = Color::BLACK;
				brotherNode->left->color = Color::BLACK;
				rightRotate(tree, node->parent);

				//问题已解决,让node指向根节点,从而退出循环
				node = tree;
			}
		}
	}

	node->color = Color::BLACK;
}

/*
 *	删除红黑树节点
 *	主要耗时是在获取删除节点的中序遍历后继节点,时间复杂度为O(lgn)
 *	而维护红黑树性质的时间复杂度为O(lgn)
 *	所以总的时间复杂度为O(lgn)
 */
RBTNode* rbDelete(RBTree* &tree, RBTNode* node)
{
	if (! tree || ! node)
		return;

	RBTNode* delNode = NULL;
	if (node->left == nilNode || node->right == nilNode)
	{
		delNode = node;
	} else {
		delNode = rbTreeSuccessor(node);
	}

	RBTNode* fillNode = NULL;
	if (delNode->left != nilNode)
	{
		fillNode = delNode->left;
	} else {
		fillNode = delNode->right;
	}

	fillNode->parent = delNode->parent;

	if (delNode->parent == nilNode)
	{
		tree = fillNode;
	} else {
		if (fillNode == fillNode->parent->left)
		{
			fillNode->parent->left = fillNode;
		} else {
			fillNode->parent->right = fillNode;
		}
	}

	if (delNode != node)
	{
		node->key = delNode->key;
	}

	//如果被删除节点是黑色,则树中某些的黑高度被减一,必须维护性质5
	//将该节点相关路径上增加一个黑节点
	if (delNode->color == Color::BLACK)
	{
		rbDeleteFixup(tree, fillNode);
	}

	return delNode;
}
时间: 2024-10-12 17:48:20

算法导论 第13章 红黑树的相关文章

算法导论第十三章 红黑树

写在前面:这一章真的把我害惨了,之前至少尝试看过3遍,每次看之前都下定决定一定要把它拿下,可是由于内容较多,深度够深,以致于每次要不是中途有什么事放弃了就跳过了,要不是花时间太多仍然不能理解而放弃.这次总算挺过来了,前后零零散散的时间加起来差不多也有两天时间.这次能坚持下来并攻克,我想大概有这么几个原因吧:第一是之前下定的决心要写一个最新版<算法导论>的读书笔记,之前几章都坚持写了,不能让这个成为拦路虎,即使再难再花时间都要弄懂:第二是通过前面几章的动手实践,发现自己的理解能力.动手能力都进步

算法导论 第十三章 红黑树(python)-1插入

红黑树是上一章二叉搜索树的改进,实现一种平衡 ,保证不会出现二叉树变链表的情况,基本动态集合操作的时间复杂度为O(lgn) 实际用途:c++stl中的set,map是用他实现的 红黑树的性质: 1.每个结点或是红色的,或是黑色的 2.跟结点是黑色的 3.每个叶结点(NIL)是黑色 4.如果一个结点是红色的,则它的两个结点都是黑色的 5.对每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同的数目的黑色结点(数目被称为黑高bh(x)) 如下图: (T.nil 哨兵后面被忽略 None) 红

【算法导论】学习笔记——第13章 红黑树

红黑树(red-black tree)是许多平衡搜索树中的一种,因此基本操作(查询.删除.搜索)等在最坏情况下的时间复杂度均为O(lgn).13. 1 红黑树的性质红黑树时一棵二叉搜索树,并且在每个结点上增加了一个属性表示颜色:红色或黑色.通过对任何一条从根到叶子的简单路径上各个结点的颜色进行约束,红黑树确保没有一条路径会比其它路径长出2倍.因而是近似于平衡的.一棵红黑树是满足下面红黑性质的二叉搜索树:1. 每个结点是红色或黑色:2. 根结点是黑色的:3. 每个叶结点(NIL)是黑色的:4. 如

算法导论学习笔记——第13章 红黑树

红黑树 红黑树是一种二叉查找树,但在每个结点上增加一个存储位存储结点的颜色,可以是red或black.通过对任意一条从根到叶的路径上结点颜色的限制,红黑树确保没有任何一条路径比其他路径长出两倍,因而是接近平衡的. 每个结点包含5个域,color,key,left,right,p 满足以下红黑性质: 1.每个结点是红色或黑色 2.根结点是黑色 3.每个叶结点(nil)是黑色 4.如果一个结点是红色,那么它的两个子结点都是黑色 5.对每个结点,从该结点到它每个子孙结点的路径上,黑结点数目相同 左旋转

第13章 红黑树

#include<iostream> #include<stdio.h> #include<stdlib.h> #include<string.h> #include<queue> #define RED 1 #define BLACK 0 using namespace std; struct node{ int color; int key; node* left; node* right; node* parent; }; //左旋 voi

算法导论——lec 13 贪心算法与图上算法

之前我们介绍了用动态规划的方法来解决一些最优化的问题.但对于有些最优化问题来说,用动态规划就是"高射炮打蚊子",采用一些更加简单有效的方法就可以解决.贪心算法就是其中之一.贪心算法是使所做的选择看起来是当前最佳的,期望通过所做的局部最优选择来产生一个全局最优解. 一. 活动选择问题 [问题]对几个互相竞争的活动进行调度:活动集合S = {a1, a2, ..., an},它们都要求以独占的方式使用某一公共资源(如教室),每个活动ai有一个开始时间si和结束时间fi ,且0 ≤ si &

编程算法 - 最小的k个数 红黑树 代码(C++)

最小的k个数 红黑树 代码(C++) 本文地址: http://blog.csdn.net/caroline_wendy 题目: 输入n个整数, 找出其中的最小k个数. 使用红黑树(multiset), 每次替换最大的值, 依次迭代. 时间复杂度: O(nlogk). 代码: /* * main.cpp * * Created on: 2014年6月29日 * Author: wang */ #include <iostream> #include <vector> #includ

算法导论 第6章 堆排序

堆数据结构实际上是一种数组对象,是以数组的形式存储的,但是它可以被视为一颗完全二叉树,因此又叫二叉堆.堆分为以下两种类型: 大顶堆:父结点的值不小于其子结点的值,堆顶元素最大 小顶堆:父结点的值不大于其子结点的值,堆顶元素最小 堆排序的时间复杂度跟合并排序一样,都是O(nlgn),但是合并排序不是原地排序(原地排序:在排序过程中,只有常数个元素是保存在数组以外的空间),合并排序的所有元素都被拷贝到另外的数组空间中去,而堆排序是一个原地排序算法. 1.在堆排序中,我们通常使用大顶堆来实现,由于堆在

算法导论 第6章 堆排序(简单选择排序、堆排序)

堆数据结构实际上是一种数组对象,是以数组的形式存储的,可是它能够被视为一颗全然二叉树,因此又叫二叉堆.堆分为下面两种类型: 大顶堆:父结点的值不小于其子结点的值,堆顶元素最大 小顶堆:父结点的值不大于其子结点的值,堆顶元素最小 堆排序的时间复杂度跟合并排序一样,都是O(nlgn),可是合并排序不是原地排序(原地排序:在排序过程中,仅仅有常数个元素是保存在数组以外的空间),合并排序的全部元素都被复制到另外的数组空间中去,而堆排序是一个原地排序算法. 1.在堆排序中,我们通常使用大顶堆来实现,因为堆