【C++】——红黑树(手撕红黑树,彻底弄懂红黑树)

目录

前言

一  红黑树简介

二  为什么需要红黑树

三  红黑树的特性

四  红黑树的操作

4.1  变色操作

4.2  旋转操作

4.3 插入操作

4.4  红黑树插入代码实现

  4.5   红黑树的删除

五 红黑树迭代器实现

总结


前言

我们之前都学过ALV树,AVL树的本质就是一颗平衡二叉树,它的作用就是查找,插入和删除节点,最坏的时间复杂度都是O(logn)的,同时维护的高度差都是小于等于1的,但是也就是因为这个原因才被红黑树所替代

一  红黑树简介

红黑树是一种自平衡的二叉查找树,是一种高效的查找树。它是由 Rudolf Bayer 于1978年发明,在当时被称为平衡二叉 B 树(symmetric binary B-trees)。后来,在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的红黑树。红黑树具有良好的效率,它可在 O(logN) 时间内完成查找、增加、删除等操作。

二  为什么需要红黑树

对于二叉搜索树,如果插入的数据是随机的,那么它就是接近平衡的二叉树,平衡的二叉树,它的操作效率(查询,插入,删除)效率较高,时间复杂度是O(logN)。但是可能会出现一种极端的情况,那就是插入的数据是有序的(递增或者递减),那么所有的节点都会在根节点的右侧或左侧,此时,二叉搜索树就变为了一个链表,它的操作效率就降低了,时间复杂度为O(N),所以可以认为二叉搜索树的时间复杂度介于O(logN)和O(N)之间,视情况而定。那么为了应对这种极端情况,红黑树就出现了,它是具备了某些特性的二叉搜索树,能解决非平衡树问题,红黑树是一种接近平衡的二叉树(说它是接近平衡因为它并没有像AVL树的平衡因子的概念,它只是靠着满足红黑节点的5条性质来维持一种接近平衡的结构,进而提升整体的性能,并没有严格的卡定某个平衡因子来维持绝对平衡)。还有一点就是,AVL树需要大量的旋转,相比较红黑树来说效率有所减低

     

三  红黑树的特性

首先,红黑树也是一个二叉搜索树,也就是右边大,左边小,红黑树保证最长路径不超过最短路径的二倍,因而近似平衡(最短路径就是全黑节点,最长路径就是一个红节点一个黑节点,当从根节点到叶子节点的路径上黑色节点相同时,最长路径刚好是最短路径的两倍。它同时满足以下特性:

1.根结点肯定树黑色的

2.不能出现连续的红色节点

3.从一节点到叶子节点到所有路径黑色节点的数量上相同的

4.节点的颜色顾名思义不是黑色就是红色

有了上面的认识,我们可以判断下面的图是不是红黑树,乍一看是没有违反规则的,但是实际上是这样吗?

但实际上,在红黑树中真正被定义为叶子结点的,是那些空节点,如下图。

可以看出他们的黑色结点数量并不相等,所以不是一颗红黑树

四  红黑树的操作

红黑树的基本操作和其他树形结构一样,一般都包括查找、插入、删除等操作。前面说到,红黑树是一种自平衡的二叉查找树,既然是二叉查找树的一种,那么查找过程和二叉查找树一样,比较简单,这里不再赘述。相对于查找操作,红黑树的插入和删除操作就要复杂的多。

对于红黑树的操作来说,因为和ALV树是有所区别的,所以旋转的操作是要少于ALV树的,那红黑树肯定就付出了其他的努力去替代这个操作,那就是变色,这也是红黑树的内核所在

4.1  变色操作

什么时候才需要变色呢?

当我们插入一个结点,造成有连续的红节点的时候,变色就是必不可少的了,之所以有连续的红是结点,是因为我们不能插入一个黑色结点,因为插入一个黑色结点会导致黑色结点的数量增多,使得另外的树无法去平衡这这棵树,所以插入黑色结点的代价要远比插入红色结点的代价要大得多

所以我们可以通过变色处理子树红色和黑色结点直接的位置关系,来达到子树本身的平衡



4.2  旋转操作

这里的旋转操作其实和ALV树的旋转是一样的,分为左旋右旋,左右双旋和右左双旋,但是我们这样旋转一般也需要用到变色操作,也就是旋转加变色操作使得红黑树平衡

如果有需要了解旋转的具体实现就看ALV树的旋转

4.3 插入操作

检测新节点插入后,红黑树的性质是否造到破坏?
因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何
性质,则不需要调整;但当新插入节点的双亲节点颜色为红色时,就违反了不能有连在一起的红色节点,此时需要对红黑树分情况来讨论:


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

情况一: cur为红,p为红,g为黑,u存在且为红 

这里向上调整导致,g的父亲结点变成下一个cur

这里可能就有疑问了,为什么单纯把p,u改为黑色,g变成红色??

1.g,p,u都为黑色

2.p 单独变黑色

这两种看起来没什么问题,但是对于1来说,如果这颗树为子树,那么就会多出来一个黑色结点,这是犯了大忌的,所以不可取。对于2来说,也是一样的道理。

所以这里采取的是p ,u 变黑色根节点变红,这样就维持了黑色结点的数量

如果 g 是根节点,那么直接变黑就行了

情况二: cur为红,p为红,g为黑,u不存在/u存在且为黑

对于这种情况单纯的变色已经处理不了了,因为我们无论怎么变色处理,右子树都没有能力使得左右子树的黑色结点数量相同

这里我们的处理就是旋转加变色

1.p为g的左孩子,cur为p的左孩子,则进行右单旋转;相反,
2.p为g的右孩子,cur为p的右孩子,则进行左单旋转
3.p、g变色--p变黑,g变红

注意:这里无论u结点是否存在,都进行一样的操作,进行操作过后都会使得左右子树的黑色结点数量相同,因为在上面的情况讨论中,如果u结点是黑色,那么cur一定是更新上来的,所以cur下面肯定是有黑色结点保持平衡的,所以这里的旋转过后也是平衡的,如果u不存在,那么旋转加变色也是没有任何问题的,可以自己画图模拟一遍

情况三: cur为红,p为红,g为黑,u不存在/u存在且为黑

p为g的左孩子,cur为p的右孩子,则针对p做左单旋转;相反,
p为g的右孩子,cur为p的左孩子,则针对p做右单旋转
则转换成了情况2后面就按 情况2 的来就行了

4.4  红黑树插入代码实现

我们要插入得先找到插入的位置在哪

bool insert(const T& data)
	{
		if (_root == nullptr)//如果头节点为空,直接插到头节点上
		{
			_root = new Node(data);
			_root->_col = BLACK;
			return make_pair(iterator(_root), true);
		}
		Node* parent = nullptr;//如果不是空,那么设置一个当前结点和一个父亲结点去寻找插入的位置
		Node* cur = _root;

		while (cur)
		{
			if (cur->_data < data)//这里和二叉搜索树的情况一样
			{
				parent = cur;
				cur = cur->_right;
			}
			else if(cur->_data>data)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;找到了那么说明存在一个相同的结点,那么直接返回false;
			}
		}

		cur = new Node(data);//走到这里说明找到的位置合适在这里进行插入
		Node* newnode = cur;
		cur->_col = RED;//颜色设置为红色
        //这里还需要判断是在父亲的右还是左,把它和它的父亲结点链接上
        if (parent->_data < data)
		{
			parent->right = cur;
			cur->_parent = parnet;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}

插入结点后,我们就要开始维护红黑树的结构了,这里我们按照上面的情况一 一去模拟就行

1.首先我们需要判断的是父亲结点是否存在,如果存在那它的颜色是什么,从这里开始判断后面的操作,如果不存在或者是黑色那么我们就不用去调整了,因为我们插入的结点是红色

2.如果需要去调整,那我们就应该设置一个祖宗结点,有便于向上调整。

3.判断叔叔结点是否存在且颜色为红还是黑,这关乎到了如果进行调整

4.如果叔叔结点不存在,如果父亲结点在祖宗结点的左边,那么对祖宗结点进行右单旋加变色处理

如果在右边,则相反

5.如果叔叔存在且为黑,和上面一样的判断,在左边,对祖宗结点进行一个右单旋加变色,在右边则对父亲结点进行左单旋,然后再对祖宗结点进行右单旋

6.如果叔叔存在且为红,那么变色就行,把父亲和叔叔结点变为黑色,把祖宗结点变为红色,这样就保持了黑色结点的平衡,如果这里把祖宗结点变为黑色,如果是根节点还可以,如果不是根节点那么就不行,因为多了一个黑色结点

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)
					{
						RotateR(grandfather);
						grandfather->_col = RED;
						parent->_col = BLACK;
					}
					else
					{
						RotateL(parent);
						RotateR(grandfather);
						cur->_clo = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}
			else if (parent == grandfather->_right)//这里就是反过来
			{
				Node* uncle = grandfather->_left;

				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->col = BLACK;
					grandfather->_col = RED;
					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					if (cur = parent->_right)
					{
						RoateL(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						RoateR(parent);
						RoateL(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}
		}
		_root->_col = BLACK;
		return true;


	}

  4.5   红黑树的删除

这里红黑树的删除其实和搜索二叉树那里的删除是差不多的思路,只不过加了红黑树的调整,如果对于搜索二叉树的删除过程忘记了可以参考搜索二叉树详解这篇博客

对于ALV树来说,删除和插入对比起来复杂太多,红黑树就更不用说了,删除一个结点,删除完毕以后还要去调整变色,删除叶子结点还行,如果是删除中间结点那么就会变得异常复杂,想到这里我就不想进行下去了😂😂,了解了解就行,看着都恐怖

五 红黑树迭代器实现

对于树形结构的迭代器来说相比于其他迭代器是要复杂一些的,因为不再是链式结构那样无脑遍历了

首先我们遍历二叉树是采取的中序遍历(左子树,根,右子树),所以我们得先想清楚遍历情况

我们在进行迭代器的++的时候,考虑的是该结点的是否存在右子树,因为我们位于一个结点上,说明左子树已经遍历完了,如果说右子树存在,那么就去找右子树的最左结点

如果右子树不存在,再++则需要看这课子树是不是遍历完了,如果当前结点的父亲结点右指针是当前结点,说明这个子树完了,则需要往上调整,继续判断这种情况

这张图很显然在根的左子树上,可以看到现在以1为结点的子树遍历完了,所以应该遍历根,再然后进入右子树找最左结点继续上面的遍历

如果上面不是很理解那么就看代码理解一遍

	Self& operator++()
	{
		if (_node->_right)
		{
			// 下一个就是右子树的最左节点
			Node* cur = _node->_right;
			while (cur->_left)
			{
				cur = cur->_left;
			}

			_node = cur;
		}
		else
		{
			// 左子树 根 右子树
			// 右为空,找孩子是父亲左的那个祖先
			Node* cur = _node;
			Node* parent = cur->_parent;
			while (parent && cur == parent->_right)
			{
				cur = parent;
				parent = parent->_parent;
			}

			_node = parent;//这个时候就是第二张图,此时_node应该指向的是父亲结点,因为下一个
                           //遍历的结点是根结点
		} 

		return *this;
	}

那我们在进行--操作其实代码和++是一样的, 这里可以想一下,我们++是怎么操作的,比如我们++要找下一个结点,那么就会找右子树的最左结点,其实最后找到的是右子子树的最右结点

比如这里的27号结点,当我们再++就往回退了。

那现在我们看这张图,6结点++以后到1,再++到8,那么我们--就需要到1,那么就和++是一样的,先判断该节点的左子树是否存在,然后找左子树的最右结点,然后退无可退的时候就往回返了

	Self& operator--()
	{

		if (_node->_left)
		{
			//下一个是左子树的最右节点
			Node* right = _node->_left;
			while (right->_right)
			{
				right = right->_right;
			}
			_node = right;
		}
		else
		{
			//孩子不是父亲的左的那个祖先
			Node* parent = _node->_parent;
			Node* cur = _node;
			//如果是父亲的右子树,就一直往上走
			//如果parent已经为空,那么就停止循环,parent已经到达了我们的end的位置
			while (parent && cur == parent->_left)
			{
				cur = cur->_parent;
				parent = parent->_parent;
			}

			_node = parent;
		}
		return *this;
	}

总结:这里可能比较绕,时间久了不记得了,我们只需要知道++的时候是往右边走,也就是找右子树的最左节点,--的时候往左边走,找左子树的最右结点,如果到底了就往回返

这里开始的begin()就是最左结点,end是空,因为我们一直++一定会返回到根,根的父亲为空

总结

红黑树这里其实插入操作也不是很难,迭代器有点绕,多结合图去理解代码!!!

评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值