红黑树:比AVL抽象、自由的、更广泛的近似平衡树

news2024/11/23 21:39:00

RBT与AVL树的比较

  1. AVL:高度要求差不超过1
  2. 红黑树:RBT要求最长路径不超过短路径的2倍,不需要像AVL一样太平衡,稍微自由,所以旋转较少。
  • AVL和RBT树性能比较:
    插入同样的数据,AVL树旋转更多,红黑树旋转更少。从查找来说,红黑树略慢,100W红黑树2LogN,需要40次,而AVL100W需要20次,而10亿量级节点时,两者分别是60次和30次。对计算机来说,根本没事。
    有人称,AVL树是天才设计,而红黑树是由天才加大佬设计的。

RTB:红黑树

概念:符合二叉搜索树且以属性为红色、黑色的节点组成的树,因此称为红黑树。
红黑树的性质(必须满足以下5点)

  1. 每个节点不是红就是黑。
  2. 根节点是黑色的。
  3. 红色节点的孩子都黑。
  4. 从任意固定节点到后代所有叶子节点的路径,黑色节点数量相同。
  5. 叶子都是黑色。(这里的叶子节点指的是空节点NIL)。传统叶子节点如果是黑色,假设两个节点时,有三条路径,如下图,不满足条件4。所以这里的意思是,不是说传统叶子节点,而是空叶子节点必须黑。如下面那张大图。
    请添加图片描述

请添加图片描述

思考:为什么满足上面条件就能保证:最长路径不超过短路径的2倍?

  答:因为性质4(任意节点起到叶子的路径上,黑色节点数量少),假设从根到叶子的所有路径上黑色节点有N个,而最短路径最短为N个黑色节点,最长路径满足黑色N个,所以是红黑红黑…,黑色节点和红色节点数量相同,长度为2N。所以红黑树中,根到叶子最长路径不能超过最短路径两倍。

红黑树实现

红黑树的节点定义:

每个节点需要存KV对,而KV需要使用模板。此外,每个节点和AVL树的节点一样,只是需要多一个红黑属性。每个节点默认给红色属性。此外,枚举类型定义颜色。

template<class K, class V>
struct RBTreeNode
{
	//三叉链
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;

	//存储的键值对
	pair<K, V> _kv;

	//结点的颜色
	int _col; //红/黑

	//构造函数
	RBTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _col(RED)
	{}
};

//枚举定义结点的颜色
enum Colour
{
	RED,
	BLACK
};

为什么默认节点颜色为红色?

  1. 如果插入黑色,那么路径上黑色节点数目就多余其它路径,破坏性质4,需要对红黑树调整。
  2. 如果插入红色,破坏性质3,需要调整;但是如果父亲是黑色,那就不需要调整。
  3. 综上,插入红色更方便,总体上调整会少一点。

红黑树的定义

对于一个RBT,主要是给一个根节点。

template <class K, class V>
class RBTree
{
    typedef RBTreeNode<K, V> Node;
public:
    bool Insert(const pair<K, V>& kv){};


private:
    RBTreeNode<K, V>* _root = nullptr;
};

RBT的函数

insert(): 参数:const pair<K, V>& kv, 给红黑树插入的是pair<k, v>,而插入函数返回的类型是:pair<Node*, bool>,意思是告诉你当前这个节点是否插入成功,返回方式是:利用make_pair(_root, true\false);

  • 步骤:
  1. 按BST思路,找合适位置:需要区分是否为根节点。
  2. 插入树中,且需要父节点
  3. 插入节点的父亲节点是红色,则需要调整(性质3,不能有连续红)。
  4. 如果插入节点的父亲是黑,没有破坏性质,不需要调整。
  • 调整的三种情况:(性质3较性质4容易维护)以下都是因为出现连续红节点而出现的调整。红黑树得看叔节点情况

情况1. 插入节点的父亲为红,叔叔存在也为红。

因为不能有连续红,把父和叔都变黑,再把祖父变红。相当于,p、u两条路径上多了黑色,而让g变红,和其它路径也保持了黑色节点数量相同。然后继续以祖父为cur(假设它是新插入的红色),继续向上观察它父亲是否也是红色,判断是否需要做因出现连红的现象而调整的操作。只变色即可,且不换位置,不用管p、u相对位置
但是如果祖父节点是根节点,则需要变回黑色
请添加图片描述

情况2. 插入节点叔叔存在,叔叔为黑

状态一:祖父、父、儿节点为直线, **当然也可以是向右的一条直线 **。
请添加图片描述状态二:祖父、父、儿节点为折线,当然,也可以是g右是p,然后向左是cur请添加图片描述
  如上述两种状态的第一张图所示,叔叔为黑,这种情况一定是在红黑树插入不理想而做向上调整过程中出现的。

  • 证明如下:

假设给p插入了红色, 造成的该连红局面。设祖父g节点之前,有黑点x个,叔叔之下有黑点y个,则在插入节点的那边cur一路加上g,共有x+1个黑点,而右边一路有x+2(g和u)+y个节点,即使y为0,也说明在插入cur时,各个路上的黑点数量不等,所以情况2一定是向上调整过程中产生的不理想情况
请添加图片描述
调整:
  状态一时,先以祖父g的右旋,再还是变父p和叔u为黑,祖父g为红;如果是状态一的右直线,先左旋g,再变父p和叔u为黑,祖父为红。
  状态二时,以父p节点做左旋,再以祖父g做右单旋,把儿cur变黑,祖父变红;而是向右的折线,先右旋p,再左旋g。

注意:情况2时,调整完就可停止向上。

情况3. 插入节点叔叔不存在

状态一:祖父、父亲、孩子 直线
请添加图片描述
调整:
  先以祖父g右单旋,再变色p为黑。

状态二:祖父、父亲、孩子 折线时
请添加图片描述
调整:
  先以父p左单旋,再以祖父g右单旋,再变cur为黑色,祖父变红。
到这里发现情况2和情况3的折线都需要旋转两次。

  • 注意:该情况一定是新插入的节点cur,而不是情况1调整便来的。
    请添加图片描述
    因为叔不存在,祖父到null路径上就一个黑色节点,而父p下面一定没有黑色节点了,不然cur之前是祖父,祖父一定有叔叔,且叔父都是黑色。

总结:
上面情况我们发现,需要调整的情况中:

  1. 情况1:当叔叔节点和父亲节点为红,我们需要同时变黑后使祖父变红,继续向上更新,是根则停止。
  2. 而情况2、3可以写为一类,因为直线不管是情况2、还是情况3,做法一样,而折线或直线的情况2和情况3,做法一样,我们的情况2、情况3都有直线和折线的两种。uncle在情况2中不涉及变色,所以它不存在也一样的操作。 折线最终都是cur黑,p、g为红。直线最终都是p黑,cur和g红。此外,直线都旋转一次,而折线旋转两次。
  3. 但又因为折线和直线各自又有向左和向右的区别,所以还是分了4种。所以代码和情况1相对的else是以直线和折线、向左和向右区分的4种情况。
//插入函数
pair<Node*, bool> Insert(const pair<K, V>& kv)
{
	if (_root == nullptr) //若红黑树为空树,则插入结点直接作为根结点
	{
		_root = new Node(kv);
		_root->_col = BLACK; //根结点必须是黑色
		return make_pair(_root, true); //插入成功
	}
	//1、按二叉搜索树的插入方法,找到待插入位置
	Node* cur = _root;
	Node* parent = nullptr;
	while (cur)
	{
		if (kv.first < cur->_kv.first) //待插入结点的key值小于当前结点的key值
		{
			//往该结点的左子树走
			parent = cur;
			cur = cur->_left;
		}
		else if (kv.first > cur->_kv.first) //待插入结点的key值大于当前结点的key值
		{
			//往该结点的右子树走
			parent = cur;
			cur = cur->_right;
		}
		else //已经存在
		{
			return make_pair(cur, false); //插入失败
		}
	}

	//2、将待插入结点插入到树中
	cur = new Node(kv); //根据所给值构造一个结点
	Node* newnode = cur; //记录新插入的结点(便于后序返回)
	if (kv.first < parent->_kv.first) //新结点的key值小于parent的key值
	{
		//插入到parent的左边
		parent->_left = cur;
		cur->_parent = parent;
	}
	else //新结点的key值大于parent的key值
	{
		//插入到parent的右边
		parent->_right = cur;
		cur->_parent = parent;
	}

	//3、颜色调整:当新插节点默认是红,且父也红,连续红色则调整

	while (parent && parent->_col == RED) {	// 条件一定是父亲存在且父也红
		Node* grandfather = parent->_parent;
		Node* uncle = nullptr;				// 定位uncle,根据父位判断叔
		if (parent == grandfather->_left)
			uncle = grandfather->_right;
		else
			uncle = grandfather->_left;

		// 情况1:叔存在且红
		if (uncle && uncle->_col == RED) {
			// 叔叔存在且为红
			parent->_col = BLACK;
			uncle->_col = BLACK;

			grandfather->_col = RED;
			cur = grandfather;
			parent = cur->_parent;
		}
		else {
			// 情况2+3:叔叔不存在或者叔叔存在且为黑
			if (parent == grandfather->_left && cur == parent->_left)	// 左直线
			{	
				// 此时,左左,右单旋+变色
				// 先变色也可以
				parent->_col = BLACK;
				grandfather->_col = RED;
				RotateR(grandfather);
			}
			else if (parent == grandfather->_right && cur == parent->_right) {	// 右直线
				// 右右,左单旋
				parent->_col = BLACK;
				grandfather->_col = RED;
				RotateL(grandfather);
			}
			// 折线情况下,因为情况3的uncle不存在,且情况2uncle颜色不变
			// 此外,情况2、3的折线翻转再变色后相同逻辑位置的cur、p、g最终颜色也一样 
			else if (parent == grandfather->_right && cur == parent->_left) {	
				// cur为红,parent为红,grandfather为黑。
				// 右左双旋。
                   RotateR(parent);
                   RotateL(grandfather);
                   // 记住这里是上黑,下面俩红即可。
                   cur->_col = BLACK;
                   grandfather->_col = RED;

			}
			else if (parent == grandfather->_left && cur == parent->_right) {
				RotateL(parent);
				RotateR(grandfather);
				// 记住这里是上黑,下面俩红即可。
				cur->_col = BLACK;
				grandfather->_col = RED;
				/*RotateL(parent);
				std::swap(cur, parent);
				parent->_col = BLACK;
				grandfather->_col = RED;
				RotateR(grandfather);*/
			}
			break;
		}
		// 当前是根才做 直接做也行
		if (cur == _root) {
			cur->_col = BLACK;
		}
	}
	return make_pair(newnode, true);
}
	

//左单旋
void RotateL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	Node* parentParent = parent->_parent;

	//建立subRL与parent之间的联系
	parent->_right = subRL;
	if (subRL)
		subRL->_parent = parent;

	//建立parent与subR之间的联系
	subR->_left = parent;
	parent->_parent = subR;

	//建立subR与parentParent之间的联系
	if (parentParent == nullptr)
	{
		_root = subR;
		_root->_parent = nullptr;
	}
	else
	{
		if (parent == parentParent->_left)
		{
			parentParent->_left = subR;
		}
		else
		{
			parentParent->_right = subR;
		}
		subR->_parent = parentParent;
	}
}

//右单旋
void RotateR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	Node* parentParent = parent->_parent;

	//建立subLR与parent之间的联系
	parent->_left = subLR;
	if (subLR)
		subLR->_parent = parent;

	//建立parent与subL之间的联系
	subL->_right = parent;
	parent->_parent = subL;

	//建立subL与parentParent之间的联系
	if (parentParent == nullptr)
	{
		_root = subL;
		_root->_parent = nullptr;
	}
	else
	{
		if (parent == parentParent->_left)
		{
			parentParent->_left = subL;
		}
		else
		{
			parentParent->_right = subL;
		}
		subL->_parent = parentParent;
	}
}

//左右双旋
void RotateLR(Node* parent)
{
	RotateL(parent->_left);
	RotateR(parent);
}

//右左双旋
void RotateRL(Node* parent)
{
	RotateR(parent->_right);
	RotateL(parent);
}

find(): 参数:const K& key (按key值查找)

  1. 树空,则返nullptr,按左右大小比较的方式走到了空,说明没有这个值。
  2. 只要当前存在,就按key找。
Node* Find(const K& key)
{
	Node* cur = _root;
	while (cur)
	{
		if (key < cur->_kv.first)	// 小就给左
			cur = cur->_left;
		else if (key > cur->_kv.first)	// 大就给 右
			cur = cur->_right;
		else
			return cur;	// 返回该节点
	}
	return nullptr;
}

红黑树的验证:

  1. 首先它是BST树,所以中序是有序的。
  2. 判断是否是红黑树:通过基本的5条性质来判断。
    其中,关于判断性质3不能有连续红节点和性质4黑色节点数不相等的方法。

  红黑树判定子函数

  1. 如果当前到了nullptr,说明到某条空叶路径,且它算黑的,但这里代码中,之前到了空,没算上,所以下面子函数中求黑色节点,也不计算这个。发现数量不对应,就返回错误,否则返回true。
  2. 当前节点,如果当前节点是红而父节点也红,则返回false。因为不符合性质3
  3. 当前颜色是黑,则计算一下
  4. 返回递归左孩子和递归右孩子的情况。
//判断是否为红黑树
//中序遍历
	void Inorder()
	{
		_Inorder(_root);
	}

	// 判断RBTree:性质1、调用性质3、4
	bool ISRBTree()
	{
		if (_root == nullptr)
			return true;
		if (_root->_col == RED)
		{
			cout << "根红error" << endl;
			return false;
		}
		// 以最左路径的黑节点为参考,虽然可能最左是错的,但是我们只关注所有的路径上黑色节点数是否一致
		Node* cur = _root;
		int blackcount = 0;
		while (cur)
		{
			if (cur->_col == BLACK)
				blackcount++;
			cur = cur->_left;
		}
		int count = 0;	// 根为黑,以根起始,看每条路径
		return _isRBTTree(_root, count, blackcount);
	}

private:
	//中序遍历子函数
	void _Inorder(Node* root)
	{
		if (root == nullptr)
			return;
		_Inorder(root->_left);
		cout << root->_kv.first << " ";
		_Inorder(root->_right);
	}
	
	// 红黑树的判断
	bool _isRBTTree(Node* root, int cur_balck, int total_balck)
	{
		if (root == nullptr)	// 当前走到空,判断这条路径钟点是否黑点一样多
		{
			if (cur_balck != total_balck)
			{
				cout << "黑子数量不同" << endl;
				return false;
			}
			return true;
		}
		// 顺便看 性质3:不能连红:当前红,一定有父亲,因为根不能红,不必判断父存在
		if (root->_col == RED && root->_parent->_col == RED)
		{
			cout << "连续红错误" << endl;
			return false;
		}
		// 当前还没到叶子,如果当前黑,则计数++
		if (root->_col == BLACK)
			cur_balck++;
		return _isRBTTree(root->_left, cur_balck, total_balck) && _isRBTTree(root->_right, cur_balck, total_balck);
	}

效果:
请添加图片描述

红黑树和AVL树的比较

  • 行为属性上:
    红黑树是较为自由的AVL树,因为它没有严格要求左右高度差,只是控制节点颜色,让最长路径别超最短2倍。
  • 效率上:
    AVL树复杂在旋转多,而红黑树降低了插入的旋转,在增、删操作中比AVL树更有,使得红黑树应用更广泛

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

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

相关文章

本地生活配送行业黑马,带你一键读懂闪飞侠

电商的黄金十年已经过去&#xff0c;本地生活的黄金市场才刚刚开启&#xff0c;本地生活市场的增长对同城配送的影响得有多大&#xff1f;2020年的新冠疫情&#xff0c;爆发了同城即时配送的投资新机遇&#xff01;即时配送用户已超5亿。而随着即时配送行业的广泛应用&#xff…

【 Vue3 + Vite + setup语法糖 + Pinia + VueRouter + Element Plus 第三篇】(持续更新中)

在第二篇我们主要学习了路径别名&#xff0c;配置.env环境变量&#xff0c;封装axios请求&#xff0c;以及使用api获取数据后渲染 Element Plus表格 本期需要掌握的知识如下: 封装列表模糊查询组件实现新增 编辑 删除 模糊查询 重置 功能实现表单校验功能实现组件间传值 下期…

Compose跨平台第一弹:体验Compose for Desktop

前言 Compose是Android官方提供的声明式UI开发框架&#xff0c;而Compose Multiplatform是由JetBrains 维护的&#xff0c;对于Android开发来说&#xff0c;个人认为学习Jetpack Compose是必须的&#xff0c;因为它会成为Android主流的开发模式&#xff0c;而compose-jb作为一…

TikTok三大流行趋势 钛动带你看懂TikTok

武汉瑞卡迪电子商务有限公司&#xff1a;近日,TikTok for Business发布了《Whats Next 2023 TikTok 全球流行趋势报告》,就2023年TikTok三大趋势主题进行了介绍。 钛动科技作为TikTok官方授权代理商,是TikTok生态服务最齐全的出海服务商,凭借出色的技术与服务能力,钛动斩获了T…

论 G1 收集器的架构和如何做到回收时间用户设定

目录G1 概念JVM的内存分代假设让用户设置应用的暂停时间G1 概念 G1其实是Garbage First的意思&#xff0c;它不是垃圾优先的意思&#xff0c;而是优先处理那些垃圾多的内存块的意思。 在大的理念上&#xff0c;它还是遵循JVM的内存分代假设。 JVM的内存分代假设 JVM的内存分代…

https如何加密解密?

背景 我们知道&#xff0c;https&#xff0c;在网络传输中&#xff0c;加密。具体来说是数据加密。//客户端和服务器&#xff0c;写数据的时候&#xff0c;都会加密。即1.客户端——》服务器 2.服务器——》客户端。 如何加密 解密&#xff1f; 加密解密 想要加密和解密&am…

【Mysql篇】数据库事务

目录 数据库事务数据库事务介绍 JDBC事务处理 事务的ACID属性 数据库的并发问题 数据库提供的4种事务隔离级别&#xff1a; 在MySql中设置隔离级别 数据库事务数据库事务介绍 事务&#xff1a;一组逻辑操作单元,使数据从一种状态变换到另一种状态。 事务处理&#xff08;…

Day1. Spring

1 课程描述IoC基础容器&#xff0c;主要涉及Bean对象的管理。AOP面向切面编程&#xff0c;主要涉及切面配置&#xff0c;声明式事务控制Spring整合Web环境。Web层解决方案-SpringMVC.1.1 IoC、DI和AOP思想的提出由于传统的JavaWeb出现的问题&#xff1a;问题1&#xff1a;层与层…

【问题记录】Process finished with exit code -1073740791 (0xC0000409) 注:LSTM股票预测案例中

目录 1. 问题来源2. 问题解决1. 问题来源 在跑一段 LSTM&神经网络 股票预测的代码时,遇到了下述报错,报错提示为: Process finished with exit code -1073740791 (0xC0000409) 报错截图为: 下面是整个报错的内容,这里我也截取下来了: H:\Python学习专用\深度学习\LS…

ECMAScript基础入门

JavaScript&#xff08;浏览器端&#xff09;ECMAScript&#xff08;语法API&#xff09;DOMBOM es6开始let代替var声明变量&#xff08;初始化后可重新赋值&#xff09;&#xff0c;而const声明常量&#xff08;初始化后不可重新赋值&#xff0c;否则会报错&#xff09; con…

Java调用百度OCR接口实现文字识别

博主在项目开发中需要完成一个文字识别功能&#xff0c;由于之前有过使用百度云平台接口进行身份证识别的经历&#xff0c;因此这次也是自然而然的再次选择了百度AI平台&#xff0c;首先需要开通百度通用文字识别功能。 然后我们需要创建一个应用&#xff1a; 然后我们就可以…

TensorFlow 基础(一)张量

文章目录BasicsAbout shapesIndexingSingle-axis indexingMulti-axis indexingManipulating ShapesMore on dtypesReferencesimport tensorflow as tf import numpy as npBasics 张量是具有统一类型&#xff08;dtype&#xff09;的多维数组。它和 NumPy 中的 np.arrays 是非常…

C进阶_C语言_函数与指针_C语言指针进阶

上一篇博客http://t.csdn.cn/GYCiM 我们了解了指针相关知识&#xff0c;今天来了解函数和指针的关系。 目录 函数指针 函数指针数组 指向函数指针数组的指针 回调函数 qsort 冒泡排序模拟实现qsort 函数指针 我们知道&#xff0c;数组指针是指向数组的指针。 int arr[…

Ribbon负载均衡服务调用

文章目录一. 什么是Ribbon二. Ribbon负载均衡三. Ribbon负载均衡策略四. Ribbon饥饿加载一. 什么是Ribbon PS: 本篇文章文作者学习笔记&#xff0c;技术参考价值不大。 Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端&#xff0c;负载均衡的工具。 简单的说&#x…

Allegro174版本新功能介绍之改变报表字体大小

Allegro174版本新功能介绍之改变报表字体大小 Allegro在升级到174的时候,默认show element的字体是非常小的,类似下图,辨认起来非常困难 但是174是支持把字体调整的大一些的,具体操作如下 选择Setup选择User Preferences

SpringBoot整合java诊断工具Arthas

一、Arthas官方文档https://arthas.aliyun.com/doc/二、springBoot整合方式1、pom文件引入<dependency><groupId>com.taobao.arthas</groupId><artifactId>arthas-spring-boot-starter</artifactId><version>3.6.7</version> </d…

机器学习:机器学习常见的算法分类和算法优缺点汇总

机器学习实战教程(13篇)_M_Q_T的博客-CSDN博客这些网址非常适合想学习机器学习&#xff0c;却苦于没有项目&#xff08;尤其缺少数据&#xff09;的人。无意中看到&#xff0c;给自己做一个记录。目录大类&#xff1a;学习方式监督式学习&#xff1a;非监督式学习&#xff1a;半…

ES6 课程概述③

文章目录5-1. 新增的对象字面量语法5-2. Object 的新增 API5-4 类&#xff1a;构造函数的语法糖传统的构造函数的问题类的特点5-5. 类的其他书写方式5-1. 新增的对象字面量语法 成员速写 如果对象字面量初始化时&#xff0c;成员的名称来自于一个变量&#xff0c;并且和变量的…

2023/01/05 java面试题每日10问

1.What is Java? Java is object-oriented, platform-independent, Multithreaded, and portable programming language.it provides its own JRE and API. 2.What is the difference between JDK, JRE, and JVM? JVM Java Virtual Machine provides the runtime environm…

小小闭门会,揭示SaaS大趋势

从2014年中国的SaaS领域开始有融资到现在已经8年&#xff0c;从最开始的一张云图填不满到现在的密密麻麻&#xff0c;厂商和产品如雨后春笋般的多了起来&#xff0c;但很多SaaS依然在孤军奋战&#xff0c;很多厂商陷入定制化泥潭。有人说中国的SaaS有特殊国情&#xff0c;大企业…