【C++笔记】红黑树的简易实现

【C++笔记】红黑树的简易实现

  • 一、什么是红黑树以及红黑树好在哪里
    • 1.1、什么是红黑树
    • 1.2、红黑树比AVL树好在哪里?
  • 二、红黑树的模拟实现
    • 2.1、红黑树的插入
    • 2.2、仅变色调整
    • 2.3、变色+单旋调整
    • 2.4、变色+双旋调整

一、什么是红黑树以及红黑树好在哪里

1.1、什么是红黑树

红黑树本质上也是一颗搜索二叉树,但它在搜索二叉树的规则上有新添了一些额外的规则,使得它比普通的搜索二叉树甚至是AVL树的性能更好。

简单来说红黑树是这样一棵树:红黑树是一棵搜索二叉树,它的每个节点上增加了一个存储位来表示每个节点的颜色,颜色要么是红色要么是黑色。并且树中没有连续的红色节点,且红黑树中确保没有任何一条路径长度会大于其他路径的两倍。

红黑树有以下几个主要特性:

1. 每个结点不是红色就是黑色
2. 根节点是黑色的
3. 如果一个节点是红色的,则它的两个孩子结点是黑色的(即没有连续的红节点)
4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均 包含相同数目的黑色结点

大家可能看完会感到疑惑,上面的特性中好像并没有与路径长度相关的啊,那怎么保证上面说到的确保没有任何一条路径长度会大于其他路径的两倍呢?

其实这个条件我们并不需要特意的去关注,因为只要满足了特性3和特性4,这个条件自然就满足了。

我们先来看特性4中所说的任意路径的黑色节点数量都一样,这个其实已经在一定程度上限制了每条路径的长度了,因为如果某棵红黑树中都是黑色节点的话,那么每一条路径的长度都是一样的,比如说下面这棵树:
在这里插入图片描述
想想就知道,上面这棵树一定是满足红黑树的条件的。

那么在每条路径的黑节点数量都相同的情况下,要加入红节点来使路径变长,应该怎么加才能使路径最长呢?

由于特性3的限制,即不能有连续的红节点,并且根节点一定是黑节点,我们要使一条路径最长的唯一添加方式就只有像下面这样:
在这里插入图片描述
只能这样一直红黑相间的加,并且到最后红黑节点的数量一定是相同的,即最长的路径是每条路径黑色节点的两倍。
而每条路径的黑色节点数量都是相同的,所以若是存在最短路径(这里的最短路径不是针对某一棵红黑树,而是在合法范围内能达到的最短路径),那么这条路径上的节点一定是全黑的。

所以这也就满足了最长路径不超过最短路径的二倍的条件了。

最后还有一点是需要特别注意的: 红黑树的路径定义其实和其他树的路径的定义不一样,其他树的路径定义都是由根节点到叶子结点,而红黑树的路径定义则是由根节点到空。

我这里先说原因,也就是为什么需要这样,然后再说如果不这样的话会导致的一个问题。

原因:
因为红黑树并不像AVL树一样使用一个平衡因子来控制整棵树的高度,所以如果红黑树的路径定义和其他树一样就会出现下面这种极端情况:
在这里插入图片描述
如果路径定义的时由根节点到叶子结点,那么这棵树的高度就有可能向上面一样变得很高,但是节点的数量并不多,并且只要路径中的黑色节点相同,它有可能一直延伸下去:
在这里插入图片描述
如果树的高度变得很高,我们的查找效率就会变低,这时候如果将路径定义成由根节点到空的话就能很好的规避这种情况了:
在这里插入图片描述
这样的话,上图红线标出来的这条路径就不合法了,就需要调整。

同时这样定义其实也是怕我们判断错误,如果路径的定义还是像其他二叉树一样,那我们就会认为上图的这棵树并没有出错的地方,就判断错了。

1.2、红黑树比AVL树好在哪里?

但是红黑树到底比AVL树好在哪里呢?

我们知道AVL树是是通过平衡因子来控制右子树与左子树的高度差不超过1的,既然高度差不超过1,那说明AVL树是一棵近似完全二叉树甚至是满二叉树的树,是一个严格平衡的树:
在这里插入图片描述
虽然它的高度控制的很好,但是我们在控制高度的时候需要进行各种旋转操作,在旋转的过程中还需要进行很多的判断,这相对于查找的判断确实多了很多。

因为AVL树对于高度的严格控制,在一些极端情况下我们可能没插入一个节点就需要旋转一次,这其实是很费时间的。

而红黑树对于高度的控制没有AVL树一样严格,这其实就省去了很多旋转的开销,虽然它的高度差较与AVL树更大,但也只是查找的层数多一点而已,相对于AVL树需要进行繁琐的旋转,旋转中还有很多的判断,显然查找更多层其实也并不赖。

所以总体而言,红黑树较于AVL树还是有一点优势的,虽然优势并不是很大,但好过没有。

二、红黑树的模拟实现

红黑树节点:

// 颜色我们用枚举来定义,比较形象一点
enum Color {
	RED,
	BLACK
};


// 红黑树节点
template <class T>
struct RBTreeNode {
	RBTreeNode<T>* _left;
	RBTreeNode<T>* _right;
	RBTreeNode<T>* _parent;
	T _val;
	Color _col;
	// 构造
	RBTreeNode(const T& val)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _val(val)
		, _col(RED) // 新增节点要给红色
	{}
};

因为红黑树也是要旋转的,所以和AVL树一样,也需要用到三叉链。

然后我们同样只需要在红黑树类里面封装一个根节点即可:

template <class T>
calss RBTree{
public :
	typedef RBTreeNode<T> Node;
	// …………
private :
	Node* _root;

}

因为红黑树的删除操作实在太过繁琐,而且也在考察范围,所以我们这里就只实现红黑树的插入操作即可。

2.1、红黑树的插入

红黑树的本质是一颗二叉排序树,所以和普通的二叉排序树一样,还是需要先找到插入的位置。

但是,红黑树有红色和黑色两种颜色,我们在插入新节点的时候,新节点是选择红色还是黑色呢?

其实这个问题好好想一下就知道了,因为红黑树最主要的特性就是每条路径的黑色节点数量相同,如果新增节点选择黑色,那么这就会导致有一条路径的黑色节点数量不同了,这影响到的棵树整棵树啊!这样的话,需要再次调整的话就需要对整棵树进行调整了,非常麻烦。

所以我们新增的节点一律选择红色,如果后面需要调整的话,调整的幅度也不会太大。

先给出查找插入位置和插入逻辑的代码,这和普通的搜索二叉树一样:

// 插入
bool insert(const T& val) {
	if (nullptr == _root) {
		_root = new Node(val);
		// 根节点一定要给黑色
		_root->_col = BLACK;
		cout << "insert->" << val << endl;
		return true;
	}
	// 先插入新节点
	Node* cur = _root;
	Node* parent = nullptr;
	while (cur) {
		if (val < cur->_val) {
			parent = cur;
			cur = cur->_left;
		}
		else if (val > cur->_val) {
			parent = cur;
			cur = cur->_right;
		}
		else {
			return false;
		}
	}
	cur = new Node(val);
	if (val < parent->_val) {
		parent->_left = cur;
	}
	else {
		parent->_right = cur;
	}
	cur->_parent = parent;
}

在插入新节点后我们还需要检查一下是否需要进行调整,也就是检查一下红黑树的结构是否被破坏了。

有一种情况是最轻松的,也就是结构没有被破坏,当我们插入的新节点的父亲是黑色的时候:
在这里插入图片描述
这时候其实并不用管,新节点插入在父亲的左边还是右边,因为都是一样的,我们直接返回即可。

// 检查是否需要调整
	Node* grandfather = parent->_parent;
	Node* uncle = nullptr;

	if (parent->_col == BLACK) {// 如果父亲是黑色,则不用调整,直接返回
		return true;
	}

2.2、仅变色调整

还有些情况是需要进行变色或旋转调整的,当新插入节点的父亲节点是红色的时候,我们是无论如何都需要调整的,但有些调整比较轻松,有些调整就比较复杂,有可能是只需要变色即可,有可能是需要变色加上旋转调整。

我们这里还是先从轻松地说起,即仅需要变色的情况。
在这里插入图片描述
如上图,当新节点的父亲是红色,且叔叔节点(uncle)即父亲的兄弟节点“存在”且为红的时候,grandfather为黑(此时的grandfather已定位黑),我们就仅需要变色处理即可。
具体的变色方案是:

将parent和uncle都变黑,将grandfather变红

在这里插入图片描述
这时候

但是我们还不能直接结束,因为上面所画的只是一个情况,也有可能当我们将grandfather变红之后发现grandfather的父亲也是一个红节点(因为当前的祖父节点不一定是根),这时候就有违反规则了。

所以我们还需要继续向上调整,我们画出个抽象图可能更好理解:
在这里插入图片描述
如上图,当cur的父亲节点§为红色,并且父亲节§点和叔叔节点(u)都为红色时,就需要进行上述变色调整,调整完当前层的时候还需要检查上一层。

只需要将g当成新的cur,然后一直迭代的进行判断。

但是这样调整为什么就能解决问题了呢?
这是因为,节点g其实是p和u所在的这两条路径“共用”的:
在这里插入图片描述
所以这样调整后看似是减少了两个黑色节点只增加了一个黑色节点,好像是少了一个黑色节点,但是从路径中的黑色节点数量来看,其实是并没有减少的。

注意:再向上层迭代的过程中,也有可能出现其他的情况,有可能他还是只需要变色,有可能是需要变色+旋转调整,所以还需要结合下面讲到的调整方式相结合。

2.3、变色+单旋调整

cur为红,p为红,g为黑,u不存在/u存在且为黑时,我们就不仅仅需要变色了,还需要进行旋转调整,具体是单旋还是双旋还需要看情况。

首先先把原理讲清楚,就是为什么这样的情况需要旋转。
首先当u节点不存在时,此时的cur必定是新增节点:
在这里插入图片描述
并且p节点在增加cur节点之前也一定是没有孩子的,因为p已经是红色了,而红节点的孩子必须是黑节点,所以如果p有孩子节点,就破坏了红黑树的规则了。

此时无论怎样变色都不不能解决问题的,因为现在的最长路径已经超过了最短路径的两倍了,只能通过调整高度来解决问题了。

如果p是g的左孩子并且cur也是p的左孩子就要进行右单旋:
在这里插入图片描述

调整完后,变色的方案是,p变黑g变红。

而如果p是g的右孩子且cur是p的右孩子,就要进行左单旋。

但为什么当u节点存在且为黑的时候也需要进行旋转呢?
首先可以确定的是,当存在且为黑的时候,cur一定不是新增节点,因为如果cur实现增节点的话,那在新增cur之前整棵树就不符合红黑树的规则了:
在这里插入图片描述
所以cur原来一定是黑节点,是下层调整上来的时候变红的,本质是少了一个黑节点。
并且子树abcde也一定是不为空的,不然就不符合规则了:

在这里插入图片描述

那我们就来细细分析一下,首先cur是从下面调整上来变成的红色,所以cur这可子树一定是符合规则的,所以cur就不能再变色了,再变色的话不就是走回头路了吗。
同时p节点也是不能变色的,因为如果p变为黑色,看似是不上了之前少了的那个黑节点,但是子树c的每条路径上又多出了一个黑节点,也是不符合要求了。

在这里插入图片描述
同时u更是不能变色的啦,及解决不了问题还会使g的右子树都少了一个黑色节点,同理g也不能变色。

所以我们就只能通过旋转来解决问题了:
在这里插入图片描述
然后我们观察发现,ab子树所在的路径都少了一个黑节点,c子树的黑色节点是不变的,所以变色方案也是和上面一样:p变黑,g变红:
在这里插入图片描述
同样左单选的情况也是类似的分析。

先给出左单旋和右单选的代码:

// 左单旋
void RotateL(Node* parent) {
	Node* subRight = parent->_right;
	Node* subRightL = subRight->_left;
	Node* parent_parent = parent->_parent;

	parent->_right = subRightL;
	if (subRightL) {
		subRightL->_parent = parent;
	}
	subRight->_left = parent;
	parent->_parent = subRight;

	if (parent == _root) { // 如果当前的parent是根
		_root = subRight;
		subRight->_parent = nullptr;
	}
	else {
		// 如果当前的parent还不是根
		if (parent == parent_parent->_left) {
			parent_parent->_left = subRight;
		}
		else {
			parent_parent->_right = subRight;
		}
		subRight->_parent = parent_parent;
	}

}

// 右单旋
void RotateR(Node* parent) {
	Node* subLeft = parent->_left;
	Node* subLeftR = subLeft->_right;
	Node* parent_parent = parent->_parent;

	parent->_left = subLeftR;
	if (subLeftR) {
		subLeftR->_parent = parent;
	}
	subLeft->_right = parent;
	parent->_parent = subLeft;

	if (parent == _root) { // 如果当前的parent为根
		_root = subLeft;
		subLeft->_parent = nullptr;
	}
	else {
		// 如果当前的parent不为根
		if (parent == parent_parent->_left) {
			parent_parent->_left = subLeft;
		}
		else {
			parent_parent->_right = subLeft;
		}
		subLeft->_parent = parent_parent;
	}
}

其实就是复制AVL树的左单旋和右单旋。

2.4、变色+双旋调整

然后就到了双旋的情况了,其实红黑树的双旋和AVL树的双旋是差不多的,只是多了变色这一步而已。
如下图,当p为g的左孩子并且cur为p的右孩子时,我们使用单旋是解决不了问题的:
在这里插入图片描述
此时我们可以像AVL树一样先对p进行左单旋,将其转化成一个单纯的右单旋的情况:
在这里插入图片描述
此时我们将cur和p调换位置,不就变成了和上面单旋处理一样的情况了吗?
在这里插入图片描述
所以我们再针对g做一次右单旋不就解决高度不平衡的问题了吗?
在这里插入图片描述

完整过程:

在这里插入图片描述
此时的变色方案就和单选的有所不同了,是将cur变为黑色,g变为红色:
在这里插入图片描述
为什么不同其实也很好理解:


如上图,我们在对g进行左单旋后是变成了上图左端的形式,我们其实是将p“看作”是cur来对g进行单旋的,所以我们可以认为是p和cur调换了位置。

所以在单选和双旋中,g都变成了红色,但是变黑色的却分别是p和cur,带着这样的理解我们其实会发现单选和双旋其实本质也是一样的。

最后给出完整的插入代码:

// 插入
bool insert(const T& val) {
	if (nullptr == _root) {
		_root = new Node(val);
		// 根节点一定要给黑色
		_root->_col = BLACK;
		cout << "insert->" << val << endl;
		return true;
	}
	// 先插入新节点
	Node* cur = _root;
	Node* parent = nullptr;
	while (cur) {
		if (val < cur->_val) {
			parent = cur;
			cur = cur->_left;
		}
		else if (val > cur->_val) {
			parent = cur;
			cur = cur->_right;
		}
		else {
			return false;
		}
	}
	cur = new Node(val);
	if (val < parent->_val) {
		parent->_left = cur;
	}
	else {
		parent->_right = cur;
	}
	cur->_parent = parent;



	// 检查是否需要调整
	Node* grandfather = parent->_parent;
	Node* uncle = nullptr;

	if (parent->_col == BLACK) {// 如果父亲是黑色,则不用调整,直接返回
		return true;
	}
	else {
		while (parent && parent->_col == RED) {
			grandfather = parent->_parent;
			if (parent == grandfather->_left) {
				uncle = grandfather->_right;
			}
			else {
				uncle = grandfather->_left;
			}
			// 如果uncle存在且为红
			if (uncle && uncle->_col == RED) {
				parent->_col = uncle->_col = BLACK;
				grandfather->_col = RED;
				cur = grandfather;
				parent = cur->_parent;
			}
			else { // 如果uncle不存在或者存在且为黑
				if (parent == grandfather->_left) {
					if (cur == parent->_left) { // 右单旋
						RotateR(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;

					}
					else { // 左右双旋
						RotateL(parent);
						RotateR(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
				}
				else {
					if (cur == parent->_right) { // 左单旋
						RotateL(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else { // 右左双旋
						RotateR(parent);
						RotateL(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
				}

				break;
			}
		}
	}

	// 再次处理,根一定要是黑色
	_root->_col = BLACK;
	return true;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/201154.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

优化邮件群发效果的方法与策略

怎样优化邮件群发效果&#xff1f;这是许多企业在进行邮件营销时常常被问到的问题。邮件营销是一种高效且经济实惠的市场推广方式&#xff0c;但如何使邮件真正引起接收者的兴趣并产生预期的效果并不容易。好的营销效果可以带来高回报、高收益率&#xff0c;但是怎么提升群发效…

零代码连接钉钉宜搭与用友U8,让业财数据管理简单高效

零代码连接钉钉宜搭与用友U8&#xff0c;让业财数据管理简单高效 如果把企业内部的业务系统比作一条条河流&#xff0c;那么它们的交汇点就像江河湖海。在这些交汇点上&#xff0c;数据的汇集、分析和共享离不开系统之间的集成。 钉钉宜搭和用友U8是两个在企业中非常重要的系统…

端口隔离度

端口隔离度 隔离度为&#xff08;本振或射频信号&#xff09;泄漏到其他端口的功率与输入功率之比&#xff0c;单位是dB。 比如 RF to LO Isolation 表示 射频输入信号的功率 与 泄漏到LO端口的功率 之比。 而 LO to RF Isolation 则表示 本振输入信号的功率 与 泄漏到RF端口的…

mvn 编译时报错 java heap space

问题描述 使用IDEA进行war打包时&#xff0c;编译类都正常&#xff0c;但是最后生成 war 包时很慢&#xff0c;有些时候还会报错&#xff1a; java head space。具体错误如图&#xff1a; 问题诊断 换电脑&#xff0c;可行清理 .idea 目录重新打包还是不行升级 maven-war-plu…

Linux 基本语句_13_消息队列

概念&#xff1a; 不同进程能通过消息队列来进行通信&#xff0c;不同进程也能获取或发送特定类型的消息&#xff0c;即选择性的收发消息。 一般一个程序采取子进程发消息&#xff0c;父进程收消息的模式 常用函数功能&#xff1a; fork(); // 创建子进程 struct msgbuf{ …

操作系统(七)| 设备管理-- 端口 驱动程序 基本I/O控制 磁盘I/O

系列文章如下 学习过程中一定要有系统观念&#xff08;知识框架&#xff0c;每一章开头都会有一个思维导图&#xff09;&#xff0c;知道目前自己在学习的是哪一板块的内容&#xff0c;和前面有什么样的联系 操作系统的很多知识点前后都是联系非常紧密的&#xff0c;去一点一…

【Openstack Train安装】十、Neutron安装

Neutron&#xff0c;是Openstack中的一大核心组件&#xff0c;设计目标是实现“网络即服务&#xff08;Networking as a Service&#xff09;”。为了达到这一目标&#xff0c;在设计上遵循了基于 SDN 实现网络虚拟化的原则&#xff0c;在实现上充分利用了 Linux 系统上的各种网…

vs配置64位汇编

vs开发64位程序无法使用内联汇编&#xff0c;需要将汇编放到一个单独文件中编译链接。 步骤如下&#xff1a; 生成汇编代码。以asm.asm为例&#xff0c;以下是模板&#xff1a; ;64位汇编程序模板 (Template) ;声明一个ExitProcess函数 ExitProcess PROTO.data;在这里声明变量…

一文1000字彻底搞懂Web测试与App测试的区别

总结分享一些项目需要结合Web测试和App测试的工作经验给大家&#xff1a; 从功能测试区分&#xff0c;Web测试与App测试在测试用例设计和测试流程上没什么区别。 而两者的主要区别体现在如下几个方面&#xff1a; 1 系统结构方面 Web项目&#xff0c;B/S架构&#xff0c;基…

Python异常处理:try语句的应用与技巧

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 异常处理在Python中是至关重要的。try-except是用于捕获和处理异常的核心机制之一。让我们深入了解如何使用try-except&#xff0c;处理各种异常情况。 try-except语句 在编程中&#xff0c;异常是指运行时发生…

Python并发编程之进程间通信

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 进程间通信是并发编程中一个重要而复杂的主题。在多任务处理时&#xff0c;多个进程之间需要共享信息、数据和资源。在并发环境下&#xff0c;进程之间的协作和通信至关重要&#xff0c;以便能够安全地共享数据&…

AMIS【部署 01】amis前端低代码框架可视化编辑器amis-editor本地部署流程

amis-editor本地部署流程 1.amis-editor是什么1.1 amis是什么1.2 amis-editor是什么 2.amis-editor本地部署2.1 准备阶段2.2 源码修改2.3 构建项目2.4 nginx配置2.5 启动nginx 3.总结 官网仅贴出了本地运行这个项目的步骤&#xff1a; # 1.安装依赖 npm i # 2.等编译完成后本地…

30岁+项目经理和PMO少奋斗10年的职业规划路线

大家好&#xff0c;我是老原。 很多项目经理小白出来工作遇到困惑时又以得过且过的态度拒绝了别人的指导和建议&#xff0c;磨磨蹭蹭的就到了30岁。 大多数人会感到迷茫的原因&#xff0c;是因为对自己要往什么方向发展&#xff1f;做什么样的事情毫无计划和想象。 为什么会…

Docker,从入门到精通

1、DockerFile 介绍 dockerfile 是啥?dockerfile 用来构建 docker 镜像的文件。 具体步骤&#xff1a; 1、编写一个 dockerfile 文件 2、docker build 构造一个镜像 3、docker run 运行镜像 4、docker push 发布镜像 DockerFile 构建过程 1、每个保留关键字都必须是大…

数字图像处理(实践篇)十三 数据增强之给图像添加噪声!

目录 一 涉及的函数 二 实践 一 涉及的函数 skimage.util.random_noise( ) skimage.util.random_noise(image, modegaussian, seedNone, clipTrue, **kwargs) 函数的功能&#xff1a;为浮点型图片添加各种随机噪声。 输入&#xff1a; ①image&#xff1a;输入图像&…

react-virtualized报bpfrpt_proptype_WindowScroller引入错误

背景 vite构建阶段react-virtualized报错 报错信息 ✘ [ERROR] No matching export in "node_modules/_react-virtualized9.22.5react-virtualized/dist/es/WindowScroller/WindowScroller.js" for import "bpfrpt_proptype_WindowScroller"node_module…

微信开发者代码管理删除项目

微信开发者代码管理删除项目 1、打开 微信开发者代码管理平台&#xff0c;选择项目&#xff0c;显示个人用户下的项目 2、点进项目里面&#xff0c;选中设置 3、进入设置页面 4、选择高级设置–> 仓库设置 5、选中删除项目 6、删除页面 这样就 OK 了

[Python入门系列之十二]安装Jupyter notebook与代码运行

引言 Jupyter Notebook将代码、图片和文本完美结合在一起&#xff0c;为编程学习带来了前所未有的便捷性。本文旨在为初学者提供一个关于Jupyter Notebook的入门指南。 什么是Jupyter Notebook Jupyter Notebook是一个开源的Web应用程序&#xff0c;允许你创建和共享包含代码…

删除排序链表的重复元素I和II,多种解法和思考

删除排序链表的重复元素I https://leetcode.cn/problems/remove-duplicates-from-sorted-list/description/ 一个循环就可以了&#xff0c;如果当前节点和下一个节点值一样&#xff0c;当前节点不移动让next后移动一个&#xff0c;如果不一样则当前节点后移。 一个循环就可以…

python 生成器的作用

1. 生成器 参考&#xff1a; https://www.cainiaojc.com/python/python-generator.html 1.1. 什么是生成器&#xff1f; 在 python 中&#xff0c;一边循环一边计算的机制&#xff0c;称为生成器&#xff1a;generator. 1.2. 生成器有什么优点&#xff1f; 1、节约内存。p…