C++高级数据结构算法#红黑树的基本操作及实现

红黑树的定义

定义 : 是一种自平衡的二叉搜索树 , 也就是特殊的二叉树 .
注 : 它不是AVL树

问题 : 红黑树一个节点的左右子树高度差最多允许差多少?
答 : 长的不能超过短的的两倍

在这里插入图片描述

红黑树的性质

  • 1.每个节点或是红的 , 或是黑的
  • 2.每个叶节点(NIL , 实际说的是它的左右孩子节点)都是空节点 , 并且颜色是黑色
  • 3.如果一个节点的颜色是红色 , 则它的两个子女都是黑色的
  • 4.从某一节点到达其子孙叶节点的每一条简单路径上包含相同个数的黑色节点
  • 5.根节点永远是黑色的

红黑树的节点组成

每个节点都包含5个域 : color , key , left , right 和 parent

  • color : 只是该节点的颜色
  • key : 存储该节点的值
  • left : 指示该节点的左子节点
  • right : 指示该节点的右子节点
  • parent : 只是该节点的父节点
    代码实现 :
enum Color
{BLACK , RED};

template<typename T>
class RBTree
{
public:
	RBTree() :_root(nullptr) {}
private:
	struct RBNode
	{
		RBNode(T data = T(), 
			Color color = BLACK,
			RBNode *parent = nullptr)
			:_data(data)  //值
			, _left(nullptr)  //左孩子
			, _right(nullptr) //右孩子
			, _parent(parent) //父节点
			, _color(color)   //颜色
		{}
		T _data;
		RBNode *_left;
		RBNode *_right;
		RBNode *_parent;
		Color _color;
	};

	RBNode *_root; // 指向红黑树的根节点

	//获取节点的颜色
	Color color(RBNode *node)
	{
		return node == nullptr ? BLACK : node->_color;
	}

	//设置节点的颜色
	void setColor(RBNode *node, Color color)
	{
		node->_color = color;
	}

	//获取左孩子节点
	RBNode* left(RBNode *node)
	{
		return node->_left;
	}

	//获取右孩子节点
	RBNode* right(RBNode *node)
	{
		return node->_right;
	}

	//获取父节点
	RBNode* parent(RBNode *node)
	{
		return node->_parent;
	}
};

       红黑树中的一个重要的概念 : 黑高度 , 将红黑树中从某个节点x 出发(不包括该节点)到达一个叶节点的任意一条路径上的黑节点的个数称为该节点的黑高度 , 用bh(x)表示.

红黑树的旋转操作

       当在红黑树上进行插入和删除的操作的时候 ,树的红黑性质将被破坏 为了保持这些性质 , 就要改变树中某些节点的颜色以及指针的结构 . 指针的结构是通过旋转来完成的.
        旋转是一种能够保持树中节点的中缀次序的局部操作
旋转(左旋和右旋)的时间复杂度都为O(1)

左旋操作

在这里插入图片描述

旋转后需要修改的6处地方
在这里插入图片描述

	//左旋 , 6处
	/*
	基本旋转过程与AVL树的相似
	但是在红黑树中 , 加入了指向父节点的指针这一成员
	所以在旋转之后要去修改相关节点的父节点的指向
	而要修改的指向有6处 : 曾祖父的孩子  , 祖父的父亲 , 
	祖父的右孩子 , 父亲的父亲 , 父亲的左孩子 , 父亲左孩子(左孙子)的父亲
	*/
	void leftRotate(RBNode* node)
	{
		//父节点在旋转之后会成为新的根节点(局部)
		RBNode* child = node->_right;
		//旋转后 , 父节点的父亲变为  原祖父节点的父亲(即父亲的父亲)
		child->_parent = node->_parent;
		//如果祖父节点是根节点 , 旋转后将根节点置为原父节点
		if (node->_parent == nullptr)
		{
			_root = child;
		}
		else
		{
			//祖父节点是曾祖父节点的左孩子 , 旋转后曾祖父节点的左孩子是父节点
			//(即曾祖父的孩子)
			if (node->_parent->_left == node)
			{
				node->_parent->_left = child;
			}
			else
			{
				//否则旋转后曾祖父节点的右孩子是父节点
				node->_parent->_right = child;
			}
		}
		//旋转后 , 祖父节点的右孩子是父节点的左孩子(即祖父的右孩子)
		node->_right = child->_left;
		if (child->_left != nullptr)
		{
			//旋转后 , 父节点的左孩子的父节点变为祖父节点(即左孙子的父亲)
			child->_left->_parent = node;
		}

		//旋转后 , 父节点的左孩子是祖父节点(即父亲的左孩子)
		child->_left = node;
		//旋转后祖父节点的父亲变为 , 原父节点(即祖父的父亲)
		node->_parent = child;
	}
右旋操作

在这里插入图片描述

旋转后需要修改的6处地方
在这里插入图片描述

	//右旋
	/*
	基本旋转过程与AVL树的相似
	但是在红黑树中 , 加入了指向父节点的指针这一成员
	所以在旋转之后要去修改相关节点的父节点的指向
	而要修改的指向有6处 : 曾祖父的孩子  , 祖父的父亲 , 
	祖父的左孩子 , 父亲的父亲 , 父亲的右孩子 , 父亲右孩子(右孙子)的父亲
	*/
	void rightRotate(RBNode* node)
	{
		//父节点在旋转之后会成为新的根节点(局部)
		RBNode* child = node->_left;
		//旋转后 , 父节点的父亲变为 原祖父节点的父亲(即父亲的父亲)
		child->_parent = node->_parent;
		//如果祖父节点是根节点 , 旋转后将根节点置为原父节点
		if (node->_parent == nullptr)
		{
			_root = child;
		}
		else
		{
			//祖父节点是曾祖父节点的左孩子 , 旋转后曾祖父节点的左孩子是父节点
			//(即曾祖父的孩子)
			if (node->_parent->_left == node)
			{
				node->_parent->_left = child;
			}
			else
			{
				//否则旋转后曾祖父节点的右孩子是父节点
				node->_parent->_right = child;
			}
		}
		//旋转后 , 祖父节点的左孩子是父节点的右孩子(即祖父的左孩子)
		node->_left = child->_right;
		if (child->_right != nullptr)
		{
			//旋转后 , 父节点的右孩子的父节点变为祖父节点(即右孙子的父亲)
			child->_right->_parent = node;
		}
	
		//旋转后 , 父节点的右孩子是祖父节点(即父亲的右孩子)
		child->_right = node;
		//旋转后祖父节点的父亲变为  原父节点(即祖父的父亲)
		node->_parent = child;
	}

红黑树的插入操作

(之前在BST树的帖子中已经详细讲了插入的操作 , 这里不再赘述 , 重点来讲插入后的调整)

       先使用普通二叉搜索树的节点插入算法将新节点插入树中 , 然后将该节点涂为红色 . 为了不破坏红黑性质 , 需要对有关节点进行重新着色并旋转 . 因为向二叉搜索树中添加一个元素 , 就是向其中添加一个叶子节点(注意这里忽略红黑树的NIL节点) , 所以在向红黑树中插入一个元素时就必须将其涂为红色 . 如果不这样 , 就会违反红黑性质(4) , 即从某一节点到达其子孙叶节点的每一条简单路径上包含的黑色节点的个数就会不同 .

插入操作的3种情况

       如果插入的节点的父节点是黑色 , 那么红黑性质没有破坏 , 操作就此完成 ; 但是如果插入的节点的父节点是红色的 , 就违反了性质(3) , 只有在父节点是红色的情况下是需要修复(调整)的

可以将情况归结为3种 :

  • 1.叔叔和父节点都是红色 , 将叔叔和父亲变为黑色 , 将祖父变为红 , 此时指向祖父 , 再向上检查
    在这里插入图片描述

  • 2.叔叔的节点是黑色 , 祖父节点是黑色 , 且插入的新节点与父节点和祖父节点在一条直线上 . 以祖父节点进行旋转 ,将祖父的黑色(送)给旋转上来的父节点 .
    在这里插入图片描述

  • 3.叔叔节点是黑色 , 祖父的节点是黑色 , 且新插入的节点与父节点和祖父节点不在一条直线上 , 以父节点进行旋转 , 使其祖父孙三节点在一条直线上 , 然后再以祖父节点进行旋转 , 将祖父的黑色(送)给旋转上来的父节点 .
    在这里插入图片描述

注 : 第一种情况相对复杂 , 如果插入的节点的叔叔节点为红色 , 那么就有可能无论如何旋转总是会出现"红红节点"的情况 . 这时所采用的解决方法就是逐层上升式的调整和着色

完整示例

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

代码实现
	// 红黑树的插入
	void insert(const T &val)
	{
		if (_root == nullptr)
		{
			_root = new RBNode(val, BLACK);
			return;
		}

		RBNode *parent = nullptr;
		RBNode *cur = _root;
		while (cur != nullptr)
		{
			parent = cur;
			if (cur->_data > val)
			{
				cur = cur->_left;
			}
			else if (cur->_data < val)
			{
				cur = cur->_right;
			}
			else
			{
				return;
			}
		}
		
		//以红色节点插入到红黑树中
		RBNode *node = new RBNode(val, RED, parent);
		if (val < parent->_data)
		{
			parent->_left = node;
		}
		else
		{
			parent->_right = node;
		}

		//父亲节点为红色 , 红黑树性质被破坏 , 需要进行调整
		if (color(parent) == RED)
		{
			fixAfterInsert(node);
		}
	}

	//插入调整
	/*
	插入的三种情况 , 只有在父节点是红色的情况下才进行调整
	情况1 : 看叔叔结点是红色, 叔叔和父节点都是红色 , 将叔叔和父亲变为黑 , 将祖父变为红 , 此时指向祖父 , 再向上检查
	情况2 : 看叔叔 , 叔叔的节点是黑色 , (祖父的节点是黑色) , 且新插入的节点与父节点和祖父节点在一条直线上 , 以祖父节点进行旋转 ,将祖父的黑色(送)给旋转上来的父节点 
	情况3 : 看叔叔 , 叔叔节点是黑色 ,  (祖父的节点是黑色) , 且新插入的节点与父节点和祖父节点不在一条直线上 , 以父节点进行旋转 , 使其祖父孙三节点在一条直线上 , 
			   然后再以祖父节点进行旋转 , 将祖父的黑色(送)给旋转上来的父节点
	*/
	void fixAfterInsert(RBNode *node)
	{
		while (color(parent(node)) == RED)
		{
			//插在了祖先节点的左子树当中
			if (left(parent(parent(node))) == parent(node))
			{
				// 插在了祖先节点的左子树当中
				RBNode *uncle = right(parent(parent(node)));
				// 情况1 : 叔叔结点是红色
				if (color(uncle) == RED)
				{
					//将父亲和叔叔节点都变为黑色
					setColor(parent(node), BLACK);
					setColor(uncle, BLACK);
					//将祖父节点变为红色
					setColor(parent(parent(node)), RED);
					//将指针指向祖父节点 , 继续向上检查
					node = parent(parent(node));
				}
				else
				{
					// 情况3
					/* 
					情况3 : 叔叔结点是黑色,且插入结点与其父亲结点 , 祖父结点不在一条直线上  
					这里我们为了和后面情况2的代码兼容,因此让node
					指向中间节点(父节点),进行旋转操作后node指向
					三个节点中最后一个结点。
					 */
					if (node == right(parent(node)))
					{
						node = parent(node);
						//进行左旋操作
						leftRotate(node);
					}

					// 情况2
					/*
					情况2 : 叔叔结点是黑色,且插入结点与其父亲结点 , 祖父结点在一条直线上
					 */
					//将父节点置为黑色 , 旋转后父节点为新的根节点(局部)
					setColor(parent(node), BLACK);
					//将祖父节点置为红色 , 旋转后成为原父节点的右孩子
					setColor(parent(parent(node)), RED);
					//进行右旋操作
					rightRotate(parent(parent(node)));
					break;
				}
			}
			else 
			{
				// 插在了祖先节点的右子树当中 , 与上述过程是镜像关系
				RBNode *uncle = left(parent(parent(node)));
				// 情况1 : 叔叔结点是红色
				/*
				将父亲和叔叔节点置为黑色 , 祖父节点置为红色 
				将指针指向祖父节点 , 继续调整
				*/
				if (color(uncle) == RED)
				{
					setColor(parent(node), BLACK);
					setColor(uncle, BLACK);
					setColor(parent(parent(node)), RED);
					node = parent(parent(node));
				}
				else
				{
					// 情况3
					/*
					叔叔结点是黑色,且插入结点与其父亲结点 , 祖父结点不在一条直线上
					先进行旋转 , 使插入节点 , 其父节点和祖先节点在同一条直线上
					*/
					if (node == left(parent(node)))
					{
						node = parent(node);
						rightRotate(node);
					}

					// 情况2
					setColor(parent(node), BLACK);
					setColor(parent(parent(node)), RED);
					leftRotate(parent(parent(node)));
					break;
				}
			}
		}

		//在调整过程中有可能修改根节点的颜色为红色 , 需要修改为黑色
		setColor(_root, BLACK);
	}

红黑树的删除操作

(之前在BST树的帖子中已经详细讲了删除的操作 , 这里不再赘述 , 重点来讲删除后的调整)

        从红黑树中删除一个节点的方法就是在节点删除(通过普通二叉搜索节点删除方法)后 , 针对不同情况对红黑树进行调整以保持其红黑性质的过程

删除红色不会影响红黑树的性质 , 如果是黑色的 , 就需要从child开始调整 .

删除操作的4种情况

删除后调整的4种情况 :

  • 1.删除的节点是红色或者它是树的根节点 , 直接调整成黑色节点 , 结束

  • 2.1.兄弟是黑色 , 兄弟的右孩子是红色 , 此时做两次旋转 , (此时才能借调一个黑色的节点到左子树上)

  • 2.2.兄弟是黑色 , 兄弟的左孩子是红色 , 此时做两次旋转
    在这里插入图片描述

  • 3.兄弟是黑色 , 兄弟的左右孩子都是黑色 , 直接把兄弟改成红色 , 将指针指向父节点 , 接着向上调整
    在这里插入图片描述

  • 4.兄弟是红色 , 兄弟的孩子都(一定)是黑色 , 此时直接做一个旋转操作 , 并改变节点的颜色

在这里插入图片描述

完整示例
示例1

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

示例2

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

代码实现
	//红黑树的删除操作
	void remove(const T &val)
	{
		if (_root == nullptr)
			return;

		RBNode *parent = nullptr;
		RBNode *cur = _root;
		while (cur != nullptr)
		{
			if (cur->_data > val)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if(cur->_data < val)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				break;
			}
		}

		if (cur == nullptr)
			return;
		//情况3 , 同BST树一样 , 找前驱节点
		if (cur->_left != nullptr && cur->_right != nullptr)
		{
			RBNode *old = cur;
			parent = cur;
			cur = cur->_left;
			while (cur->_right != nullptr)
				cur = cur->_right;
			old->_data = cur->_data;
		}
		
		RBNode *child = cur->_left;
		if (child == nullptr)
			child = cur->_right;
		//情况2
		if (child != nullptr)
		{
			child->_parent = cur->_parent;
			if (cur->_parent == nullptr)
			{
				_root = child;
			}
			else
			{
				if (cur->_parent->_left == cur)
				{
					cur->_parent->_left = child;
				}
				else
				{
					cur->_parent->_right = child;
				}
			}

			Color color = color(cur);
			delete cur;

			if (color == BLACK)
			{
				fixAfterRemove(child);
			}
		}
		else
		{
			// child == nullptr
			if (parent == nullptr)
			{ 
				_root = nullptr;
			}
			else
			{
				if (color(cur) == BLACK)
				{
					fixAfterRemove(cur);
				}

				if (cur->_parent->_left == cur)
				{
					cur->_parent->_left = nullptr;
				}
				else
				{
					cur->_parent->_right = nullptr;
				}
				delete cur;
			}
		}
	}
	//删除调整
	/*
	删除后调整的4种情况 : 
	 1.删除的节点是红色或者它是树的根节点  , 直接调整成黑色节点 , 结束
	 2.1.兄弟是黑色 , 兄弟的右孩子是红色 , 此时做两次旋转 , (此时才能借调一个黑色的节点到左子树上)
	 2.2.兄弟是黑色 , 兄弟的左孩子是红色 , 此时做两次旋转
	 3.兄弟是黑色 , 兄弟的左右孩子都是黑色 , 直接把兄弟改成红色 , 将指针指向父节点 , 接着向上调整
	 4.兄弟是红色 , 兄弟的孩子都(一定)是黑色 , 此时直接做一个旋转操作 , 并改变节点的颜色 
	*/
	void fixAfterRemove(RBNode* node)
	{
		while (Color(node) == BLACK)
		{
			//删除的节点在父节点的左侧
			if (left(parent(node)) == node)
			{
				//兄弟节点就在右侧
				RBNode *brother = right(parent(node));
				//兄弟节点是红色 , 情况4
				if (color(brother) == RED)
				{
					//旋转左旋后兄弟节点变为黑色
					setColor(brother, BLACK);
					//旋转后父节点变为红色
					setColor(parent(node), RED);
					//左旋操作
					leftRotate(parent(node));
					//旋转后的兄弟节点更新为原父节点(现在的)右节点
					brother = right(parent(node));
				}
				//兄弟节点的左右孩子都为黑色 , 情况3
				if(color(left(brother)) == BLACK 
					&& color(right(brother) == BLACK)
				{
					//将兄弟变为红色
					setColor(brother, RED);
					//将指针指向父节点 , 继续向上调整
					node = parent(node);
				}
				else
				{
					//情况2.1和2.2
					//兄弟的左孩子为红色
					if (color(right(brother)) != RED)
					{
						//旋转后 , 兄弟节点的颜色变为红色
						setColor(brother, RED);
						//旋转后 , 兄弟节点的左孩子的颜色变为黑色
						setColor(left(brother), BLACK);
						//右旋操作
						rightRotate(brother);
						//旋转后的兄弟节点更新为原父节点(现在的)右节点
						brother = right(parent(node));
					}
					//兄弟的右孩子为红色
					//旋转后的兄弟节点的颜色为原父节点的父亲的颜色
					setColor(brother, color(parent(node)));
					//旋转后父节点的颜色为黑色
					setColor(parent(node), BLACK);
					//左旋操作
					leftRotate(parent(node));
					break;
				}
			}
			//删除的节点在父节点的右侧 , 与在左侧为镜像关系
			else
			{
				RBNode *brother = left(parent(node));
				if (color(brother) == RED)
				{
					setColor(brother, BLACK);
					setColor(parent(node), RED);
					rightRotate(parent(node));
					brother = left(parent(node));
				}

				if (color(left(brother)) == BLACK
					&& color(right(brother) == BLACK)
				{
					setColor(brother, RED);
					node = parent(node);
				}
				else
				{
					if (color(left(brother)) != RED)
					{
						setColor(brother, RED);
						setColor(right(brother), BLACK);
						leftRotate(brother);
						brother = left(parent(node));
					}

					setColor(brother, color(parent(node)));
					setColor(parent(node), BLACK);
					rightRotate(parent(node));
					break;
				}
			}
		}

		// 当前路径上如果碰见红色,直接把红色节点调整成黑色节点,结束	
		setColor(node, BLACK);
	}
删除操作的总结
  • 1.红黑树的删除操作是最复杂的操作 , 复杂的地方就在于删除了黑色节点的时候 , 如何从兄弟节点去借调节点 , 以保证树的颜色符合定义 . 由于红色的兄弟节点是没法借调出黑色节点的 , 这样只能通过选择操作让他上升到父节点 , 而由于它是红色节点 , 所以他的子节点就是黑色的 , 可以借调 .
  • 2.对于兄弟节点是黑色节点的可以分成3种情况来处理 , 当所以的兄弟节点的子节点都是黑色节点时 , 可以直接将兄弟节点变红 , 这样局部的红黑树颜色是符合定义的 . 但是整棵树不一定是符合红黑树定义的 , 需要往上追溯继续调整 .
  • 3.对于兄弟节点的子节点为左红右黑或者(全部为红 , 右红左黑)这两种情况 , 可以先将前面的情况通过选择转换为后一种情况 , 在后一种情况下 , 因为兄弟节点为黑 , 兄弟节点的右节点为红 , 可以借调出两个节点出来做黑节点 , 这样就可以保证删除了黑节点 , 整棵树还是符合红黑树的定义的 , 因为黑色节点的个数没有改变 .
  • 4.红黑树的删除操作是遇到删除的节点为红色 , 或者追溯调整到了root节点 , 这是删除的修复操作完毕 .

链接 :
<< C++高级数据结构算法#二叉搜索树(BST树)的基本操作(递归与非递归) >>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值