【C++进阶】心心念念的红黑树,它来了!

本文介绍了红黑树的概念,规则总结,以及在插入操作中的策略,包括颜色选择、旋转调整和代码实现。对比了红黑树与AVL树的区别,强调了红黑树在实际应用中的优势。
摘要由CSDN通过智能技术生成

在这里插入图片描述

👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨


一、红黑树的概念

  • 红黑树是除AVL-tree之外,另一个被广泛运用的平衡二叉搜索树
  • 红黑树比AVL-tree还牛逼。这是因为AVL-tree需要严格遵守平衡因子不超过1的规则;而红黑树是 通过颜色(红/黑)的限制,来达到最长路径不超过最短路径的2,因此并不是严格的平衡,而是近似平衡

二、红黑树的规则总结

  1. 每个结点不是红色就是黑色。
  2. 根节点必须是黑色的。
  3. 如果一个节点是红色的,那么它的孩子结点必须是黑色的(说明任何路径不可能存在连续的红色结点
  4. 每条路径上黑色结点的数量相等。(路径是由根结点到空结点)
  5. 每个空结点NIL都是黑色的。

根据以上规则,一颗红黑树就诞生了

在这里插入图片描述

上图中,红黑树的路径有11条!

三、红黑树的定义

红黑树和AVL-tree都是一个三叉链结构,只是控制平衡的方式不同,红黑树是通过颜色来控制的

在这里插入图片描述

四、新增结点颜色的选择

在红黑树中,新增的默认结点颜色可以选择红色,也可以选择黑色。但是,建议选择红色。

接下来分析为什么选择红色。

首先在红黑树规则中,最重要的只有两条:

  1. 如果一个节点是红色的,那么它的孩子结点必须是黑色的(说明任何路径不可能存在连续的红色结点
  2. 每条路径上黑色结点的数量相等。

如果为新增结点默认为黑色,必然违反每条路径的黑色结点数量相同,并且同时因为这一条路,导致其他路径的黑色结点数量不同,这需要对现有的红黑树进行更多的旋转和重新着色操作,从而导致更大的改动,增加了调整平衡的复杂度。

在这里插入图片描述

如果为新增结点默认为红色,可能违反如果一个节点是红色的,它的孩子结点必须是黑色的,那么需要进行适当调整。当然也可能不需要调整。

在这里插入图片描述

因此,为了尽可能少地改变树的结构,让新结点默认为红色,插入后,不一定调整,但即使调整,也不至于影响全局。

五、插入分析及代码实现

5.1 前言

RB-tree的平衡条件虽然不同于AVL-tree,但同样运用了单旋转和双旋转来调节平衡。

为了方便讨论,可以为某些特殊结点[取别名]

  • 插入的新结点为cur

  • 新结点的父结点为parent

  • 新结点的祖父结点为(父结点的父亲)grandparent

  • 叔叔结点(父结点的兄弟结点)为uncle

通常情况下,我们会 特别关注叔叔结点。具体来说会有以下三种情况:

5.2 uncle存在且为红

当cur插在parent的左边时

在这里插入图片描述

【解决方法】 变色 + 继续向上更新看是否需要调整

变色后

  • 【变色】 parent(父亲结点一定要为黑色)和uncle变黑,grandparent变红。因此在grandparent子树中,变黑是解决当前路径出现连续的红色结点,变红是保证每条路径的黑色结点个数相同。

  • 【继续向上调整】 解决整个树可能出现连续红结点情况(三种):

如果grandparent没有父亲,将grandparent变黑即可。(保证根的颜色是黑的)

在这里插入图片描述

如果grandparent有父亲,且父亲是黑色的,那么不用调整。

如果grandparent有父亲,且父亲是红色的,就要向上进行调整,因为不能出现连续的红色结点。

比如说以下这种:
在这里插入图片描述

此时uncle为红色,并且cur插在parent的右边。虽然插入位置不同,但解决方法还是一样的。

当cur插在parent的右边时

【解决方法】变色 + 继续向上更新看是否需要调整。详细细节可以看看上面的解释说明

【上图的修改】

在这里插入图片描述

【总结】

  • uncle存在且为红,并且无论curparentuncle在左在右。

解决方法都是:

  1. 先将parentuncle变黑,再将grandparent变红
  2. 然后再继续向上调整
  • 如果grandparent有父亲且为黑,则无需向上调整
  • 如果grandparent没有父亲,则grandparent变黑
  • 如果grandparent有父亲且为红,继续向上调整

5.3 当uncle不存在

当uncle不存在于grandparent的右边时

在这里插入图片描述

解决方法:旋转 + 变色

【旋转】 什么旋转是根据cur插入的位置来定的。如果插入在parent的左边,那么就要以grandparent结点进行右单旋;如果插入在parent的右边,就要进行双旋,先左单旋,最后再右单旋。

在这里插入图片描述

【变色】parent变黑,grandparent变红。

在这里插入图片描述

当uncle不存在于grandparent的左边时

【解决方法】旋转 + 变色

在这里插入图片描述

接下来再基于uncle不存在时,看看 【双旋】 是怎么个事:uncle不存在于grandparent的左边时

在这里插入图片描述

解决方法同样是变色

  • 【双旋】:我们在上面说过,对于uncle不存在于grandparent的左边这种情况,并且cur插入在parent的左侧,那么就要进行双旋。首先先对parent进行右单旋;再对grandparent进行左单旋。

在这里插入图片描述

  • 【变色】cur变黑,grandparent变红

在这里插入图片描述

当然了,对于对于uncle不存在于grandparent的右边这种情况,并且cur插入在parent的右侧。这种调整的解决方法同样是双旋 + 变色。双旋是先对于parent左旋转,再对grandparent右旋,最后再将cur变黑以及grandparent变红。由于演示的样例过多,这里就不再演示了hh

【总结】

  • 不管uncle不存在于grandparent的左边或者右边,其解决方法都是旋转 + 变色。

而什么旋转是根据cur插入的位置来定的。

  • 如果插入在parent的左边,那么就要以grandparent结点进行右单旋。然后将parent变成黑色,grandparent变为红色
  • 如果插入在parent的右边,就要进行双旋,先左单旋,最后再右单旋。然后将cur变成黑色(旋转后cur变为根了,根一定为黑),grandparent变为红色

5.4 uncle存在且为黑

来看看一下这种情况
在这里插入图片描述

首先我们需要处理uncle存在且为红的情况,解决方法很简单:parent + uncle变黑 + grandparent变红 + 继续向上更新

在这里插入图片描述

继续向上更新时,就出现了uncle存在且为黑的情况

在这里插入图片描述

解决方法:旋转 + 变色(parent变黑、grandparents变红)

在这里插入图片描述

我们发现:uncle存在且为黑的情况好像和uncle不存在的解决方法是一模一样的,因此我们可以将其归为一类。

六、代码实现

6.1 插入操作

在这里插入图片描述

6.2 左旋和右旋代码

在这里插入图片描述

  • 至于旋转代码的讲解可以参考AVL树的博客:点击跳转

七、验证红黑树

注意:不能使用最长路径(高度)不能超过最短路径的2倍来验证,因为你写的程序有可能会破坏红黑树的规则,比如说你写的红黑树可能会出现连续的红色结点,可能会出现最长路径不会超过最短路径的2倍。我们这里使用红黑树的规则来进行检查。

// backnumber - 用于统计黑色结点的数量
// benchmark - 基准值。此变量是为了求出一条路径的黑色结点个数作为基准值
bool CheckColour(Node* root, int blacknums, int benchmark)
{
	if (root == nullptr)
	{
		// 前序遍历走到空就拿backnumber与基准值benchmark比较即可
		if (blacknums != benchmark)
		{
			return false;
		}
		return true;
	}

	// 2. 每条路径的黑色结点数量相等
	if (root->_col == BLACK) // 遇到黑结点backnumber自增1
	{
		++blacknums;
	}

	// 2. 不可能出现连续的红结点
	// 检查当前结点的颜色和其父亲结点的颜色即可
	if (root->_col == RED && root->_parent && root->_parent->_col == RED)
	{
		cout << root->_key.first << "连续红色结点" << endl;
		return false;
	}

	// 递归检查左子树和右子树
	return CheckColour(root->_left, blacknums, benchmark)
		&& CheckColour(root->_right, blacknums, benchmark);
}

bool _IsBalance(Node* root)
{
	// 根结点为空也算红黑树
	if (root == nullptr)
	{
		return true;
	}
	// 1. 每个结点不是红色就是黑色。(这个不需要验证)
	// 2. 根节点必须是黑色的。
	if (root->_col != BLACK)
	{
		return false;
	}
	
	// 求出某一路径的黑色结点个数
	int benchmark = 0;	
	Node* cur = _root;
	while (cur)
	{
		if (cur->_col == BLACK)
		{
			++benchmark;
		}
		cur = cur->_left;
	}

	// 3. 颜色的检查
	return CheckColour(root, 0, benchmark);
}

八、红黑树与AVL树的比较

红黑树和AVL树都是自平衡的二叉搜索树,它们在维护树的平衡性方面有些不同,因此在不同的应用场景下会有不同的性能表现。

  1. 平衡性:

    • AVL树:AVL树通过保持任意节点的左右子树高度之差不超过1来维护平衡。(严格平衡)
    • 红黑树:红黑树通过保持以五个性质来维护平衡。(近似平衡)
  2. 插入和删除操作:

    • AVL树:AVL树在进行插入和删除操作时,也会通过旋转来调整树的结构并保持平衡。但相比红黑树,AVL树对平衡的要求更加严格,可能需要进行更多的旋转操作。这使得插入和删除操作的时间复杂度略高于红黑树,为O(log n)

    • 红黑树:红黑树在进行插入和删除操作时,只需通过旋转和颜色变换来调整树的结构并保持平衡。这些操作的时间复杂度为O(log n),其中n是树的节点数量。

  3. 查询操作:

    • 红黑树和AVL树在查询操作上具有相同的时间复杂度,都为O(log n)。这是因为它们都是二叉搜索树,具有相似的查找性能。
  4. 存储空间:

    • 红黑树:红黑树通过颜色标记来维护平衡,需要额外存储每个节点的颜色信息,因此在空间上稍微占用更多的内存。
    • AVL树:AVL树不需要额外的信息来维护平衡,因此在空间上相对较小。

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

九、代码

本篇博客我放到gitte仓库了,感兴趣的小伙伴可以自取:点击跳转

对了,关于红黑树的删除操作大家不用担心,因为在面试中一般只会考察插入操作 ~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值