数据结构之<RBTree >

发布于:2023-01-16 ⋅ 阅读:(452) ⋅ 点赞:(0)

🌈前言

本篇文章进行数据结构中RBTree(红黑树)的学习!!!


🌅红黑树

🌷1、RBTree的概念

概念:

  • 红黑树:它是二叉搜索树的一种,区别在于在每个结点上增加一个存储位表示结点的颜色,可以是 “RED” 或 “RLACK”
  • 它通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是"接近"平衡的

在这里插入图片描述

红黑树的规则:

  1. 根节点一定为黑色
  2. 如果一个节点为红色,则它的左右孩子均为黑色(没有连续的红节点)
  3. 对于每个结点,从该结点到其所有后代的叶结点的路径上,均包含相同数目的黑色结点
  4. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
  5. 红黑树的最长路径的黑色节点不超过最短路径的二倍

🌸2、RBTree节点的定义

// 红黑树的颜色
enum Color
{
	RED,
	BLACK
};

// RBTree的节点
template <typename K, typename V>
struct RBTreeNode
{
	RBTreeNode(pair<K, V> _kv)
		: left(nullptr)
		, right(nullptr)
		, parent(nullptr)
		, kv(_kv)
		, col(RED)
	{}

	RBTreeNode<K, V>* left;   // 节点的左孩子
	RBTreeNode<K, V>* right;  // 节点的右孩子
	RBTreeNode<K, V>* parent; // 节点双亲
	pair<K, V> kv;  // 数值
	Color col;		// 颜色
};

注意:RB树节点与avl树节点相似,不同的是增加了节点的颜色


🌰3、RBTree的插入

红黑树是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树的插入可分为两步:

  1. 按搜索树的规则进行插入新的节点
// 红黑树的规则:1.根节点是黑色的,黑节点的左右孩子可能为红也可能为黑
//			   2.如果一个节点为红色,则它的左右孩子节点为黑色(没有连续的红节点)
//			   3.对于每个节点,从该节点到其所有后代叶节点的路径上,均包含相同的数目的黑色节点(每条路径包含相同数目的黑色节点)
//			   4.叶节点(nullptr)是黑色的

bool insert(const pair<K, V>& kv)
	{
		if (root == nullptr)
		{
			root = new Node(kv);
			root->col = BLACK; // 根节点为黑色
			return true;
		}

		// 按搜索树规则插入
		Node* cur = root;
		Node* parent = nullptr;

		while (cur != nullptr)
		{
			if (cur->kv.first > kv.first) {
				parent = cur;
				cur = cur->left;
			}
			else if (cur->kv.first < kv.first) {
				parent = cur;
				cur = cur->right;
			}
			else {
				return false;
			}
		}

		cur = new Node(kv);
		// 如果插入节点是红色,可能破坏规则2  ---  如果插入节点是黑色,一定会破坏规则3(3很难处理)
		cur->col = RED;
		if (parent->kv.first > kv.first) {
			parent->left = cur;
		}
		else {
			parent->right = cur;
		}
		cur->parent = parent;
		// ...
	}

注意:

  • 插入新节点的颜色必须为红色,因为是红色的话可能会破坏规则2
  • 如果插入节点的颜色是黑色,则必定破坏规则3,规则3最难处理,最好别惹

在这里插入图片描述


  1. 检测新节点插入后,红黑树的性质是否造到破坏

前言:因为新节点的默认颜色是红色,因此:

  • 如果其双亲节点的颜色是黑色,没有违反红黑树任何性质,则不需要调整;

  • 如果新插入节点的双亲节点颜色为红色时,就违反了规则三:“不能存在连续的红色节点”,此时需要对红黑树分情况来讨论:

约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点💗💗💗

  • 情况1,cur为红,p为红,g为黑,u存在且为红

解决方式:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整

		// 存在连续红色节点时,需要一直向上调整
		while (parent != nullptr && parent->col == RED) // 因为插入节点是红色,如果parent也是红色(连续红节点),那么需要调整
		{
			Node* grandfather = parent->parent; // cur的祖父

			// 找叔叔时需要判断在祖父的哪一边
			if (grandfather->left == parent) 
			{
				Node* uncle = grandfather->right;

				// 情况一:grandfather为黑,parent为红,uncle为红,cur为红 -- 需要调整g,p,u的颜色
				if (uncle && uncle->col == RED)
				{
					parent->col = BLACK;
					uncle->col = BLACK;
					grandfather->col = RED;

					// 调整的这颗树可能为局部子树,需要继续向上处理,祖父的父亲可能为红节点, 更新cur,parent
					cur = grandfather;
					parent = cur->parent;
				}
			else
			{
				Node* uncle = grandfather->left;

				if (uncle != nullptr && uncle->col == RED)
				{
					parent->col = BLACK;
					uncle->col = BLACK;
					grandfather->col = RED;

					cur = grandfather;
					parent = cur->parent;
			}
			

在这里插入图片描述


  • 情况2:cur为红,p为红,g为黑,u不存在或u存在且为黑

在这里插入图片描述

		// 存在连续红色节点时,需要一直向上调整
		while (parent != nullptr && parent->col == RED) // 因为插入节点是红色,如果parent也是红色(连续红节点),那么需要调整
		{
			Node* grandfather = parent->parent; // cur的祖父

			// 找叔叔时需要判断在祖父的哪一边
			if (grandfather->left == parent) 
			{
				Node* uncle = grandfather->right;

				// 情况一:grandfather为黑,parent为红,uncle为红,cur为红 -- 需要调整g,p,u的颜色
				if (uncle && uncle->col == RED)
				{
					parent->col = BLACK;
					uncle->col = BLACK;
					grandfather->col = RED;

					// 调整的这颗树可能为局部子树,需要继续向上处理,祖父的父亲可能为红节点, 更新cur,parent
					cur = grandfather;
					parent = cur->parent;
				}
				// 情况二:grandfather为黑,parent为红,uncle为nullptr/黑,cur为红 -- 需要旋转+变色处理
				else
				{
					// 右旋,插入节点为parent的左边,祖父为旋转点,parent为祖父的左边,uncle为祖父的右边
					if (cur == parent->left)
					{
						//     g
						//   p
						// c
						RotateR(grandfather);
						// 更新颜色 -- 旋转后parent为根,根要变黑(根变黑后所有子树的路径的黑节点数量都加1) -- grandfather为根的子树,变红
						parent->col = BLACK;
						grandfather->col = RED;
					}
			}
			else
			{
				Node* uncle = grandfather->left;

				if (uncle != nullptr && uncle->col == RED)
				{
					parent->col = BLACK;
					uncle->col = BLACK;
					grandfather->col = RED;

					cur = grandfather;
					parent = cur->parent;
				}
				else
				{
					// 左旋,插入节点为parent的右边,祖父为旋转点,parent为祖父的右边,uncle为祖父的左边
					// g
					//    p
					//      c
					if (cur == parent->right)
					{
						RotateL(grandfather);
						// 更新颜色
						parent->col = BLACK;
						grandfather->col = RED;
					}
			}

`
具体情况分析 + 解决方法:

  • p为g的左孩子,cur为p的左孩子,则进行右单旋转;相反,
  • p为g的右孩子,cur为p的右孩子,则进行左单旋转
  • 旋转后更新颜色,p色黑色,g变红色

在这里插入图片描述


  1. 情况三:cur为红,p为红,u不存在或u存在且为黑

注意:情况1是直线的情况,而情况三是折线的情况,即cur插入的方向与p指向c的方向相反

在这里插入图片描述
具体图 + 解决方案:

  • p为g的左孩子,u为p的右孩子或不存在,c为p的右孩子时,使用左右双旋解决,左旋用p作为旋转点,,右旋使用g为旋转点,旋转后将g颜色改为红,c改为黑

  • p为g的右孩子,u为p的左孩子或不存在,c为p的左孩子时,使用右左双旋解决,右旋用p作为旋转点,,左旋使用g为旋转点,旋转后将g颜色改为红,c改为黑

在这里插入图片描述

  • 完整插入代码 – 包括这里的情况三
bool insert(const pair<K, V>& kv)
	{
		if (root == nullptr)
		{
			root = new Node(kv);
			root->col = BLACK; // 根节点为黑色
			return true;
		}

		// 按搜索树规则插入
		Node* cur = root;
		Node* parent = nullptr;

		while (cur != nullptr)
		{
			if (cur->kv.first > kv.first) {
				parent = cur;
				cur = cur->left;
			}
			else if (cur->kv.first < kv.first) {
				parent = cur;
				cur = cur->right;
			}
			else {
				return false;
			}
		}

		cur = new Node(kv);
		// 如果插入节点是红色,可能破坏规则2  ---  如果插入节点是黑色,一定会破坏规则3(3很难处理)
		cur->col = RED;
		if (parent->kv.first > kv.first) {
			parent->left = cur;
		}
		else {
			parent->right = cur;
		}
		cur->parent = parent;


		// 存在连续红色节点时,需要一直向上调整
		while (parent != nullptr && parent->col == RED) // 因为插入节点是红色,如果parent也是红色(连续红节点),那么需要调整
		{
			Node* grandfather = parent->parent; // cur的祖父

			// 找叔叔时需要判断在祖父的哪一边
			if (grandfather->left == parent) 
			{
				Node* uncle = grandfather->right;

				// 情况一:grandfather为黑,parent为红,uncle为红,cur为红 -- 需要调整g,p,u的颜色
				if (uncle && uncle->col == RED)
				{
					parent->col = BLACK;
					uncle->col = BLACK;
					grandfather->col = RED;

					// 调整的这颗树可能为局部子树,需要继续向上处理,祖父的父亲可能为红节点, 更新cur,parent
					cur = grandfather;
					parent = cur->parent;
				}
				// 情况二:grandfather为黑,parent为红,uncle为nullptr/黑,cur为红 -- 需要旋转+变色处理
				else
				{
					// 右旋,插入节点为parent的左边,祖父为旋转点,parent为祖父的左边,uncle为祖父的右边
					if (cur == parent->left)
					{
						//     g
						//   p
						// c
						RotateR(grandfather);
						// 更新颜色 -- 旋转后parent为根,根要变黑(根变黑后所有子树的路径的黑节点数量都加1) -- grandfather为根的子树,变红
						parent->col = BLACK;
						grandfather->col = RED;
					}
					else // 第三种情况,左右双旋,cur插入parent的右边,且parent为g的左边,,形成折线
					{
						//    g
						// p
						//    c
						RotateL(parent);
						RotateR(grandfather);

						// 更新颜色
						grandfather->col = RED;
						cur->col = BLACK;
					}
					break;
				}
			}
			else
			{
				Node* uncle = grandfather->left;

				if (uncle != nullptr && uncle->col == RED)
				{
					parent->col = BLACK;
					uncle->col = BLACK;
					grandfather->col = RED;

					cur = grandfather;
					parent = cur->parent;
				}
				else
				{
					// 左旋,插入节点为parent的右边,祖父为旋转点,parent为祖父的右边,uncle为祖父的左边
					// g
					//    p
					//      c
					if (cur == parent->right)
					{
						RotateL(grandfather);
						// 更新颜色
						parent->col = BLACK;
						grandfather->col = RED;
					}
					else // 右左双旋,cur插入parent的左边,且parent为g的右边,形成折线
					{
						// g
						//    p
						// c
						RotateR(parent);
						RotateL(grandfather);

						// 更新颜色
						grandfather->col = RED;
						cur->col = BLACK;
					}
					break;
				}
			}
		}
		root->col = BLACK; // 调整完后,根节点可能是红,需要调整为黑
		return true;
	}
  • 左旋与右旋代码
	// 左旋,右边高,左边矮
	void RotateL(Node* parent)
	{
		Node* subR = parent->right;
		Node* subRL = subR->left;

		parent->right = subRL;
		if (subRL != nullptr)
			subRL->parent = parent;

		Node* PpNode = parent->parent;

		subR->left = parent;
		parent->parent = subR;

		if (parent == root)
		{
			root = subR;
			root->parent = nullptr;
		}
		else
		{
			if (PpNode->left == parent) {
				PpNode->left = subR;
			}
			else {
				PpNode->right = subR;
			}
			subR->parent = PpNode;
		}
	}

	// 右旋,左边高,右边矮
	void RotateR(Node* parent)
	{
		Node* subL = parent->left;
		Node* subLR = subL->right;

		parent->left = subLR;
		if (subLR != nullptr)
			subLR->parent = parent;

		Node* PpNode = parent->parent;

		subL->right = parent;
		parent->parent = subL;

		if (parent == root)
		{
			root = subL;
			root->parent = nullptr;
		}
		else
		{
			if (PpNode->left == parent) {
				PpNode->left = subL;
			}
			else {
				PpNode->right = subL;
			}
			subL->parent = PpNode;
		}
	}

🌭4、红黑树的删除

其他文章
红黑树的删除不做讲解(比插入难很多,性价比低):《算法导论》或者《STL源码剖析》

  • 这里我只实现了第一步,以搜索树规则删除节点(注意空指针访问问题)
bool erase(const K& key)
	{
		Node* cur = root;
		Node* Cparent = nullptr;
		while (cur)
		{
			if (cur->kv.first > key)
			{
				Cparent = cur;
				cur = cur->left;
			}
			else if (cur->kv.first < key)
			{
				Cparent = cur;
				cur = cur->right;
			}
			else // 找到开始删除
			{
				// 删除节点存在左节点
				if (cur->right == nullptr) 
				{
					// 右节点为空且没有父节点,说明只有右子树或只有一个根节点
					if (Cparent == nullptr) 
					{
						root = root->left;
						if (root !=nullptr)
							root->parent = nullptr;
					}
					else
					{
						if (cur == Cparent->left) {
							Cparent->left = cur->left;
							if (cur->left)
								cur->left->parent = Cparent;
						}
						else
						{
							Cparent->right = cur->left;
							if (cur->left)
								cur->left->parent = Cparent;
						}
					}
				}
				// 删除节点存在右节点
				else if (cur->left == nullptr)
				{
					// 右节点为空且没有父节点,说明只有右子树或只有一个根节点
					if (Cparent == nullptr)
					{
						root = root->right;
						if (root != nullptr)
							root->parent = nullptr;
					}
					else
					{
						if (cur == Cparent->right)
						{
							Cparent->right = cur->right;
							if (cur->right)
							cur->right->parent = Cparent;
						}
						else
						{
							Cparent->left = cur->right;
							if (cur->right)
								cur->right->parent = Cparent;
						}
					}
				}
				else // 删除节点存在左右节点 -- 替换数据法
				{
					Node* Left_Max = cur->left;
					Node* LMparent = cur;

					while (Left_Max->right)
					{
						LMparent = Left_Max;
						Left_Max = Left_Max->right;
					}
					// 交换Cparent左子树和删除节点的值
					std::swap(Left_Max->kv.first, cur->kv.first);
					std::swap(Left_Max->kv.second, cur->kv.second);

					// Cparent链接cur中左/右节点
					if (LMparent->left == Left_Max)
					{
						LMparent->left = Left_Max->left;
						if (cur->left)
							Left_Max->left->parent = LMparent;
					}
					else
					{
						LMparent->right = Left_Max->left;
						if (Left_Max->left)
							Left_Max->left->parent = LMparent;
					}
					cur = Left_Max;
				}

				while (Cparent && Cparent->col == RED)
				{
					Node* grandfather = Cparent->parent;
					if (grandfather && grandfather->right == Cparent)
					{
						Node* uncle = grandfather->left;
						break;
					}
					else
					{
						Node* uncle = grandfather->right;
						break;
					}
				}

				if (root)
					root->col = BLACK; // 删除节点可能为根 -- 删除的处理是找替代节点,只有二个节点时让root指向下一个节点

				delete cur;
				return true;
			}
		}
		return false;
	}

🌮5、红黑树与AVL树的比较

  • 红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O( l o g 2 N log_2 N log2N),红黑树不追
    求绝对平衡,其只需保证最长路径不超过最短路径的2倍

  • 相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多


🌯6、 红黑树的应用

  1. C++ STL库 – map/set、mutil_map/mutil_set
  2. Java 库
  3. linux内核
  4. 其他一些库
本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到