【C++小白到大牛】红黑树那些事儿

目录

前言:

一、红黑树的概念

二、红黑树的性质

三、红黑树结点的定义

四、红黑树的插入

情况一:u存在且为红

情况二:u不存在/u存在且为黑

小总结:

原码:

五、红黑树的检验

六、性能比较


前言:

我们之前已经学过了二叉搜索树的优化版——AVL树,这次我们来学习二叉搜索树的另外一种优化版本——心心念念的红黑树。

之前还是初学者的博主觉得红黑树简直就是神明般的存在,而现如今也能理清楚红黑树的基本框架和插入规则,不禁感慨万分。忆往昔峥嵘岁月,展未来任重道远~

一、红黑树的概念

红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。因此下面我们就来研究怎样的一个配色规则使得红黑树接近平衡。

二、红黑树的性质

  1.  每个结点不是红色就是黑色 
  2. 根节点是黑色的  
  3. 如果一个节点是红色的,则它的两个孩子结点必须是黑色的,没有连续的红色节点  
  4.  每条路径均包含相同数目的黑色结点  

问题:为什么满足上面的性质,红黑树就能保证:其最长路径中节点个数不会超过最短路径节点个数的两倍?

答:因为在上面的规则制约下,最长路径就是一黑一红,而最短路径就是全黑,又因为每条路径的黑色结点数量相同,因此能推出上面的结论最长路径中节点个数不会超过最短路径节点个数的两倍!

三、红黑树结点的定义

enum Colour
{
	RED,
	BLACK
};

template<class K, class V>
struct RBTreeNode
{
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	pair<K, V> _kv;
	Colour _col;

	RBTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		,_col(RED)
	{}
};

问题:在节点的定义中,为什么要将节点的默认颜色给成红色的?也就是新增节点为什么是红色?

答:

因为新插入结点颜色是红色不一定错误,但是插入黑色一定有问题

插入黑色结点为什么难以控制,违反规则?因为你在任何一个叶子结点的位置插入黑色结点就不满足,每条路径的黑色结点的数量是一样的,没法解决! 

四、红黑树的插入

第一步: 按照二叉搜索的树规则找新节点的插入位置

这里根二叉搜索树的所有先前插入规则是一样的,都需要根据二叉搜索树的性质去找到新插入结点的具体位置

第二步:检测新节点插入后,红黑树的性质是否造到破坏

因为默认新插入结点的颜色是红色,所以首先需要判断父亲的颜色:

  1. 插入位置的父亲是黑色,不需要处理,插入结束
  2. 插入位置的父亲是红色,出现了连续的红色结点,需要处理

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

所以下面的所有情况都是根据插入结点的父亲是红色来判断。我们来根据叔叔的情况来分情况讨论~

我们为什么要以u的存在与否、颜色与否来分情况讨论呢?因为只有叔叔的结点是不确定的,c、p、g颜色都是确定的!因为c、p颜色都是红色,而g的颜色一定是黑色,因为不能有两个连续的红色结点,所以只有叔叔是未知数。

情况一:u存在且为红

解决方法:p/u变黑,g变红,如果g是根,再把g变黑,如果g不是根,继续往上处理(g当成c,此时的g就相当于新插入的红色结点c,循环判断,直到根节点为止)

爷爷为什么要变红?因为爷爷有可能不是整个树的根,为了保持当前树的黑色结点不变,如果爷爷是根,将爷爷变成黑即可。

这里的a/b/c/d/e不用管,p/u是g的左或者右都不影响,cur是p的左或者右也不影响,处理方式都是一样的

情况二:u不存在/u存在且为黑

下面是u不存在的情况:

这时就已经违反了红黑树的规则,就需要我们进行旋转处理

p为g的左孩子,cur为p的左孩子,则进行有单旋转;相反,p为g的右孩子,cur为p的右孩子,则进行左单旋转。

颜色变换:p、g变色,p变黑,g变红

下面是u存在且为黑的情况

这里也是单旋,因此在结构一样的情况下,u不存在和u存在且为空的情况处理方式都是一样的


上面因为因插入的结点都是与原先结点在同一侧,因此直接采用单旋就可以解决问题,下面我们来讲解,新插入结点与原先结点不在同一侧,需要先单旋变为同一侧,接着再以根节点为旋转点,朝着另外一个方向进行旋转,这里就是双旋,与AVL树旋转的思路几乎一样

下面是u不存在的情况,旋转最后将cur和g变色

然后是u存在且为黑的情况

小总结:

p为g的左孩子,cur为p的右孩子,左右双旋+变色

p为g的右孩子,cur为p的左孩子,右左双旋+变色。

看是否在一边,如果在一边直接单旋即可,若不在,先单旋变为同一边,接着再单旋即可。

原码:

template<class K, class V>
class RBTree
{
	typedef RBTreeNode<K, V> Node;
public:
	bool Insert(const pair<K, V>& kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			_root->_col = BLACK;
			return true;
		}

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

		cur = new Node(kv); // 红色的
		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		cur->_parent = parent;

		while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent;
			if (parent == grandfather->_left)
			{
				Node* uncle = grandfather->_right;
				// 情况一:叔叔存在且为红
				if (uncle && uncle->_col == RED)
				{
					// 变色
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					// 继续往上处理
					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					// 情况二:叔叔不存在或者存在且为黑
					// 旋转+变色
					if (cur == parent->_left)
					{
						//       g
						//    p    u
						// c
						RotateR(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						//       g
						//    p     u
						//      c
						RotateL(parent);
						RotateR(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}

					break;
				} 
			}
			else
			{
				Node* uncle = grandfather->_left;
				// 情况一:叔叔存在且为红
				if (uncle && uncle->_col == RED)
				{
					// 变色
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					// 继续往上处理
					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					// 情况二:叔叔不存在或者存在且为黑
					// 旋转+变色
					//      g
					//   u     p
					//            c
					if (cur == parent->_right)
					{
						RotateL(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						//		g
						//   u     p
						//      c
						RotateR(parent);
						RotateL(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}

					break;
				}
			}
		}

		_root->_col = BLACK;

		return true;
	}

五、红黑树的检验

思路:

要想验证这棵树是不是红黑树,需要从红黑树的概念入手,分为以下几点:

  1. 根是黑的
  2. 没有连续的红色结点
  3. 每条路径的黑色结点的数量相同

可以先遍历一条路径直接到根节点,以这条路径的黑色结点作为基准值,然后再接着遍历其他路径,看其他路径的黑色结点数量是否与其相同,进行判断即可。

bool Check(Node* cur, int blackNum, int refBlackNum)
	{
		if (cur == nullptr)
		{
			if (refBlackNum != blackNum)
			{
				cout << "黑色节点的数量不相等" << endl;
				return false;
			}

			//cout << blackNum << endl;
			return true;
		}

		if (cur->_col == RED && cur->_parent->_col == RED)
		{
			cout << cur->_kv.first << "存在连续的红色节点" << endl;
			return false;
		}

		if (cur->_col == BLACK)
			++blackNum;
		
		return Check(cur->_left, blackNum, refBlackNum)
			&& Check(cur->_right, blackNum, refBlackNum);
	}

	bool IsBalance()
	{
		if (_root && _root->_col == RED)
			return false;

		int refBlackNum = 0;
		Node* cur = _root;
		while (cur)
		{
			if(cur->_col == BLACK)
				refBlackNum++;

			cur = cur->_left;
		}

		return Check(_root, 0, refBlackNum);
	}

六、性能比较

红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O(logN),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,红黑树降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。

  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

可涵不会debug

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

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

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

打赏作者

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

抵扣说明:

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

余额充值