【C++】模拟实现map和set(用红黑树进行封装)

news2024/11/25 12:53:58

模拟实现map和set

  • 前言
  • 正式开始
    • 简单框架
    • data的比较
    • 迭代器
    • operator++
    • operator-\-
    • [ ]重载

在这里插入图片描述

前言

本篇以前一篇红黑树模拟实现插入功能为基础:【C++】红黑树模拟实现插入功能(包含旋转和变色)

本篇中不会再讲解关于旋转和变色的知识。只是对于红黑树进行简单的封装。

如果你在此之前已经对于红黑树的旋转和变色很了解了,就不需要再看前一篇了,但若没了解过的话,建议先看看前一篇,前一篇中的内容懂了才能看下面的内容。

我先将前一篇中实现的代码放这,各位可以先不看,我讲到要修改的时候再看也不迟。

#pragma once

enum color
{
	RED,
	BLACK
};

template<class K, class V>
struct RBTreeNode
{
	RBTreeNode(const pair<K, V>& kv = make_pair(K(), V()))
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_kv(kv)
	{}

	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;

	pair<K, V> _kv;
	color _col;
};

template<class K, class V>
class RBTree
{
	typedef RBTreeNode<K, V> Node;

public:
	bool Insert(const pair<K, V>& kv)
	{
		// 树若为空,就让根直接指向新节点,再让根节点的颜色变为黑色
		if (_root == nullptr)
		{
			_root = new Node(kv);
			_root->_col = BLACK;
			return true;
		}

		// 找到合适的插入位置
		Node* cur = _root;
		Node* parent = nullptr;
		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 (kv.first < parent->_kv.first)
			parent->_left = cur;
		else if (kv.first > parent->_kv.first)
			parent->_right = cur;
		else
			assert(false);

		cur->_parent = parent;

		// 插入结束,开始调整树结构
		cur->_col = RED; // 插入节点一定要为红色
		while (parent && parent->_col == RED) 
			// 父节点存在且为红时才需要调整。
		{
			// 当父节点为红色时,爷爷节点一定为黑色
			Node* grandParent = parent->_parent;
			assert(grandParent);
			assert(grandParent->_col == BLACK);
			Node* uncle = nullptr;

			if (parent == grandParent->_left)
				uncle = grandParent->_right;
			else
				uncle = grandParent->_left;

			if (uncle && uncle->_col == RED)
			{
				parent->_col = uncle->_col = BLACK;
				grandParent->_col = RED;
				cur = grandParent;
				parent = cur->_parent;
			}
			else // uncle不存在 或 uncle存在且为黑
			{
				if (parent == grandParent->_left)
				{
					if (cur == parent->_left) // 单边 左左 ==》右单旋
					{
						RotateR(grandParent);
						parent->_col = BLACK;
						grandParent->_col = RED;
					}
					else // 左 右 ==》左右双旋
					{
						RotateL(parent);
						RotateR(grandParent);
						cur->_col = BLACK;
						grandParent->_col = RED;
					}
				}
				else // parent == grandParent->_right
				{
					if (cur == parent->_right) // 单边 右右 ==》左单旋
					{
						RotateL(grandParent);
						parent->_col = BLACK;
						grandParent->_col = RED;
					}
					else // 右左 ==》右左双旋
					{
						RotateR(parent);
						RotateL(grandParent);
						cur->_col = BLACK;
						grandParent->_col = RED;
					}
				}
				// 只要旋转过后就平衡了
				break;
			}
		}

		_root->_col = BLACK;
		return true;
	}

	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

	bool IsBalance()
	{
		// 空树是平衡的
		if (_root == nullptr)
			return true;

		// 根为黑(第一条)
		if (_root->_col == RED)
			return false;

		// 找到最左边路径中黑色节点个数
		// 以该个数为基准,用来比较其他路径的黑色节点
		Node* cur = _root;
		int blackNum = 0;
		while (cur)
		{
			if (cur->_col == BLACK)
				++blackNum;

			cur = cur->_left;
		}

		// 判断二三四条
		return _IsBalance(_root, 0, blackNum);
	}

private:
	bool _IsBalance(Node* root, int blackCount, int& blackNum)
	{
		if (root == nullptr)
		{
			// 判断当前路径黑节点个数是否与其他相同
			if (blackCount != blackNum)
				return false;
			else
			return true;
		}

		//  是黑色节点就让blackCount++
		if (root->_col == BLACK)
			++blackCount;

		// 不能有连续黑色节点
		if (root->_col == RED && root->_parent->_col == RED)
			return false;

		// 节点非黑即红
		if (root->_col != BLACK && root->_col != RED)
			return false;

		// 继续左右树
		return _IsBalance(root->_left, blackCount, blackNum)
			&& _IsBalance(root->_right, blackCount, blackNum);
	}

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

		_InOrder(root->_left);
		cout << root->_kv.first << ":::" << root->_kv.second << endl;
		_InOrder(root->_right);
	}

	void RotateL(Node* parent)
	{
		Node* ppNode = parent->_parent;
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;

		subR->_left = parent;
		parent->_parent = subR;

		if (ppNode)
		{
			if (ppNode->_left == parent)
				ppNode->_left = subR;
			else
				ppNode->_right = subR;

			subR->_parent = ppNode;
		}
		else
		{
			_root = subR;
			subR->_parent = nullptr;
		}
	}

	void RotateR(Node* parent)
	{
		Node* ppNode = parent->_parent;
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;

		subL->_right = parent;
		parent->_parent = subL;

		subL->_parent = ppNode;
		if (ppNode)
		{
			if (ppNode->_left == parent)
				ppNode->_left = subL;
			else
				ppNode->_right = subL;
		}
		else
		{
			_root = subL;
			subL->_parent = nullptr;
		}
	}

private:
	Node* _root = nullptr;
};

正式开始

首先,STL库中map和set底层就用的是红黑树,但是map是k/v模型的,set是k模型的,库中是实现了两棵红黑树吗?

不是的,STL库是非常注重代码的复用性的,不会说实现两棵红黑树的。

我们自己模拟实现之前先来看看STL库中是样搞的:

对于map和set另个头文件而言:
在这里插入图片描述

对于stl_tree.h而言:

红黑树的节点颜色:
在这里插入图片描述
这里用的是bool类型的,0表示RED,1表示BLACK。我前一篇的模拟实现中是用枚举来搞的,和这里不太一样,但是都可以。

树节点:
在这里插入图片描述
在这里插入图片描述
库里面是先实现了一个除存放值以外的指针等节点内容的结构体__rb_tree_node_base,然后再搞了一个结构体__rb_tree_node来继承前一个结构体,在__rb_tree_node内部定义值域,并用模版来使这个值域泛型化,从而使得当模版参数传的是k模型的时候就是set,传pair的时候就是map。

树:
在这里插入图片描述

这样就实现出了一棵泛型结构的rbtree,通过不同实例化参数,实现出map和set。

对于stl_set.h和stl_map.h而言:
在这里插入图片描述

这样就可以动手搞了。

为了和库中的map和set区别,我就将模拟实现的文件命名为 Map.h 和 Set.h 。

简单框架

先把红黑树改改。
我前面直接模拟实现的是 key/value 模型的,但是set是k模型的,所以要将存储的数据类型改成模版,而非直接是pair。那么我就直接用T来表示了。

树节点:
在这里插入图片描述

树:
在这里插入图片描述

要改的地方不多,就是把所有原来用到模版V的地方改为T,然后将用到kv的地方改为data就好了。

上面我还圈出了用data直接比较的地方,这里直接用大于小于号进行比较是错误的,因为不知道data的类型是key还是key/value,所以直接比的话就出问题了。库中虽然重载了pair的大于小于号,但是并不是我们想用的,我们是想直接比较first就行了, 库中的是比较完first还会比second,所以我们得另寻他路。但是这里还不能讲,得等会在Map.h和Set.h中写点东西才能改。

先给Map和Set打框架:

Map

在这里插入图片描述

Set

在这里插入图片描述

可能有同学要问,为啥还要有第一个模版参数K呢?

因为插入元素或者查找元素的时候都需要用到key,而当我们只搞一个V时,就会导致map传
pair的时候没法得到key的类型而导致无法比较。

我刚刚也提到了直接用data比较会导致错误,现在我们来解决一下。

data的比较

我们可以参考一下库中是怎么搞的。
在这里插入图片描述
库中是用第三个模版参数来解决这个问题的,库里面的原理就不说了,我这里直接实现。

第三个参数是仿函数,KeyOfValue,意思就是提取中value中的key,对于set而言可以直接接将仿函数的返回值给其value,因为set的value就是key。而map则需要返回其元素对应pair
中的key。

所以对于set而言:
在这里插入图片描述

对于map而言:
在这里插入图片描述

上面传KeyOfValue的用法是将data传给()重载,map的data就是pair,set的data就是key,然后就返回对应的key值。

然后再改一下红黑树中用到data的地方:

先在用到data比较的函数中定义一个KeyOfValue的对象:
在这里插入图片描述
然后再给每一个用到data且进行比较的地方都添上kov()
在这里插入图片描述
这样就好了。

我们在map和set中封装一下insert:
在这里插入图片描述

在这里插入图片描述

测试一下:

map:
在这里插入图片描述

set:
在这里插入图片描述

迭代器

上面我们没法直接打印信息,只能通过调试简单看一下,因为迭代器还没实现。

下面就搞搞迭代器。

还是,红黑树的迭代器就是map和set的迭代器。而红黑树迭代器的实现可以参考参考链表的迭代器。模版参数也是T, Ref, Ptr这三个。

基本框架:
在这里插入图片描述

迭代器,无非那几个功能。
我们这里先实现一下*、->、==、!=。

在这里插入图片描述

==传参的时候要传一个迭代器,类型太长了,重命名一下:
在这里插入图片描述

在这里插入图片描述

然后在RBTree中封装一下:
在这里插入图片描述
写begin和end时要确定一下begin和end分别指向哪里。

库中是这样搞的:
在这里插入图片描述
上面的header就相当于是双向带头循环链表中的哨兵位头结点,begin就是树最左侧的节点,end就是树最右侧的节点。begin的话就是header->_left,end就是header->_right。

但是我就不搞那个header了,不搞也是可以实现的:
在这里插入图片描述

那么这里begin就是最左侧的节点,end给空指针就行。
在这里插入图片描述

在Map中我们需也要封装一下其迭代器:
在这里插入图片描述
注意上面用到了typename关键字,就是为了标识出红黑树中的iterator是一个类型名,而非静态成员,因为静态成员也可直接通过类域访问。

还需要加上begin和end:
在这里插入图片描述

然后距离迭代器遍历整棵树还差一步,就是++和--。其实++就够了。

operator++

这里要稍微想一想。

红黑树遍历就指的是中序遍历,这样打印出来的东西是有序的。

但是我们这里不是遍历了,而是访问一个节点一个节点的走。怎样实现呢?

想一想,中序遍历,访问到一个节点是,该节点的左子树一定已经访问过了,此时访问该节点之后,就要访问右子树了。

那么右子树可以分两种情况。

非空

非空的话,按照中序遍历的顺序,应该是直接跑到右子树中最左边的节点。

空的话,按照中序遍历的顺序,应该跑到其祖先路径上未被访问过的节点。如果祖先路径都被访问过了,那么就是这棵树已经遍历完了。
在这里插入图片描述
想要控制这一点的话,就得不断向上寻找到一个祖先里面孩子不是祖先的右子树的那个祖先节点。因为左根右的顺序,往上寻找的话,根的右一定已经访问过了,所以就是要找根的左的节点。
.
如上图中的7。5、6均已在7之前访问过了,当前为7的话,++就应该跑到8的位置,也就是说不断向祖先节点寻找,7是6的右,6被访问过了,继续看8,6是8的左,8未被访问,就该跑到8的位置了。
.
而图中的15,一路向上,都是父的右,直到8上面为空时,找不到符合条件的祖先节点了。此时就应该停止,operator++返回空指针对应的节点即可。

那我们就按照上面的逻辑实现一下:
在这里插入图片描述

测试:
在这里插入图片描述

再来看看set:
在这里插入图片描述

在这里插入图片描述

operator--

--的话,就是++倒着来就行。

逻辑反过来,看左子树就行。

实现一下:
在这里插入图片描述

[ ]重载

再来说一下[ ]重载。

这个对于map来说非常有用。也只有map能用,前面map和set介绍的那一篇我也讲了,[ ]的返回值,这里不再细说了,不懂的同学点传送门:【C++】STL map和set用法基本介绍。

首先我们要把红黑树中的insert返回值改一下。改成pair<iterator, bool>,然后内部返回值的细节再改改:

在这里插入图片描述

然后map和set中的封装也要改:
在这里插入图片描述

然后再在map中重载一下[ ]。
在这里插入图片描述

测试:

迭代器走一遍:
在这里插入图片描述

范围for走一遍:
在这里插入图片描述

erase还是不讲,本篇就只讲一下插入对应的封装。迭代器中有些功能也可以实现但这里没有给,比如说后置++、--等等。红黑树中的find等。如果感兴趣的同学可自行查找资料进行学习。

到此结束。。。

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

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

相关文章

java操作mongdb【超详细】

Java操作 搭建 搭建 依赖 <!--mongodb--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-mongodb</artifactId></dependency>配置文件 spring:data:mongodb:host…

opencv图片换背景色

#include <iostream> #include<opencv2/opencv.hpp> //引入头文件using namespace cv; //命名空间 using namespace std;//opencv这个机器视觉库&#xff0c;它提供了很多功能&#xff0c;都是以函数的形式提供给我们 //我们只需要会调用函数即可in…

烧写PYNQ镜像到SD卡

一&#xff0c;安装 Win32diskimager 首先将Micro SD卡插入读卡器的卡槽中&#xff0c;然后再将读卡器插入计算机USB接口&#xff0c;此时计算机将会识别到插入的可移动磁盘。双击打开Win32DiskImager-1.0.0.zip 压缩文件&#xff0c;里面win32diskimager-1.0.0-install.exe文…

ORCA优化器浅析——CDXLOperator Base class for operators in a DXL tree

如上图所示&#xff0c;CDXLOperator作为Base class for operators in a DXL tree&#xff0c;其子类CDXLLogical、CDXLScalar、CDXLPhysical作为逻辑节点、物理节点和Scalar节点的DXL表示类&#xff0c;因此其包含了这些类的共同部分特性&#xff0c;比如获取其DXL节点表示的函…

CNN之图像识别

文章目录 1. 图像识别1.1 模式识别1.2 图像识别的过程1.3 图像识别的应用 2. 深度学习发展2.1 深度学习为何崛起2.2 分类与检测2.3 常见的卷积神经网络 3. VGG3.1 VGG163.2 VGG16的结构&#xff1a;3.3 使用卷积层代替全连接3.4 1*1卷积的作用3.5 VGG16代码示例 4. 残差模型-Re…

ctfshow-web8

0x00 前言 CTF 加解密合集CTF Web合集 0x01 题目 0x02 Write Up 这道题实际上就是一个单纯的布尔型盲注&#xff0c;只不过是过滤了一些东西&#xff0c;一个是过滤的空格&#xff0c;还有一个是过滤了逗号 那么我们需要做的就是对这两个进行绕过&#xff0c;空格还是用/**…

Spring系列五:手动实现Spring底层机制

文章目录 &#x1f35d;类加载器和classpath详解 &#x1f497;实现任务阶段1&#x1f35a;编写自己Spring容器, 扫描包得到bean的class对象 &#x1f497;实现任务阶段2&#x1f35a;扫描将bean信息封装到BeanDefinition对象, 并放入到Map &#x1f497;实现任务阶段3&#x1…

Unity 鼠标控制 UI 放大、缩小、拖拽

文章目录 1. 代码2. 测试场景 1. 代码 using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems;public class UIDragZoom : MonoBehaviour, IDragHandler, IScrollHandler {private Vector2 originalSize;private Vector2 originalPosition;private RectTr…

一个案例:Vue2组件化开发组件从入门到入土

1. 环境搭建 1.1. 创建项目 npm install -g vue/clivue create vue_study_todolist1.2. 清空项目代码 清楚HelloWorld.Vue代码中的内容。 1.3. 启动空项目 1.4 项目目标 项目组件实现以下效果 2. 组件拆分代码 Vue是一个基于组件的框架&#xff0c;允许您将界面拆分成小的…

【LeetCode】617.合并二叉树

题目 给你两棵二叉树&#xff1a; root1 和 root2 。 想象一下&#xff0c;当你将其中一棵覆盖到另一棵之上时&#xff0c;两棵树上的一些节点将会重叠&#xff08;而另一些不会&#xff09;。你需要将这两棵树合并成一棵新二叉树。合并的规则是&#xff1a;如果两个节点重叠…

了解 Langchain️是个啥?:第 1 部分

一、说明 在日常生活中&#xff0c;我们主要致力于构建端到端的应用程序。我们可以使用许多自动 ML 平台和 CI/CD 管道来自动化 ml 管道。我们还有像Roboflow和Andrew N.G.的登陆AI这样的工具来自动化或创建端到端的计算机视觉应用程序。 如果我们想在OpenAI或拥抱脸的帮助下创…

Mr. Cappuccino的第62杯咖啡——Spring之Bean的生命周期

Spring之Bean的生命周期 Aware接口项目结构项目代码运行结果源代码使用场景 InitializingBean接口项目结构项目代码运行结果源代码 BeanFactoryPostProcessor接口项目结构项目代码运行结果源代码 Bean的生命周期项目结构项目代码运行结果源代码 Aware接口 实现Aware接口是为了…

Kotlin 中的 Lambda 与 Inline

在Kotlin中&#xff0c;有很多很酷很实用的特性&#xff0c;比如Lambda和高阶函数&#xff0c;利用这些特性&#xff0c;我们可以更加快速的实现开发&#xff0c;提升效率。 比如我们实现一个捕获Throwable&#xff0c;安全执行部分代码的高阶函数 fun safeRun(runnable: () …

最强自动化测试框架Playwright(26)-对话框

page.on(dialog) playwright 框架可以监听dialog事件&#xff0c;不管你alert 什么时候弹出来&#xff0c;监听到事件就自动处理了。 当出现 JavaScript 对话框时发出&#xff0c;例如alert、prompt或。侦听器必须dialog.accept()或dialog.dismiss()对话框 - 否则页面将冻结等…

Promise.all 静态方法

合并多个 Promise 对象&#xff0c;等待所有同时成功完成&#xff08;或某一个失败&#xff09;&#xff0c;做后续逻辑 语法&#xff1a; <script>const p Promise.all([Promise对象, Promise对象, ...])p.then(result > {// result 结果: [Promise对象成功结果, P…

mac出现java程序运行版本不一致

mac出现java程序运行版本不一致 Burpsuite_pro_v2022版本Burpsuite_pro_v2023.6.2版本解决方案&#xff1a; Burpsuite_pro_v2022版本 在安装BurpSuite的时候&#xff1a; 执行启动程序&#xff1a; java -noverify -javaagent:BurpSuiteLoader.jar -jar burpsuite_pro.jar运…

Windows Hyper-V Ubuntu 22.04 LTS安装

文章目录 UbuntuHyper-V启动 Ubuntu 下载Ubuntu-Desktop&#xff0c;这是个iso文件。 Hyper-V 启用虚拟化支持 services.msc 打开服务列表&#xff0c;关注Hyper-V服务是否启动 打开管理器 创建虚拟机 右键管理器》连接到服务器》连接到本地 快速创建 用到下载的ubu…

打造企业或者个人IP引流法

打造企业或者个人IP引流法. 大家好&#xff0c;我是百收网SEO编辑&#xff1a;狂潮老师&#xff0c;今天给大家分享企业IP打造的方法 首先我们想让人知道你的企业叫什么&#xff0c;怎么找到你的企业 这个时候我们就需要去各大平台发布信息&#xff0c;客户想了解直接去搜索…

JAVA多线程案例——实现三个窗口卖票功能

一、需求分析 利用多线程的思想模拟三个窗口售票员卖30张票的功能&#xff1a; 我们采用线程对象来模拟售票窗口&#xff0c;实现多个窗口同时卖票&#xff0c; 采用 Runnable 接口子类来模拟票数。 二、代码实现 1、继承 Thread 类的方式 class TicketWindow extends Threa…

【Unity】VS Code 没有智能提示 Unity 中的类

正常来说&#xff0c;VS Code中会对部分输入类名进行提示&#xff0c;如下图所述 假如你从Unity 中进入 VS Code后发现没有提示相关 Unity的类&#xff0c;可能是 Unity 中 有关于 VS Code的相关Package 没有跟着 VS Code升级到最新版本。 点击Unity Windows 下拉框中的 Pac…