【数据结构】认识红黑树 --map/set的底层原理

1. 红黑树的概念

红黑树也叫RB树(Red-Black Tree),实际就是一种二叉搜索树,只是它的节点不是通过平衡因子来控制树的形状,而是为节点设置了两种颜色来控制,使得树中最长路径中节点个数不会超过最短路径节点个数的两倍,这样就能保证树触发旋转的几率大大减小。因此红黑树在每个节点上增加了一个一个的存储位表示节点的颜色,可以是Red或Black。
在这里插入图片描述

2.红黑树的性质

  1. 每个节点不是红色就是黑色
  2. 根节点是黑色的
  3. 红色结点的两个孩子结点,颜色必须是黑色的
  4. 对于每个结点,从该结点到其所有后代的叶结点的简单路径上,均包含相同数目的黑色结点。
  5. 每个空的叶子节点都是黑色的(非空的叶子节点可以不是黑色)
思考1:满足上面的条件,为什么就可以保证最长路径中节点个数不会超过最短路径节点个数的两倍?

解释:首先我们根据红黑树的特性知道,红黑树中的最短路径即为全黑结点的路径(即黑结点个数),而最长路径只能是黑红相间的路径(即最多黑结点+红结点个数,且红结点<=黑结点);故由数学只是可以推出:最长路径<=最短路径*2
其次,根据上述性质,画图就可简单的看到这个原理
在这里插入图片描述

思考2:我们为什么要把新增结点的默认颜色给成红色?

解释:若我们新增的结点是黑色必定会违反第4条性质,因为根节点一边的黑色节点肯定多于另外一边,从而就会引起每条路径都不平衡的结果,这很危险,也很难判断该如何操作才能再次达到平衡。
但我们若把默认结点给为红色有可能违反第3条性质,因为可能会连续出现两个红色结点,不过此时就只是对当前路径上的平衡有影响,不会影响其他路径的平衡。我们可以根据红黑树的变换规则,对该路径进行变色或者旋转的调整,再次达到标准的红黑树!

3.红黑树结点的定义

// 定义结点的颜色
enum Color {RED,BLACK};
template<class K,class V>
struct RBSTreeNode
{
	RBSTreeNode(const pair<K, V>& kv)
	:_left(nullptr)
	, _right(nullptr)
	, _parent(nullptr)
	, _kv(kv)
	, _col(RED)
	{}
	RBSTreeNode<K, V>* _left;
	RBSTreeNode<K, V>* _right;
	RBSTreeNode<K, V>* _parent;

	pair<K, V> _kv;
	color _col;
};

4.红黑树的插入操作详解

1)按二叉搜索树的性质插入红色结点 — 比根节点小插入根节点左边,比根节点大插入右边,与根节点相同则插入失败
2)检测插入新结点后,红黑树的性质是否遭到破坏
3)如果被破坏,则进行调整,使得重新平衡

因为新增结点cur是红色.
所以若新增结点的双亲结点parent是黑色就没有违反红黑树的任何性质,不需要做任何调整;
只有新增结点的双亲结点parent是红色才会违反第三条性质,此时需要分情况进行讨论:

在这里插入图片描述

根据具体情况调整如下图所示:
首先约定:cur为当前新插入的结点,grandfather为根节点且是黑色,parent是cur的父节点,uncle是grandfather的另一个孩子

情况一:cur的叔叔结点uncle存在且颜色为红(变色) 情况二可参考反思

  • 调整方法:将parent和uncle的颜色变为黑色,把grandfather的颜色变为红色,再将grandfather赋值给cur,继续向上调整结点颜色即可;若当前cur的parent为root结点,则直接将cur结点变为黑色停止即可。具体颜色变化如下图
    在这里插入图片描述
    在这里插入图片描述

情况三:cur与parent对应位置为左左/右右,uncle节点不存在或存在且为黑(单旋) 情况四可参考反思

注意:下图中的这种情况一定是发生在上面我们讲的第一种情况调整之后,因为如果cur是新插入节点,那这颗树在插入前是不满足各路径黑色节点相同的性质(即原本不平衡),所以cur绝对不可能是新插入的节点。

  • 调整方法:对树以grandfather为轴节点进行旋转,然后将parent颜色变为黑色,grandfather变为红色。具体颜色变化如下图:
    在这里插入图片描述
    在这里插入图片描述

情况五:cur与parent对应位置为左右/右左,uncle节点不存在或存在且为黑(单旋) 情况六可参考反思

  • 调整方法:先以parent为轴节点进行左旋,颜色不改变;然后以grandfather为轴节点进行右旋,调整parent和grandfather的颜色。具体颜色变化如下图
    在这里插入图片描述

在这里插入图片描述

5.红黑树旋转和变色实现的代码

注意: 红黑树中,仍需看情况进行以下四种旋转:(cur与parent的相对位置)
左左——右单旋,右右——左单旋, 左右——左右双旋 ,右左——右左双旋
并且红黑树在单旋后,需要在旋转后变色
红黑树在进行双旋时,第一次旋转后不用变色,第二次旋转后需要变色

不清除旋转过程的,可以参考我在AVL树 中对旋转的讲解,这里就不做赘述了,下面看关于红黑树旋转的代码实现

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)
		{
			parent = cur;
			if (kv.first < cur->_kv.first)
				cur = cur->_left;
			else if (kv.first > cur->_kv.first)
				cur = cur->_right;
			else
				return false;//如果树中已经有该元素,则插入失败
		}

		//找到插入位置,插入节点(cur颜色为红色)
		cur = new Node(kv);
		cur->_col = RED;   //定义cur颜色
		if (kv.first < parent->_kv.first)
		{
			parent->_left = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_right = cur;
			cur->_parent = parent;
		}

		//插入节点成功后,检查红黑树的性质有没有被破坏
		//若被破坏,则要进行节点的颜色调整以满足红黑树性质
		//只有父节点存在且父节点的颜色为红色则需要调整,否则仍然满足红黑树性质,不需要调整
		while (parent && parent->_col == RED)
		{
			// 注意:grandFather一定存在
			// 因为parent存在,且不是黑色节点,则parent一定不是根,则其一定有双亲
			Node* grandfather = parent->_parent;

			//1、父节点是祖父节点的左孩子
			if (grandfather->_left == parent)
			{
				Node* uncle = grandfather->_right;
				//1、叔叔节点存在且叔叔节点的颜色为红色
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

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

				//2、叔叔节点不存在或者叔叔节点的颜色为黑色
				else
				{
					//1、如果cur是parent的右孩子,此时需要进行左单旋将情况转换为情况2
					if (parent->_right == cur)
					{
						RotateL(parent);
						swap(cur, parent);
					}

					//1、如果cur是parent的z左孩子,此时只需进行一个右单旋,并将parent的颜色变为黑,grandparent的颜色置红
					RotateR(grandfather);
					grandfather->_col = RED;
					parent->_col = BLACK;
					break;
				}

			}

			//2、父节点是祖父节点的右孩子
			else
			{
				Node* uncle = grandfather->_left;
				//1、叔叔节点存在且叔叔节点的颜色为红色
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

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

				//2、叔叔节点不存在或者叔叔节点的颜色为黑色
				else
				{
					//1、若是cur为parent的左孩子,先进行一个右单旋转换为情况二一起处理
					if (parent->_left == cur)
					{
						RotateR(parent);
						swap(cur, parent);
					}

					//2、若是cur为parent的右孩子,进行一个左单旋,并将parent的颜色变为黑,grandparent的颜色置红
					RotateL(grandfather);
					grandfather->_col = RED;
					parent->_col = BLACK;
					break;
				}
			}
		}

		//旋转完成之后,将根节点的颜色置成黑色
		_root->_col = BLACK;
		return true;
	}

红黑树实现的完整代码在下篇博客:红黑树的完整实现

7. 红黑树的删除

红黑树是一颗二叉搜索树,毋庸置疑。所以红黑树删除的逻辑,与二叉树删除的实现逻辑一致,就是找一个叶子节点替换当前要删除的结点,然后将叶子节点删除后再次按红黑树的特性进行调整即可。因为红黑树的删除稍微有点复杂,我还没有实现,这里就不详解了。

8.红黑树的验证

红黑树的验证也是较重要的问题,根据前面的内容,我们判断红黑树应该遵循以下几点:

  1. 查看树的根节点是否为黑色
  2. 若当前结点为红色,查看它的孩子是否为黑色
  3. 统计某条路径上的黑结点个数,并且记录count;查看其他路径的黑色结点个数是否与count相等

以下是验证红黑树的代码实现:

bool ISRBtree()
	{
		if (_root->_col == RED)
		{
			return false;
		}
		size_t n = 0;//基准值
		size_t m = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK)
			{
				n++;//统计一条路径上的黑色节点个数
			}
			cur = cur->_left;
		}
		return _ISRBtree(_root, m, n);
	}

	bool _ISRBtree(Node* root, size_t m, size_t n)//递归对每一条路径上的值进行查看是否都相同
	{
		if (root == NULL)
		{
			if (m == n)
				return true;
			else
				return false;
		}

		if (root->_col == BLACK)
		{
			m++;
		}
		if (root->_col == RED && root->_parent && root->_parent->_col == RED)//判断是否存在连续的红色节点
		{
			return false;
		}
		return _ISRBtree(root->_left, m, n) && _ISRBtree(root->_right, m, n);
	}

总结

红黑树一个非常重要的数据结构,并且它的实现很复杂,仅仅从上面一个插入结点的分析就可以看出。
它的优点很明了,同样是平衡二叉搜索树,相对于要求高度平衡的 AVL树来说,红黑树的出现可以大大提高了二叉搜索树的性能。因为二叉搜索树最大的作用就是进行插入、删除、查找的操作,而操作AVL树比操作红黑树要复杂的多。由于AVL树为了保持平衡的性质(左右子树的高度差绝对值不大于一),它在进行插入数据时就要有大量的旋转操作,如果我们的数据量也非常的大,那么我们在进行插入时需要旋转调整这颗树达到平衡的操作也会有很大的开销。相比于AVL树,红黑树在插入新结点时的旋转操作,就少之甚少了。
但有利必有弊,红黑树的查找效率一定会比AVL树低。我们不难看出AVL树的查找时间复杂度为log(n),红黑树的查找时间复杂度为nlog(n)。不过计算机的运算速度是惊人的,每秒的运算量都是亿级的,所以对于log(n)和nlog(n)我们不需要太在意。。
不论怎么说,设计出红黑树的人真是个奇才,他的这一发明为世界创造了更多可能性。现如今红黑树的应用也很广泛,其中C++的STL中,JDK的集合类TreeMap和TreeSet底层实现,还有Java8中的HashMap都应用到了红黑树。
上面仅是我的一些浅析,还有更多内容待发现~~

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值