【红黑树 -- 理论与实现】

news2024/11/26 4:49:24

前言

打怪升级:第88天
在这里插入图片描述

红黑树,可以说是树中的绝对大佬了,它和我们前一篇讲解的avl树一样,都属于二叉排序树
avl树中我们通过记录平衡因子以及旋转来保证一棵树的绝对平衡,而今天所讲的红黑树则是通过给各个节点添加颜色属性来保证一棵树的平衡,那么下面我们就一起揭开红黑树神奇的面纱吧~。

注:三叉树的旋转操作在avl树中进行了讲解,此篇文章不再赘述,有需要的朋友可以跳转观看。

红黑树的概念

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

在这里插入图片描述

红黑树的性质

  1. 每个节点不是红色就是黑色;
  2. 根节点是黑色的
  3. 红色节点两个孩子都是黑色
  4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均 包含相同数目的黑色结点
  5. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点);

通过上面的五条性质我们可以得到那些信息呢?

  1. 红色节点的孩子只能是黑色的,黑色节点的孩子可以是红色也可以是黑色
  2. 从一个节点到其所有后代节点的路径上,均包含相同数目的黑色节点
    从这里我们是否可以得出最长路径与最短路径?
    – 答案是可以的,
    最短路径:全部都是黑色节点
    最长路径:每个黑色节点之间插入一个红色节点
    那么从这里我们就论证了红黑树的概念描述:没有一条路径会比其余路径长出两倍。
  • 红黑树节点的定义
enum Color  // 颜色枚举
{
	RED,
	BLACK
};

// 1. 节点 -- 三叉链
template<class T>
struct _RBTree_Node
{
	_RBTree_Node(const T& val)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _val(val)
		, _col(RED)
	{}

	typedef _RBTree_Node<T> Node;
	Node* _left;
	Node* _right;
	Node* _parent;
	T _val;
	Color _col;
};

提一个小问题:节点初始化时为什么默认是红色的,黑色的可以吗?
– 要回答这个问题,我们就需要从五条性质出发来对比初始化为红色和黑色的影响;
首先我们看到,对于性质1,2,5都是没有任何影响的,
那么对于性质3:默认为红色,如果新插入节点的父节点是黑色,这次就插入成功了,但是如果新插入节点的父亲也是红色就会产生冲突,我们需要改变这两个节点的颜色,具体的下面我们会分析;
默认为黑色则不会出现两个节点都为红色的情况;
对于性质4:默认为红色,不会影响黑色节点的数量;
默认为黑色,那么每次新增节点都会影响这个分支上的黑色节点个数,我们每次都需要进行调整,使得各个支路黑色节点个数相同。

由此我们决定选择调整次数少的情况 – 也就是默认为红色节点的初始化方案。


插入过程遇到的情况

情况1 – 根节点

在这里插入图片描述

此时是一个空树,插入节点就是这棵树的根节点,插入结束后就要更改节点的颜色,保证根节点为黑色。

情况2 – parent为黑色

在这里插入图片描述

情况3 – parent为红色

当parent为红色的时候我们就要更改新增节点(cur)或者parent的颜色,以满足红黑树的性质3,
那么,我们到底是改变cur的颜色还是改变parent的颜色呢?
假设我们改变cur,那么此时和我们直接将节点初始化为黑色岂不是一样的,我们初始化为红色就没有意义了,
因此我们要来改变parent的颜色,把它变为黑色后这个分支好像又多了一个黑色节点,
那么我们想要让它减少一个黑色节点我们是不是就要进行往上调节啊,
我们再把在这里插入图片描述节点(爷爷 ,grand)改为红色,
如果这样改过之后该分支黑色节点该好了,但是,grand的另外一棵子树上就都少了一个黑色节点,
现在,我们还要让叔叔节点(父亲的兄弟 , uncle)变为黑色即可,
说着很简单,但是谁说叔叔节点就一定是红色的,而且,叔叔存不存在还两说呢;
所以在这里就分出了很多中情况;


我们来理一理上面的思路:
1.parent为红色,那么我们一定有grand,因为根节点一定为黑色,parent为红色就一定不是根节点,所以一定存在祖先,并且祖先一定是黑色节点;
2.但是我们不一定有uncle,因此这里需要判断的就是nucle不存在、nucle为红色、以及uncle为黑色三种情况;
下面我们逐个分析。

uncle为红色

变色放案:新增节点不变色,parent与uncle变为黑色,grand变为红色;

上方变色方案走过一轮后grand的左右子树都调整结束了,但是grand变成了红色,我们就需要判断grand的parent是否为红色,继续往上走。。。

注:下方示例特殊了一些,往上迭代时uncle不一定还是红色的。
在这里插入图片描述

uncle为黑色

我们的变色规律为:parent、uncle变为黑色,uncle变为红色,当uncle也为红色的情况下我们可以保证各个分支的黑色节点个数不变,
但是,当uncle为黑色时,再进行变色就会使得nucle分支的黑色节点个数减少,如下图第二次变色:
那么为了使parent与uncle分支节点个数相同我们就需要保持parent不变,uncle分支黑色加一,
或者uncle分支不变,parent黑色节点个数减一,
但是我们就是从第二种情况下变换过来的,因此第二种丢弃,选择使uncle分支黑色节点加一,
这里我们就需要用到旋转,由于左边黑色节点个数多,我们就进行右旋

情况1:
在这里插入图片描述
上方,我们之所以进行右旋,是因为左边的黑色节点个数多,因此我们就需要判断到底是哪个子树黑色节点多,也就是parent到底在grand的左边还是右边

情况2:
那么下方这种情况,我们看到parent在grand的右边,右边黑色节点多,因此我们进行一次左旋,
但是左旋之后我们并没有得到预期的结果,如果大家继续按照“变色,旋转”的方法往后画会发现这是一个死循环,这里就不再一一画出。
在这里插入图片描述
那么为什么会出现这种情况,这里和上方第一种情况有何不同呢? – cur 与parent的相对位置,与 parent与grand的相对位置不同!
第一种:cur为parent的左,parent为grand的左,因此进行旋转后parent变为新的根,cur与grand两个红色节点分别在他两侧;
上方这种:cur为parent的左,parent为grand的右,经过旋转后parent变为了新的根,但是cur和grand两个红色节点又连在了一起,
那么我们就需要避免这种情况的发生,如果我们将这种情况转换为第一种情况就可以将两者分开了,所以我们要对parent进行一次右旋,使得新的cur是parent的右,新的parent成为grand的右。
如图:
在这里插入图片描述
当出现双旋的情况时我们就要改变变色方案了,至于怎么更改就需要我们旋转之后往可以达到颜色平衡的情况下改变。

下方为单旋的详细流程图:
在这里插入图片描述

uncle不存在

uncle不存在的情况和uncle为黑的分析情况完全相同 – 变色方案是要将uncle变为黑色,而uncle不存在与uncle本就为黑时uncle都不需要改变,都会使得uncle分支少一个黑色节点。

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


插入过程代码实现

		// 插入
		bool Insert(const T& val)
		{
			// 空树
			if (_root == nullptr)
			{
				_root = new Node(val);
				_root->_col = BLACK; // 根节点为黑色
				++_node_const;
				return true;
			}

			// 查找是否存在
			Node* parent = nullptr;
			Node* cur = _root;
			while (cur)
			{
				if (cur->_val < val)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (cur->_val > val)
				{
					parent = cur;
					cur = cur->_left;
				}
				else
					return false; // 插入失败
			}

			cur = new Node(val);
			if (parent->_val < val)
				parent->_right = cur;
			else
				parent->_left = cur;
			cur->_parent = parent;
			++_node_const;

			// 调整节点
			while (parent && parent->_col == RED)
			{
				Node* grand = parent->_parent;
				Node* uncle = nullptr;
				if (parent == grand->_left)
					uncle = grand->_right;
				else
					uncle = grand->_left;
				
				if (uncle && uncle->_col == RED) // uncle 存在且为红色,uncle与parent变为黑色,grand变为红色
				{
					// 更新颜色
					uncle->_col = parent->_col = BLACK;
					grand->_col = RED;
					//  继续向上更新节点
					cur = grand;
					parent = grand->_parent;
				}
				else // uncle不存在 或者 uncle为黑色 ,依然采用上方的变色方案,特殊的是要进行旋转 -- 因为uncle分支黑色节点减少了
				{
					
					// 判断旋转策略
					if (cur == parent->_left)
					{
						if (parent == grand->_left) // 右旋
						{
							//       g
							//    p     u
							// c
							RotateR(grand);
							parent->_col = BLACK;
							grand->_col = RED;
						}
						else // 右左双旋
						{
							//       g
							//   u       p     
							//        c
							RotateR(parent);
							RotateL(grand);
							cur->_col = BLACK;
							//parent->_col = BLACK;
							grand->_col = RED;
						}
					}
					else // cur == parent->_right
					{
						if (parent == grand->_right) // 左旋
						{
							//         g
							//     u      p  
							//		    c
							RotateL(grand);
							parent->_col = BLACK;
							grand->_col = RED;
						}
						else // 左右双旋
						{
							//          g
							//      p       u  
							//		  c
							RotateL(parent);
							RotateR(grand);
							cur->_col = BLACK;
							//parent->_col = BLACK;
							grand->_col = RED;
						}
					}
				}

			}
			 
			// 根节点为黑色

			_root->_col = BLACK;

			return true;
		}


// 旋转
		void RotateL(Node* cur) // 左旋
		{
			// 三组关系,四个节点
			Node* curR = cur->_right; 
			Node* curRL = curR->_left;
			Node* curP = cur->_parent;

			curR->_left = cur;
			cur->_parent = curR;

			cur->_right = curRL;
			if (curRL) curRL->_parent = cur;

			if(curP)
			{
				if (cur == curP->_left)
					curP->_left = curR;
				else
					curP->_right = curR;
			}
			else
			{
				_root = curR;
			}
			curR->_parent = curP;
		}

		void RotateR(Node* cur)  // 右旋
		{
			// 三组关系,四个节点
			Node* curL = cur->_left;
			Node* curLR = curL->_right;
			Node* curP = cur->_parent;

			curL->_right = cur;
			cur->_parent = curL;

			cur->_left = curLR;
			if (curLR) curLR->_parent = cur;

			if (curP)
			{
				if (cur == curP->_left)
					curP->_left = curL;
				else
					curP->_right = curL;
			}
			else
			{
				_root = curL;
			}
			curL->_parent = curP;
		}

分析红黑树是否构建成功

  1. 中序遍历是否为排序树
		void InOrder()
		{
			_InOrder(_root);
			cout << endl;
		}


		void _InOrder(Node* root)
		{
			if (root == nullptr) return;

			_InOrder(root->_left);
			cout << root->_val << ' ';
			_InOrder(root->_right);
		}
  1. 判断是否符合红黑树性质
		void IsRBTree() // 判断是否符合红黑树的条件
		{
			if (_root->_col == RED) // 跟节点为黑色
				cerr << "_root color error" << endl;
			_IsRBTree(_root);
		}


		void _IsRBTree(Node* root) // 检测是否红黑树
		{
			if (root == nullptr) return;

			// 1. 左右子树高度差 -- 2倍
			int lenL = Height(root->_left);
			int lenR = Height(root->_right);
			if (lenL < lenR) swap(lenL, lenR);
			if (lenL > lenR * 2) cerr <<"节点" << root->_val  << ", height error" << endl;
			// 2. 红色节点的孩子不能为红色
			TowRed(root);
			// 3. 黑色节点个数相同
			BlackCnt(root);

			_IsRBTree(root->_left);
			_IsRBTree(root->_right);
		}

		int Height(Node* root)
		{
			if (root == nullptr) return 1; // 空节点算作黑节点

			int lenL = Height(root->_left);
			int lenR = Height(root->_right);
			return (lenL > lenR ? lenL : lenR) + 1;
		}
		
		void TowRed(Node* root)
		{
			if (root->_col == RED)
			{
				if (root->_left && root->_left->_col == RED)
					cerr << "节点" << root->_val << ", left color error" << endl;
				if (root->_right && root->_right->_col == RED)
					cerr << "节点" << root->_val << ", right color error" << endl;
			}
		}
		
		void BlackCnt(Node* root)
		{
			Node* lnode = root->_left;
			Node* rnode = root->_right;
			int lcnt = 0;
			int rcnt = 0;
			while (lnode)
			{
				if (lnode->_col == BLACK) ++lcnt;
				lnode = lnode->_left;
			}
			while (rnode)
			{
				if (rnode->_col == BLACK) ++rcnt;
				rnode = rnode->_right;
			}

			if (lcnt != rcnt)
				cerr << "节点" << root->_val << ", black count error" << endl
				<< lcnt << " " << rcnt << endl;
		}

  1. 测试
void Test_rbTree1()
{
	srand((unsigned int)time(0));
	kz::RBTree<int> t;
	for (int i = 0; i < 20; ++i)
	{
		t.Insert(rand() % 100);
	}

	t.InOrder(); // 中序遍历打印结果
	t.IsRBTree(); // 判断是否为红黑树
}

void Test_rbTree2()
{
	srand(time(0));
	kz::RBTree<int> t;
	for (int i = 0; i < 10000; ++i)
	{
		t.Insert(rand());
	}

	t.IsRBTree();
}

int main()
{
	Test_rbTree1();
	cout << "------------------ - " << endl;
	Test_rbTree2();

	return 0;
}

这里是引用

总结

以上就是我们关于红黑树概念以及变色、旋转的全部内容,红黑树的旋转和avl的旋转都是一样的,不一样的一点就是红黑树旋转更新节点颜色,avl旋转更新节点的平衡因子。
红黑树是近似平衡,avl则是绝对平衡,而为了达到绝对的平衡自然就需要进行更多的旋转操作,所以avl在插入过程中会消耗大量的时间,
而因为是绝对平衡的,树的高度接近完全二叉树,查找的效率自然就会提高很多,
不过由于二叉树查找效率为logN,
在10亿个数据中进行查找也只需要查找30次,由于红黑树允许1倍左右的高度差,因此10亿个数据最多可能需要查找60次,
而这中间的30次查找对于cpu来说简直可以忽略不计,由于绝对平衡调整时间消耗大而且查找优势也并不明显,因此我们平时很少使用avl,而更多的使用红黑树。

  • 红黑树的应用
  1. C++ STL库 – map/set、mutil_map/mutil_set
  2. Java 库
  3. linux内核
  4. 其他一些库


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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Xubuntu16.04 系统偶发出现自动登出的问题

继上次发现的这个问题 xubuntu系统偶发自动登出&#xff0c;这次问题又浮现了&#xff0c;我第一时间拷贝了系统日志。 为了减少搜索量&#xff0c;可以先清除之前的系统日志&#xff0c;待问题出现 echo > /var/log/syslog echo > /var/log/kern.logMar 21 15:07:58 au…

面试:解决数字精度丢失

理论上用有限的空间来存储无限的小数是不可能保证精确的&#xff0c;但我们可以处理一下得到我们期望的结果 当你拿到 1.4000000000000001 这样的数据要展示时&#xff0c;建议使用 toPrecision 凑整并 parseFloat 转成数字后再显示&#xff0c;如下&#xff1a; parseFloat(…

为什么新产品没热度,流量分析

很多人反馈新产品上线之后却没有多少热度&#xff0c;这究竟是什么样原因呢?今天来为大家分享下为什么新产品没热度&#xff0c;流量分析。 新产品没有热度其实可以从两个主要方面进行探讨&#xff1a; 一、主观原因 1.缺乏吸引消费者的独特卖点 这个原因可能是新产品太过于普…

9个服务端提升debug效率的IDEA Debugger技巧

不可否认&#xff0c;未来的一到两年中&#xff0c;程序员的编码体验将会发生剧烈的变化。作为一名一线开发&#xff0c;要如何提前准备&#xff0c;来应对这种变化呢&#xff1f; 前言 在AIGC时代&#xff0c;虽然深度学习模型可以仅通过一段注释来生成我们想要的代码&#xf…

.net 混淆工具

obfuscation tools .net 社区有很多混淆工具, 比如这个清单: https://github.com/NotPrab/.NET-Obfuscator 比较有名的商业工具有 .NET REACTOR https://www.eziriz.com/, 开源软件中, 最受欢迎的有: obfuscar https://github.com/obfuscar/obfuscar老版 ConfuserEx https://gi…

代码危机!如何利用自定义异常应对复杂业务逻辑

大家好&#xff0c;我是小米&#xff0c;在这篇文章中&#xff0c;我将和大家分享关于自定义异常的使用场景以及一个实际的电商项目案例。自定义异常在软件开发中起到了重要的作用&#xff0c;能够帮助我们更好地管理和处理各种异常情况。让我们一起来看看各个场景下如何使用自…

85.建立主体页面-第一部分

记住我们之前画的草图&#xff0c;根据我们的草图来构建初始的页面 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta http-equiv"X-UA-Compatible" content"IEedge" /><meta n…

202313读书笔记|《山居七年》——我只想在广袤璀璨的星河里享受生的鲜活,独自飞,游走

202313读书笔记|《山居七年》——我只想在广袤璀璨的星河里享受生的鲜活&#xff0c;独自飞&#xff0c;游走 《山居七年》 作者张二冬&#xff0c;选择隐士山居是一种很自由随性的生活态度&#xff0c;我觉得这不是普通人可以拥有的&#xff0c;比如我&#xff0c;并未入世也…

Nginx学习1--介绍和安装

文章目录 官方网站常用功能核心组成下载安装源码安装linux包安装ubuntu安装docker安装 官方网站 http://nginx.org/ 官方文档 常用功能 静态资源部署处理静态文件、处理索引文件以及支持自动索引&#xff1b; Rewrite地址重写正则表达式 反向代理提供反向代理服务器&#xf…

大数据:配置云服务器,主机名映射和SSH免密登录,创建Hadoop用户,复制免密登录

大数据&#xff1a;配置云服务器&#xff0c;主机名映射和SSH免密登录&#xff0c;创建Hadoop用户&#xff0c;复制免密登录 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#xff0c;大厂不招人&#xff0c;可能很多算法学生都得去找开发&#xff0c;测开 …

十、Camera 启动流程分析

和你一起终身学习&#xff0c;这里是程序员Android 经典好文推荐&#xff0c;通过阅读本文&#xff0c;您将收获以下知识点: 一、Camera 启动流程概览二、Launcher 跟Camera APP 交互三、Camera app 与FWK Camera Service 交互四、Camera FWK 与 Camera HAL 交互五、Camera FWK…

35岁,阿里6年,被打了低潜,已生无可恋,纠结该不该出去,40岁的人能不能给点建议?...

35岁是个转折点&#xff0c;许多人都在35岁时陷入迷茫&#xff0c;比如下面这位网友&#xff1a; 35岁&#xff0c;在阿里已经6年了&#xff0c;现在要混是可以混下去&#xff0c;但发展肯定是没有了&#xff0c;已经被老板打了低潜。目前被这个业务摩擦得已经生无可恋&#xf…

APP开发中的UI设计

UI设计是 APP开发中一个必不可少的部分&#xff0c;用户体验也是影响 APP产品成功与否的重要因素&#xff0c;用户体验包括用户的使用感受和操作感受。就 UI设计来说&#xff0c;它的主要内容有&#xff1a;界面、图标、颜色、字体、布局、页面布局等。 一个好的 UI设计可以增加…

通过extundelete实现CentOS6 ext4文件系统误删除文件的恢复

1.介质下载 路径&#xff1a; 链接&#xff1a;extundelete 提取码&#xff1a;ztj0 版本&#xff1a;extundelete.0.2.4 2.实验环境 1.CentOS6.8 系统版本命令&#xff1a; cat /etc/redhat-release 2.磁盘&#xff1a;/dev/sdb 磁盘查看命令&#xff1a; lsblk |g…

mysql数据备份-主从同步恢复

【1】数据库备份和恢复 1、xtrabackup-物理备份 全量备份主库数据&#xff1a;xtrabackup --backup --userroot --passwordxxx --port3306 --target-dir/comProject/backup/db/full_20220831 将full_20220831文件上传到从库服务器上 关闭存库服务&#xff1a;service mysqld …

将一维数组作为一列合并到二维数组中的numpy.column_stack()方法

【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 将一维数组作为一列合并到二维数组中 numpy.column_stack() 选择题 关于以下代码说法错误的一项是? import numpy as np a np.array([0,0]) b np.array([[1,2],[3,4]]) print("【显示…

C++ 线性数据结构系列之低调而强大的单调栈

1. 前言 单调栈是在栈基础上进行变化后的数据结构。除了遵循栈的先进后出的存储理念&#xff0c;在存储过程中还需保持栈中数据的有序性。 根据栈中数据排序的不同&#xff0c;单调栈分为&#xff1a; 单调递增栈&#xff1a;从栈顶部向栈的底部&#xff0c;数据呈递增排序。…

【JY】ABAQUS正交各向异性弹性本构模型

写在前文 材料的线弹性本构模型能够很好的描述处于工作荷载水平下的材料性能情况&#xff0c;后续材料的塑性理论也需要在弹性本构模型的基础上进行开展。由于砌体结构所采用的砌体材料具有明显的正交各项异性&#xff0c;故先从正交各向异性弹性入手&#xff0c;根据弹性理论中…

Java基础篇 | Java开发环境的搭建

✅作者简介&#xff1a;大家好&#xff0c;我是Cisyam&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Cisyam-Shark的博客 &#x1f49e;当前专栏&#xff1a; Java从入门到精通 ✨特色…

中创“六一”公益关爱活动 | 慈善守护童心,爱心筑梦未来

每一个孩子都是一朵花 有的盛开在春天&#xff0c;有的绽放在夏天 每一朵花&#xff0c;都有与众不同的美好 年年盛夏&#xff0c;如约出发&#xff1a; 在第73个“六一国际儿童节”来临之际&#xff0c;中创算力开展“六一公益关爱活动”&#xff0c;希望通过这样一个爱心…