C++AVL树拓展之红黑树原理及源码模拟

news2024/11/26 12:24:34

前言:我们之前已经从零开始掌握AVL树http://t.csdnimg.cn/LaVCCicon-default.png?t=N7T8http://t.csdnimg.cn/LaVCC

现在我们将继续学习红黑树的原理并且实现插入等功能,学习本章的前提要求是掌握排序二叉树和AVL树,本章不再提及一些基础知识,防止本文结构臃肿,对二叉排序树和AVL树有兴趣的可以阅读上面链接的文章,很多人可能说既生瑜何生亮,有了AVL树为什么还要红黑树,当然是因为红黑树的效率更高啦,AVL树的平黑太过于依赖平衡因子,稍微不平衡就会旋转,而大量的旋转自然降低了效率,红黑树相对AVL树没有那么平衡,旋转次数也少了,但是查询效率略微的降低就减少了不少的旋转,何乐而不为呢?更何况C++是一门以高效出名的语言。

目录

一,红黑树的基本准则

二,红黑树为什么是平衡的

三,代码实现

1)敲前准备

2)查找

3)插入

4)迭代器


一,红黑树的基本准则

希望大家先记住红黑树本质还是一颗二叉排序树。在二叉排序树的基础上,AVL树是加了平衡因子,来保持树的结构平衡,红黑树则是通过给每个结点标记颜色达到相对平衡。(为什么要平衡是为了提高查询效率,不懂看链接博客)

1)每个结点的颜色不是黑色就是红色

2)红黑树根节点的颜色是黑色的(这条规定会在后面平衡的调整那里给出原因,现在记住即可)

3)红黑树上不能出现两个相邻的红色结点(红黑树平衡的重要准则)

4)每个叶子结点都是黑色的。(注意这里的叶子结点指的是NULL结点)

5)每条路径上的黑色结点的数目都是一样多的

6)最短路径小于最长路径的两倍(这个其实不是原则,是一个推论,下面会讲解,不必纠结)

二,红黑树为什么是平衡的

接下来我们将讨论一下为什么红黑树是平衡的。

讨论这个性质我们要从上面说的红黑树的基本准则入手。红黑树不过三种情况我们分类讨论

1)结点的颜色全是黑色

如果红黑树的结点颜色全是黑色,那么这棵树必定是一个完全二叉树,因为如果不是完全二叉树,红黑树的结点有全是黑色,那就违背了上面的第五条原则(每条路径上的黑色结点的数目都是一样多的)。

得出来红黑树的结点全是黑色的则次数必定平衡。

2)除了根节点其他结点都是红色

这种情况只有四种情况,我直接给大家画出来,记住不能违背上面的第三个准则(红黑树上不能出现两个相邻的红色结点)。

     

如果再插入结点必然出现黑色结点,不满足我们这种情况了。

3)既有红色结点也有黑色结点

首先根据上面的准则,每条路径上的黑色结点数目一样,红色结点不能相邻出现,也就是两个红色结点之间必然有若干个黑色结点,然而每条路径上黑色结点的数目已经固定了,我们现在看极端情况,也就是最短的路径一个红色结点也没有,最长的路径上每个红色结点之间只有一个黑色结点。

从上面的图可以得到最长路径绝对不会超过最短路径的两倍,因为红色结点的数目不会超过黑色结点 ,当然上面是把路径单独列出来了来,实际上是树状结构。

综上所述红黑树的是一个相对平衡的二叉树。

三,代码实现

1)敲前准备

首先我们需要一个标记位来记录当前结点的颜色,我们采用枚举类型,可读性强

enum color {
	red,
	black
};

结点里面的内容应该包括什么呢?data存储数据,三个指针,一个parent指针,一个leftchild和rightchild指针,结构体里面应该包括我们刚刚的枚举。

template<class T>
struct RBTreeNode {
	RBTreeNode(T data) {
		_pParent = NULL;
		_pLeft = NULL;
		_pRight = NULL;
		_data = data;
		c = red;
	}
	RBTreeNode() {
		_pParent = NULL;
		_pLeft = NULL;
		_pRight = NULL;
		c = red;
	}
	color c;
	RBTreeNode* _pParent;
	RBTreeNode* _pLeft;
	RBTreeNode* _pRight;
	T _data;
};

那么大致框架就搭起来了

enum color {
	red,
	black
};
template<class T>
struct RBTreeNode {
	RBTreeNode(T data) {
		_pParent = NULL;
		_pLeft = NULL;
		_pRight = NULL;
		_data = data;
		c = red;
	}
	RBTreeNode() {
		_pParent = NULL;
		_pLeft = NULL;
		_pRight = NULL;
		c = red;
	}
	color c;
	RBTreeNode* _pParent;
	RBTreeNode* _pLeft;
	RBTreeNode* _pRight;
	T _data;
};
template<class T>
class RBTree
{
private:
	Node* _pHead; //哨兵位
	size_t _size;//结点个数
};
2)查找

查找还是一个老套路,大于当前结点找右边,小于当前结点找左边,直到找到或者为空,属实是老生常谈了,这里不过多介绍。

// 检测红黑树中是否存在值为data的节点,存在返回该节点的地址,否则返回nullptr
	Node* _Find(const T& data) {
		Node* cur = _pHead->_pParent;//从根节点开始
		while (cur&&cur!=_pHead) {
			if (data < cur->_data)  //小于找左边
				cur = cur->_pLeft;
			else if (data > cur->_data) {   //大于找右边
				cur = cur->_pRight;
			}
			else
				return cur;   //找到返回
		}
		return NULL;  //找不到
	}
3)插入

插入的第一件事就是找到应该插入的位置,这个简单,这个逻辑和查找一样。插入之后的颜色应该是红色还是黑色值得商榷,但仔细考虑,如果插入黑色的话就违背了每条路径上的黑色结点个数相等的原则,插入红色则可能碰到连续的红色结点,那到底是插入红色还是黑色呢?我们现在来讨论一下。

如果插入黑色结点的话,那么完全是牵一发而动全身,因为根据结点规则每条路径上的黑色结点的数目都是一样多的,我们需要把所有路径的黑色结点数目全部增加一个,这显然不是一个明智之举。那我们只剩下一个选择了,插入的新结点默认为红色结点,接下来我们需要分情况讨论。

1)插入结点的父亲结点是黑色,如果是黑色插入红色节点不需要改变任何结点,因为完全满足红黑树的规则,既没有连续的红色结点,每条路径的黑色结点数也都相同。

2)如果是父亲是红色的结点呢?

     注:圆形代表一个结点,长方形代表很多种可能

这种情况我们需要看parent的兄弟结点的颜色了,接下来又要分情况讨论

1)兄弟节点是红色,这种情况我们把两个兄弟节点全变成黑色,把爷爷结点变成红色,然后继续递归往上,往上有两种可能,一种是一直递归到根节点,然后根节点变成红色,最后我们强制把根节点变成黑色就行了,并不会违背任何原则。当然可能中途兄弟节点是黑色,这个时候我们需要使用下面情况2的旋转来弥补了。

2)兄弟节点是黑色的时候,证明单纯靠变色已经无法将这颗红黑树拉上正途了,我们不得已采取暴力手段旋转了,旋转结果仍然需要遵守红黑树原则。这里面又分为好几种情况

旋转具体详细过程,参考我的往期博客

http://t.csdnimg.cn/a13umicon-default.png?t=N7T8http://t.csdnimg.cn/a13um

1)左旋(之所以每个节点下面都可能有节点是因为,新插入的节点不可能碰到这种情况,只可能是情况1向上递归解决的时候出现的)

void RotateL(Node* pParent)
	{
		Node* pSubR = pParent->_pRight;
		Node* pSubRL = pSubR->_pLeft;

		pParent->_pRight = pSubRL; //防止访问空结点
		if (pSubRL)
			pSubRL->_pParent = pParent;

		pSubR->_pLeft = pParent;
		Node* pPParent = pParent->_pParent;
		pSubR->_pParent = pPParent;
		pParent->_pParent = pSubR;

		if (pPParent == _pHead)     //根节点单独处理
			_pHead->_pParent = pSubR;
		else
		{
			if (pParent == pPParent->_pLeft)
				pPParent->_pLeft = pSubR;
			else
				pPParent->_pRight = pSubR;
		}
	}

2)右旋

 

void RotateR(Node* pParent)
	{
		Node* pSubL = pParent->_pLeft;
		Node* pSubLR = pSubL->_pRight;

		pParent->_pLeft = pSubLR;
		if (pSubLR)       //防止访问空结点
			pSubLR->_pParent = pParent;

		pSubL->_pRight = pParent;

		Node* pPParent = pParent->_pParent;
		pParent->_pParent = pSubL;
		pSubL->_pParent = pPParent;

		if (pPParent == _pHead)      //根节点单独处理
			_pHead->_pParent = pSubL;
		else
		{
			if (pParent == pPParent->_pLeft)
				pPParent->_pLeft = pSubL;
			else
				pPParent->_pRight = pSubL;
		}
	}

3)右旋加左旋

4)左旋加右旋

 双旋代码复用单旋就行了

插入代码:

bool _Insert(const T& data) {
		if (_Find(data)) {
			cout << "元素已经存在" << endl;
			return false;
		}
		//插入第一个元素的时候
		if (_pHead->_pParent == _pHead) {
			Node* root = new Node(data);
			root->c = black;
			root->_pParent = _pHead;
			_pHead->_pParent = root;
			_pHead->_pLeft = root;
			_pHead->_pLeft = root;
			return 1;
		}
		Node* cur = _pHead->_pParent;
		Node* parent=cur;
		//找该插入的位置
		while (cur&&cur!=_pHead) {
			parent = cur;
			if (cur->_data > data) {
				cur = cur->_pLeft;
			}
			else if (cur->_data < data) {
				cur = cur->_pRight;
			}
			else {
				cout << "值:" << data << "已经存在" << endl;
				return 0;
			}
		}
		//插入
		cur = new Node(data);
		if (parent->_data > data) {
			parent->_pLeft = cur;
			cur->_pParent = parent;
		}
		else {
			parent->_pRight = cur;
			cur->_pParent = parent;
		}
		//调整
		Node* gparent = parent->_pParent;
		Node* uncle = _pHead;
		while (gparent&&parent->c != black) {
			if (gparent->_pLeft == parent) {
				uncle = gparent->_pRight;
			}
			else {
				uncle = gparent->_pLeft;
			}
			if (!uncle || uncle->c == black)
				break;
			else {
				uncle->c = black;
				gparent->c = red;
				parent->c = black;
			}
			cur = gparent;;
			parent = cur->_pParent;
			gparent = parent->_pParent;
		}
		if (cur == parent->_pLeft && parent == gparent->_pLeft && (uncle == NULL || uncle->c == black)) {
			RRotate(gparent); //左旋情况
			parent->c = black;
			gparent->c = red;
		}
		if (cur == parent->_pRight && parent == gparent->_pRight && (uncle == NULL || uncle->c == black)) {
			LRotate(gparent);  //右旋情况
			parent->c = black;
			gparent->c = red;
		}
		if (cur == parent->_pLeft && parent == gparent->_pRight && (uncle == NULL || uncle->c == black)) {
			RRotate(parent); //右左双旋
			LRotate(gparent);
			cur->c = black;
			gparent->c = red;
		}
		if (cur == parent->_pRight&& parent == gparent->_pLeft && (uncle == NULL || uncle->c == black)) {
			LRotate(parent);  //左右双旋
			RRotate(gparent);
			cur->c = black;
			gparent->c = red;
		}
		_pHead->_pLeft = LeftMost();
		_pHead->_pRight = RightMost();
		RightMost()->_pRight = _pHead;
		_pHead->_pParent->c = black;
		_size++;
		return 1;
	}
4)迭代器

迭代器属于老生常谈了,就是运算符重载,我们这里不做过多讲解,但是我们这里面有两个难点,就是++,--拿的是哪个结点?

首先看4的下一个下一个结点是什么(也就是++)?如果右子树不为空的话,下一个结点是右子树的最左结点。

那7的下一个结点是什么呢?当右子树为空时,一直递归向上直到这个这颗子树是某个结点的左孩子,这个结点就是下一个结点。

struct RBTreeIterator
{
	typedef RBTreeNode<T> Node;
	typedef typename RBTreeIterator<T> Self;
public:
Self& operator++() {
		if (_pNode->_pRight != NULL) //右子树不为空的情况下
		{
			_pNode = _pNode->_pRight;
			if (_pNode->_pParent->_pParent == _pNode) {
				RBTreeIterator<T> ret(_pNode);
				return ret;
			}
			while (_pNode->_pLeft != NULL)
				_pNode = _pNode->_pLeft;

			RBTreeIterator<T> ret(_pNode);
			return ret;
		}
		while (_pNode != _pNode->_pLeft) {       //一直递归向上直到这个这颗子树是某个结点的左孩子
			if (_pNode->_pParent->_pParent == _pNode) {
				RBTreeIterator<T> ret(NULL);
				return ret;
			}
			_pNode = _pNode->_pParent;
		}
		RBTreeIterator<T> ret(_pNode->_pParent);
		return ret;
	}
};

那--呢?也就是上一个结点。例如4,当左孩子不为空时,左子树的最右结点就是你的上一个结点。

如果最子树为空呢?例如5,那就一直向上递归,直到这颗子树是某个结点的右孩子,这个结点就是上一个结点。

struct RBTreeIterator
{
	typedef RBTreeNode<T> Node;
	typedef typename RBTreeIterator<T> Self;
public:	
Self& operator--() {
		if (_pNode->_pLeft != NULL) { //左子树为空的情况
			_pNode = _pNode->_pLeft;
			while (_pNode->_pRight) {
				_pNode = _pNode->_pRight;
			}
			Self a(_pNode);
			return a;
		}
		else {   //一直向上递归,直到这颗子树是某个结点的右孩子
			while (_pNode->_pParent->_pRight != _pNode) {
				if (_pNode->_pParent->_pParent == _pNode) {
					RBTreeIterator<T> ret(NULL);
					return ret;
				}
				_pNode = _pNode->_pParent;
			}
			Self a(_pNode->_pParent);
			return a;
		}
	}
};

其他的运算符重载没啥难度,大家完全可以靠自己敲出来。

这篇博客花了作者大量心思,希望大家你点赞+收藏+转发。如果博客有不对的地方,可以评论区讨论。

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

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

相关文章

Flutter 全局控制底部导航栏和自定义导航栏的方法

1. 介绍 导航栏在移动应用中扮演着至关重要的角色&#xff0c;它是用户与应用之间进行导航和交互的核心组件之一。无论是简单的页面切换&#xff0c;还是复杂的应用导航&#xff0c;导航栏都能够帮助用户快速找到所需内容&#xff0c;提升用户体验和应用的易用性。 在移动应用…

Electron 读取本地配置 增加缩放功能(ctrl+scroll)

最近&#xff0c;一个之前做的electron桌面应用&#xff0c;需要增加两个功能&#xff1b;第一是读取本地的配置文件&#xff0c;然后记载配置文件中的ip地址&#xff1b;第二就是增加缩放功能&#xff1b; 第一&#xff0c;配置本地文件 首先需要在vue工程根目录中&#xff0…

华为OD机试 - 芯片资源限制(Java 2024 C卷 100分)

华为OD机试 2024C卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;A卷B卷C卷&#xff09;》。 刷的越多&#xff0c;抽中的概率越大&#xff0c;每一题都有详细的答题思路、详细的代码注释、样例测试…

uniapp微信小程序消息订阅详解

一、微信公众平台申请订阅模板 注意&#xff1a;订阅信息 这个事件 是 当用户 点击的时候触发 或者 是 支付成功后触发&#xff0c; 用户勾选 “总是保持以上选择&#xff0c;不再询问” 之后或长期订阅&#xff0c;下次订阅调用 wx.requestSubscribeMessage 不会弹窗&#xf…

爬虫的验证码处理

1.我们先进入chrome浏览器的审查页面找到input方法&#xff1a; 为了不少找到一个input&#xff0c;我们ctrlf的方法输入input来查找 看见我们有6个需要输入的参数。 除了上面几个的input参数&#xff0c;我们还需要获取验证码的图片&#xff0c;后续要将字母填入进去。 二.安…

【蓝桥杯】矩阵快速幂

一.快速幂概述 1.引例 1&#xff09;题目描述&#xff1a; 求A^B的最后三位数表示的整数&#xff0c;A^B表示&#xff1a;A的B次方。 2&#xff09;思路&#xff1a; 一般的思路是&#xff1a;求出A的B次幂&#xff0c;再取结果的最后三位数。但是由于计算机能够表示的数字…

Vue ElementPlus Form、Form-item 表单

Form 表单 由输入框、选择器、单选框、多选框等控件组成&#xff0c;用以收集、校验、提交数据&#xff0c;组件升级采用了 flex 布局&#xff0c;以替代旧版本的 float 布局。 典型表单 包括各种表单项&#xff0c;比如输入框、选择器、开关、单选框、多选框等。 在 Form 组件…

数据结构之单链表实现(JAVA语言+C语言)

一、理论 1 单链表结构 2 增、删、查 、改思路 &#xff08;增&#xff09;直接添加放到最后即可。按顺序添加&#xff1a;找到要修改的节点的前一个节点&#xff0c;插入新节点&#xff08;&#xff09;。&#xff08;改&#xff09;要修改的节点修改内容即可。&#xff08;…

03-MySQl数据库的-用户管理

一、创建新用户 mysql> create user xjzw10.0.0.% identified by 1; Query OK, 0 rows affected (0.01 sec) 二、查看当前数据库正在登录的用户 mysql> select user(); ---------------- | user() | ---------------- | rootlocalhost | ---------------- 1 row …

Docker:使用MinIO搭建对象存储平台

请关注微信公众号&#xff1a;拾荒的小海螺 1、简述 MinIO是一个基于对象存储技术的开源项目&#xff0c;它可以帮助用户快速搭建起私有的、高性能的对象存储平台。MinIO兼容Amazon S3 API&#xff0c;使得用户可以使用标准的S3工具和SDK来访问和管理MinIO存储的数据。此外&a…

查找--二分查找(Binary Search)

二分查找属于静态查找表&#xff0c;当以有序表表示静态查找表时&#xff0c;查找函数可用折半查找来实现。 查找过程&#xff1a;先确定待查记录所在的范围&#xff08;区间&#xff09;&#xff0c;然后逐步缩小范围直到找到或找不到该记录为止。 以处于区间中间位置记录的…

B样条曲线(记录)

B样条曲线的生成靠的两点&#xff1a; 1、控制点 2、基函数 B样条曲线的基函数是一个De Boor的递归表达式[1]&#xff1a; (1) (2) 其中是第个阶基函数。 而B样条曲线可以表示为[2]&#xff1a; (3) 如何理解上式&#xff1f;首先&#xff0c;我们知道&#xff0c;如果一个函数…

高端的电子画册,手机打开你见过吗?

手机阅读的高端电子画册&#xff0c;你见过吗&#xff1f;随着移动互联网的发展&#xff0c;越来越多的人选择在手机上阅读电子画册&#xff0c;而不是传统的纸质画册。这种趋势不仅节省了纸张资源&#xff0c;还提升了阅读体验。用户可以通过触摸屏幕、放大缩小、翻页等操作与…

【Blockchain】区块链浏览器 | 以太坊Etherscan比特币Blockchain门罗币Monero

区块链浏览器概述 区块链浏览器是一种软件,它使用API(应用程序编程接口)和区块链节点从区块链中提取各种数据&#xff0c;然后使用数据库来排列搜索到的数据&#xff0c;并以可搜索的格式将数据呈现给用户。 用户的输入是资源管理器上的可搜索项&#xff0c;然后通过数据库上…

empdll文件安装在哪里,详细的修复教程分享

在我们运行《荒野大镖客2》游戏的时候&#xff0c;有些玩家在游玩过程中可能会遇到emp.dll文件丢失的问题。此文件作为游戏运行过程中不可或缺的动态链接库&#xff08;DLL&#xff09;组件之一&#xff0c;丢失会导致游戏无法正常运行。小编将介绍5种解决emp.dll文件丢失的方法…

linux安装Zookeeper的详细步骤

1.Java环境确认 确保已经安装了Java环境&#xff0c;没有的自行安装 2.官网下载包 Apache ZooKeeper 3.安装 3.1上传到linux&#xff0c;解压 我的目录为/root/apache-zookeeper-3.8.4-bin 进入到/root/apache-zookeeper-3.8.4-bin/conf目录下&#xff0c;执行命令复制zoo…

C++2D原创我的世界1.00.3版本上市!!!

我很郁闷&#xff0c;为什么就是整不了昼夜交替啊喂&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 虽然这看上去很简单&#xff0c;但做起来要我命&#xff01;&#xff01;&#xff01; 优化过后总共1312行&#xff0c…

微信公众号迁移公证书在哪?

公众号迁移有什么作用&#xff1f;只能变更主体吗&#xff1f;很多小伙伴想做公众号迁移&#xff0c;但是不知道公众号迁移有什么作用&#xff0c;今天跟大家具体讲解一下。首先公众号迁移最主要的就是修改公众号的主体了&#xff0c;比如我们公众号原来是A公司的&#xff0c;现…

指针强化练习(详解)

更多学习内容 结构体内存对齐 和 位段-CSDN博客指针初级&#xff08;基础知识&#xff09;-CSDN博客指针进阶(深入理解)-CSDN博客 目录 1.sizeof与strlen的区别 2.一维数组 3.字符指针 4.二维数组 5.指针运算(笔试题) 6.函数指针 1.sizeof与strlen的区别 请思考以下运行结…

第1章.提示词:开启AI智慧之门的钥匙

什么是提示词&#xff1f; 提示词&#xff0c;是引导语言模型的指令&#xff0c;让用户能够驾驭模型的输出&#xff0c;确保生成的文本符合需求。 ChatGPT&#xff0c;这位文字界的艺术大师&#xff0c;以transformer架构为基石&#xff0c;能轻松驾驭海量数据&#xff0c;编织…