当前位置: 首页 > news >正文

平衡二叉树之红黑树

文章目录

      • 红黑树产生的历史背景
      • 红黑树的五个性质的解读
      • 红黑树的插入算法
        • 左单旋
        • 右单旋
        • 右左双旋
        • 左右双旋
        • 代码实现
      • 和AVL树进行对比
      • 红黑树的应用

红黑树产生的历史背景

前面我们介绍了二叉搜索树,因为二叉搜索树在极端情况下会退化成一个链表,导致搜索的优势荡然无存。因此人们创造出了AVL树来解决这个问题。 但是由于AVL树的规则太严格,也就意味着每次插入节点对AVL树维护的成本会比较高。所以人们需要一棵相对平衡的二叉树但是维护的成本相比AVL会更小。于是,计算机的先驱们就提出了红黑树,红黑树不仅有着接近AVL的查找性能,并且它的维护成本相较于AVL会小很多。 接下来,我们就来走进红黑树。

红黑树的五个性质的解读

首先,如果一棵二叉搜索树有如下5个性质,那么它就是一棵红黑树:

1.每个节点不是红色就是黑色
2.根节点的颜色是黑色(空节点也可以认为是黑色节点)
3.没有连续的红色节点
4.每条路径上的黑色节点的数量相同
5.最长的路径不超过最短路径长的2倍

接下来,我们来看一看这五条规则是怎么保证红黑树的平衡。
首先规则1没什么好说的,然后就是规则3和规则4 保证了规则5情况,因为在规则3规则4的限制下

最短的是全黑色节点的路径,而最长的路径就是一红一黑交替的路径。设纯黑色的路径有x个黑色节点,那么最长的路径就是2x个节点,刚好就是性质5。

因此,红黑树也可保证相对的平衡。在查找的效率上也可以接近O(logn)。下面我们就来模拟实现红黑树。

红黑树的插入算法

首先我们先来定义红黑树的节点结构:

//红黑树的节点结构
/*
*模拟实现红黑树
* 1.每个节点不是黑色就是红色
* 2.根节点是黑色
* 3.没有连续的红节点
* 4.每条路径有相同数量的黑节点
* 5.最长路径不超过最短路径的2倍
*/
//使用枚举类型,红色和黑色
enum Colour
{
	RED,
	BLACK,
};
template<typename K,typename V>
struct RBTreeNode
{
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	pair<K, V> _kv;
	Colour _col;
	//expilict限制隐式类型转换,可加可不加
	explicit RBTreeNode(const pair<K,V>& kv)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_kv(kv)
		,_col(RED)
	{}
};

接下来我们来分析,红黑树插入的各种情况。

首先,红黑树的插入的第一个问题就是我们需要插入的是什么颜色的节点,红色还是黑色?那么我们来分析一下:
如果插入红色节点:可能会破坏规则3,但是不会破坏规则4
如果插入黑色节点:必然破坏规则4。

而权衡规则3和规则4,规则4的维护成本非常大!因此我们选择插入的是红色节点!
接下来我来解释一下图示的符号:

g:表示祖父节点
u:表示叔叔节点
p:表示父亲节点
c:表示当前节点

cur就是新增的节点:
而红黑树的插入的调整方式最关键的就是看叔叔的情况。
情况1;叔叔存在且是红色
那么这时候这种情况下,假设说我们此时新增一个节点cur如下图:
在这里插入图片描述
那么我们处理的方式就是:pa和uncle节点变成黑色,然后把祖父节点变成红色,然后我们需要继续向上调整处理!
在这里插入图片描述
那么cur节点是红色节点的情况还有可能是由于情况1的调整过来的!但是我们的处理方式也是和前面是相同的!
那么这是叔叔节点存在并且叔叔节点是红色节点的情况。

而插入情况里面,最为复杂的就是叔叔节点不存在或者叔叔节点存在且叔叔节点是黑色节点的情况,这种情况的插入就是相对复杂。下面我们就来分析一下

左单旋

情况2:叔叔节点不存在或者叔叔节点存在且为黑色
由于规则里面规定了空节点的颜色是黑色,所以情况2可以概括成为叔叔节点为黑色。那么对于这种情况,cur节点是红色必然只有可能是由情况1变化而来!(否则先前就不是红黑树).那么我们来看如下的这种情况:
在这里插入图片描述
而这种情况我们就需要进行旋转处理:以祖父为轴进行一次左单旋,然后祖父变红,父亲变黑,而这个时候因为父亲节点就变成祖父节点,此时不需要再继续向上调整了。
在这里插入图片描述

右单旋

而如果是下面这种情况,那么就要进行右单旋进行调整:
在这里插入图片描述
那么进行右单旋以后,对g,p进行变色即可:
在这里插入图片描述

右左双旋

前面两种情况都是一次旋转就可以解决的。回想我们先前AVL树插入会出现需要双旋的情况。红黑树也会存在需要进行双旋转的情况。我们来看下面这种情况:
在这里插入图片描述
处理的方式:以父亲为轴进行右单旋,再以祖父节点为轴进行左单旋转
在这里插入图片描述

左右双旋

和上面的情况类似,下面的情况需要进行的就是左右双旋:
在这里插入图片描述

代码实现

下面就是对前面描述的各种情况的代码描述:

bool insert(const pair<K, V>& kv)
	{   
		if (!_root)
		{  
			_root = new Node(kv);
			_root->_col = BLACK;
			return true;
		}
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{   //左子树
			if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			//右子树
			else if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			//停止重复插入
			else
			{
				return false;
			}
		}
		//插入节点默认是红色
		cur = new Node(kv);
		if (parent->_kv.first > kv.first)
			parent->_left = cur;
		else
			parent->_right = cur;
		cur->_col = RED;
		cur->_parent = parent;
		//如果父亲存在且为红色,违反规则三,需要处理
		while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent;
			if (parent == grandfather->_left)
			{
				Node* uncle = grandfather->_right;
				//情况1:叔叔存在且为红色
				//处理方式:叔叔和父亲变黑,祖父变红,继续向上调整
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;
					cur = grandfather;
					parent = cur->_parent;
				}
				//叔叔不存在||叔叔存在且是黑色
				else
				{
					//如果父亲是祖父的左且cur也是父亲的左要右单旋
					if (cur == parent->_left)
					{
						RotateR(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					//如果cur是父亲的右,就要双旋
					/*   g
					*   p
					*    c
					*/
					else
					{
						RotateL(parent);
						RotateR(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;						
					}
					break;
				}
			}
			//父亲是祖父的右
			else
			{
				Node* uncle = grandfather->_left;
				//情况1:叔叔存在且为红色
				//处理方式:叔叔和父亲变黑,祖父变红,继续向上调整
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;
					cur = grandfather;
					parent = cur->_parent;
				}
				//叔叔不存在||叔叔存在且是黑色
				else
				{  
					
					//如果父亲是祖父的右且cur也是祖父的右要左单旋转
					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;
	}
	
		
private:
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		Node* ppNode = parent->_parent;
		parent->_left = subLR;
		subL->_right = parent;
		parent->_parent = subL;
		if (subLR)
			subLR->_parent = parent;
		if (parent == _root)
		{
			_root = subL;
			_root->_parent  = nullptr;
		}
		else
		{
			
			if (parent == ppNode->_left)
				ppNode->_left = subL;
			else
				ppNode->_right = subL;
			subL->_parent = ppNode;
		}
		
	}
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		Node* ppNode = parent->_parent;
		parent->_right = subRL;
		parent->_parent = subR;
		subR->_left = parent;
		if (subRL)
			subRL->_parent = parent;
		if (_root == parent)
		{
			_root = subR;
			_root->_parent = nullptr;
		}
		else
		{
			if (parent == ppNode->_left)
				ppNode->_left = subR;
			else
				ppNode->_right = subR;
			subR->_parent = ppNode;
		}
	}

和AVL树进行对比

由于红黑树的删除相对比较复杂,后面会单独抽出一篇博客专门讲解红黑树的删除(AVL树的删除也会单独写一篇博客),接下来我们就来对比一下AVL树和红黑树。

1.AVL树是严格的平衡,查询效率是O(logn),而红黑树是近似的平衡,不过对于计算机来说,也可以近似认为是logn

2.AVL树插入的时候会频繁进行旋转,性能消耗大
3.红黑树实际实现相对简单

红黑树的应用

  1. STL里的set和map的底层数据结构
  2. linux的epoll
  3. java标准库的TreeMap底层容器

以上就是本文的主要内容,如果有不足之处还望指出。

相关文章:

  • 【python-Unet】计算机视觉~舌象舌头图片分割~机器学习
  • 【云原生】Hive on k8s 环境部署
  • 一起来学Kotlin:概念:1. Kotlin ArrayListOf 的使用案例
  • 基于MATLAB/GUI的自组网仿真平台,对比leach,ADOV协议
  • 四、哈希表相关题目
  • 【WSN定位】基于改进chan算法和talor算法实现多基站目标定位附matlab代码
  • 【LeetCode每日一题】2022-10-02 777. 在LR字符串中交换相邻字符 Java实现
  • 网络安全从业人员能力图谱
  • 从程序员的角度看人类通信史
  • OpenCV之识别银行卡号
  • 回归-线性回归算法(房价预测项目)
  • 【一起学数据结构与算法】Java实现双链表
  • Spring Boot集成阿里云视频点播服务的过程记录
  • SpringBoot社区居民联系方式管理系统(附源码)
  • [ccc3.0][数字钥匙] UWB配置和使用(二)
  • 「前端」从UglifyJSPlugin强制开启css压缩探究webpack插件运行机制
  • 【css3】浏览器内核及其兼容性
  • Android单元测试 - 几个重要问题
  • CSS进阶篇--用CSS开启硬件加速来提高网站性能
  • Java 网络编程(2):UDP 的使用
  • Java多线程(4):使用线程池执行定时任务
  • JS笔记四:作用域、变量(函数)提升
  • Material Design
  • Redis的resp协议
  • Spring Cloud Alibaba迁移指南(一):一行代码从 Hystrix 迁移到 Sentinel
  • Xmanager 远程桌面 CentOS 7
  • 基于 Babel 的 npm 包最小化设置
  • 基于OpenResty的Lua Web框架lor0.0.2预览版发布
  • 浅析微信支付:申请退款、退款回调接口、查询退款
  • 十年未变!安全,谁之责?(下)
  • LevelDB 入门 —— 全面了解 LevelDB 的功能特性
  • ​​​​​​​ubuntu16.04 fastreid训练过程
  • ​ssh免密码登录设置及问题总结
  • (003)SlickEdit Unity的补全
  • (01)ORB-SLAM2源码无死角解析-(56) 闭环线程→计算Sim3:理论推导(1)求解s,t
  • (3)选择元素——(17)练习(Exercises)
  • (AngularJS)Angular 控制器之间通信初探
  • (delphi11最新学习资料) Object Pascal 学习笔记---第5章第5节(delphi中的指针)
  • (二)丶RabbitMQ的六大核心
  • (附源码)springboot炼糖厂地磅全自动控制系统 毕业设计 341357
  • (强烈推荐)移动端音视频从零到上手(上)
  • (万字长文)Spring的核心知识尽揽其中
  • (原創) 未来三学期想要修的课 (日記)
  • (转)linux下的时间函数使用
  • (转)ObjectiveC 深浅拷贝学习
  • (自适应手机端)响应式新闻博客知识类pbootcms网站模板 自媒体运营博客网站源码下载
  • ***监测系统的构建(chkrootkit )
  • .bat批处理(四):路径相关%cd%和%~dp0的区别
  • .mysql secret在哪_MySQL如何使用索引
  • .NET “底层”异步编程模式——异步编程模型(Asynchronous Programming Model,APM)...
  • .net core 6 集成和使用 mongodb
  • .Net Remoting(分离服务程序实现) - Part.3
  • .NET 中选择合适的文件打开模式(CreateNew, Create, Open, OpenOrCreate, Truncate, Append)
  • .NET面试题解析(11)-SQL语言基础及数据库基本原理
  • .NET文档生成工具ADB使用图文教程