C++——关联式容器(3):红黑树

3.红黑树

3.1 红黑树的概念

        上一篇文章介绍了AVL树,AVL树是解决一般的搜索二叉树效率退化的一种很好的方式。除了AVL树之外,红黑树也是一种非常好的选择。红黑树也是一种搜索二叉树,从其名字上就能够发现红黑树依靠标识红色或黑色来构建整棵树,在安排结点位置时需要考虑到颜色的问题。下面给出红黑树需要满足的条件,同时也是其性质:

①每个结点不是红色就是黑色;

②红黑树根结点的颜色是黑色

红色结点的两个孩子颜色必须是黑色,即不允许红色结点连续链接

任意一条路径(从根结点到任一叶子结点)上的黑色结点数目都是相等的

⑤叶子结点(指的是空结点)颜色是黑色的。

        通过以上性质,我们很容易的就可以发现一条路径最短的情况就是全为黑色的情况,而最长的情况就是黑红相互交替出现的情况。所以不难发现红黑树满足最长路径不超过最短路径的两倍的这个结论,这也是红黑树维持其搜索效率不退化的原因。

3.2 红黑树详解

3.2.1 红黑树的实现

3.2.1.1 红黑树的结点

        对于一棵红黑树而言,首先它是搜索二叉树,所以需要指向孩子的指针和值域。又因为其结点需要标识红色、黑色,而且如平衡二叉树一样需要特殊情况的旋转处理,所以还需要表示颜色的成员和指向父亲结点的指针。此处将其颜色定义为枚举常量进行管理。

	//定义枚举类型来标识红、黑两种颜色
	enum color {
		RED,
		BLACK
	};

	//红黑树的结点
	template<class K, class V>
	struct RBTreeNode {
		pair<K, V> _pair;
		RBTreeNode<K, V>* _left;
		RBTreeNode<K, V>* _right;
		RBTreeNode<K, V>* _parent;
		color _color;

		RBTreeNode(pair<K, V> kv)
			:_pair(kv)
			, _left(nullptr)
			, _right(nullptr)
			, _parent(nullptr)
			//红黑树要求每条路径的黑色节点数目相等,所以默认插入红色节点更加合理
			, _color(RED)
		{}
	};

        对于结点的构造函数,需要指出的是新结点的默认颜色。由于红黑树要求各路径下的黑色节点树木相同,因此如果插入的是黑色节点,则必然会打破这个规则;而如果插入红色结点,则有可能违反不允许连续红色结点规则,但是仅是有可能。因此两害相权取其轻,我们选择红色结点作为默认插入的颜色。

3.2.1.2 默认成员函数

        红黑树的默认成员函数和二叉搜索树也几乎一致。拷贝构造用前序遍历的方式;赋值重载复用拷贝构造;析构函数用后序遍历的方法来析构结点。

	template<class K, class V>
	class RBTree {
		typedef RBTreeNode<K, V> RBNode;

	public:
		//无参构造
		RBTree()
			:_root(nullptr)
		{}

		//拷贝构造
		RBTree(const RBTree<K, V>& rb)
		{
			_root = copy(rb._root);
		}
	private:
		RBNode* copy(RBNode* root)
		{
			if (root == nullptr) return nullptr;
			RBNode* newnode = new RBNode(root->_pair);
			newnode->_left = copy(root->_left);
			newnode->_right = copy(root->_right);
			return newnode;
		}

	public:
		//析构函数
		~RBTree()
		{
			destroy(_root);
			_root = nullptr;
		}
	private:
		void destroy(RBNode* root)
		{
			if (root == nullptr) return;
			destroy(root->_left);
			destroy(root->_right);
			delete root;
		}

	public:
		//赋值重载操作符
		RBTree<K, V>& operator=(const RBTree<K, V> rb)
		{
			swap(_root, rb->_root);
			return *this;
		}
	private:
		RBNode* _root;
	};

3.3 插入结点——旋转变色

        同样的,如何处理新节点的插入逻辑也是红黑树的难点之一,红黑树的插入主要用到的调整方法也是在AVL树中介绍过的单旋和双旋,然后再进行颜色的调整。

3.3.1 第一步——插入结点

        第一步依旧是喜闻乐见的插入结点操作,搜索二叉树都是这个插入方法,找到对应位置将结点链接上去即可。

3.3.2 第二步——调整红黑树的颜色

        我们提前约定好各个可能涉及到的结点的命名,以便后文的叙述。如图所示,以新结点为标准,其父亲结点我们成为parent,简称p;p的兄弟节点称作uncle,简称u;而p和u的父结点则是grandparent,简称g。

        我们在上文提到了,插入红色结点后可能由于新结点的父结点是黑色而没有违反规则,无需调整的情况。所以当且仅当p也为红色时,这时新结点和parent形成了连续的红色,需要进行调整。

        在调整的过程中,我们可以将所有可能出现的情况总结为两类。下面将针对这两种情况一一讨论,在讨论中需要时刻关注到任意一条路径上的黑色结点数目都是相等的这一特性。

        a. u为红色

        当看到有两个连续的红结点,第一想法肯定是寻找能否直接变色。面对u为红色的情况,可以发现左子树p和右子树均为红色,因此当同时使p和u变为黑色后,以g为根的整颗子树黑色结点数目都+1,为了保持黑色节点数目不变,所以将黑色的g变为红色(因为p和u是红色的,所以g一定是黑色),这样就维持了黑色节点数目稳定。

        但是这样会产生另一个问题,即g变为红色,可能g的父亲也是红色,这就又导致了连续的红结点。不过处理方法也不难,就是将g作为新的cur,然后对应着此处总结的三种方法去变换即可,即为一个循环的过程。

        b. u为黑色或u不存在

        当u为黑色或不存在时,说明不可以再同时变色了(因为u那边没有现成的红色节点),所以需要提供其他办法。于是就请到了二叉搜索树的传统方法:旋转,通过合理的旋转改变两个红色结点的位置来消除连续红结点的问题。于是我们可以继续细分为四种处理防方式。

        四种处理方式又可以根据位置关系分为单旋和双选,当g、p和cur是顺位(左左或右右)时,仅一次单旋即可;当g、p和cur是逆位(左右或右左)时,则需要双旋

        ①左左顺位——右旋,p变黑,g变红

        ②右右顺位——左旋,p变黑,g变红

        ③左右逆位——左右双旋,cur变黑,g变红

        ④右左逆位——右左双旋,cur变黑,g变红

3.3.3 代码整理

        最后给出插入逻辑以及颜色调整的完整代码。

		//插入
		bool Insert(const pair<K, V>& kv)
		{
			//第一个结点特殊处理
			if (_root == nullptr)
			{
				_root = new RBNode(kv);
				_root->_color = BLACK;
				return true;
			}

			RBNode* cur = _root;
			RBNode* parent = nullptr;
			while (cur)
			{
				if (cur->_pair.first > kv.first)
				{
					parent = cur;
					cur = cur->_left;
				}
				else if (cur->_pair.first < kv.first)
				{
					parent = cur;
					cur = cur->_right;
				}
				else
				{
					return false;
				}
			}
			cur = new RBNode(kv);
			if (parent->_pair.first > kv.first)
			{
				parent->_left = cur;
				cur->_parent = parent;
			}
			else
			{
				parent->_right = cur;
				cur->_parent = parent;
			}

			//调整红黑树颜色
			//红黑树规则:
			// ①根结点颜色一定是黑色
			// ②不能出现连续的红结点,即红结点的孩子一定是黑色
			// ③各条路径(根结点->叶子结点)上的黑色节点数目相同
			// ④叶子结点(此处认为是空结点)颜色为黑色
            //在这样的规则限制下,不难发现红黑树最长路径一定小于最短路径的二倍这个特征
			
			//当违反了红黑树规则才需要调整红黑树颜色
			//插入新的结点时,选择插入红色节点可能违反不能有连续的红色节点的规则;选择插入黑色节点则必然会违反黑色节点数目相同的规则
			//因此两害相权取其轻,选择插入红色节点,因此我们主要处理的就是连续红结点的问题
			//于是连续的两个节点:cur和p都是红色的,而u作为p的兄弟节点决定了调整方式,而在调整中受影响的则是p和u的父结点g
			while (parent && parent->_color == RED)
			{
				//根据形式的不同,一般分为三类处理
				//在解决连续红色的问题时,也要兼顾到黑色节点数目相同这一规则
				RBNode* grandparent = parent->_parent;
				RBNode* uncle = parent == grandparent->_left ? grandparent->_right : grandparent->_left;
				//①u为红色(p、u均为红)
				//p、u同时变为黑色,g变为红色,因为g是红色,因此需要继续向上检查
				if (uncle && uncle->_color == RED)
				{
					parent->_color = uncle->_color = BLACK;
					grandparent->_color = RED;
					parent = grandparent->_parent;
					cur = grandparent;
				}
				//②u为黑色或不存在,而g、p和cur是顺位(左左或右右)
				//此时单纯的变色会使得p子树和u子树路径黑色节点数目不同(因为在修改p为黑,u本就为黑,u相较p黑色节点少一个)
				//为了可以顺利变色,我们首先要旋转,红色的p成为了子树的根,黑色的g成为了u这棵树的父结点,此时可以证明只需要p变为黑,g变为红即可
				//旋转操作就是AVL树中的左右单旋

				//③u为黑色或不存在,而g、p和cur是逆位(左右或右左)
				//此时只需要将p结点左旋或右旋一次即可形成如②的情况,因此这种情况使用双旋即可
				else
				{
					if (parent == grandparent->_left)
					{
						//左左顺位——右旋,p变黑,g变红
						if (cur == parent->_left)
						{
							RotateR(grandparent);
						}
						//左右逆位——左右双旋,cur变黑,g变红
						else
						{
							RotateLR(grandparent);
						}
					}
					else
					{
						//右右顺位——左旋,p变黑,g变红
						if (cur == parent->_right)
						{
							RotateL(grandparent);
						}
						//右左逆位——右左双旋,cur变黑,g变红
						else
						{
							RotateRL(grandparent);
						}
					}
					//由于②③结果的子树根结点都是黑色因此不会影响上一层,无需向上检查
					break;
				}
			}
			//根结点有可能变色,需要修改
			_root->_color = BLACK;
			return true;
		}
	private:
		void RotateL(RBNode* grandparent)
		{
			RBNode* subR = grandparent->_right;
			RBNode* subRL = subR->_left;

			//结点链接三组:subR和grandparent、grandparent和sunRL、grandparent->_parent和subR
			subR->_left = grandparent;
			grandparent->_right = subRL;
			if (grandparent->_parent == nullptr)
			{
				_root = subR;
			}
			else if (grandparent->_parent->_left == grandparent)
			{
				grandparent->_parent->_left = subR;
			}
			else
			{
				grandparent->_parent->_right = subR;
			}

			subR->_parent = grandparent->_parent;
			grandparent->_parent = subR;
			if (subRL)	//右左子树为空树
				subRL->_parent = grandparent;

			//修改颜色:p变黑,g变红
			subR->_color = BLACK;
			grandparent->_color = RED;
		}

		void RotateR(RBNode* grandparent)
		{
			RBNode* subL = grandparent->_left;
			RBNode* subLR = subL->_right;

			//结点链接三组:subL和grandparent、grandparent和sunLR、grandparent->_parent和subL
			subL->_right = grandparent;
			grandparent->_left = subLR;
			if (grandparent->_parent == nullptr)
			{
				_root = subL;
			}
			else if (grandparent->_parent->_left == grandparent)
			{
				grandparent->_parent->_left = subL;
			}
			else
			{
				grandparent->_parent->_right = subL;
			}

			subL->_parent = grandparent->_parent;
			grandparent->_parent = subL;
			if (subLR)	//左右子树为空树
				subLR->_parent = grandparent;

			//修改颜色:p变黑,g变红
			subL->_color = BLACK;
			grandparent->_color = RED;
		}

		//左右双旋
		void RotateLR(RBNode* grandparent)
		{
			RBNode* subL = grandparent->_left;
			RBNode* subLR = grandparent->_left->_right;

			//只需要旋转,颜色最后指定
			RotateL(subL);
			RotateR(grandparent);

			//修改颜色:cur变黑,g变红
			subLR->_color = BLACK;
			grandparent->_color = RED;
		}

		//右左双旋
		void RotateRL(RBNode* grandparent)
		{
			RBNode* subR = grandparent->_right;
			RBNode* subRL = grandparent->_right->_left;

			//只需要旋转,颜色最后指定
			RotateR(subR);
			RotateL(grandparent);

			//修改颜色:cur变黑,g变红
			subRL->_color = BLACK;
			grandparent->_color = RED;
		}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

犀利卓

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值