C++ 【set、map模拟实现】

news2024/11/15 11:45:23

目录

set概念

 set基本使用

map概念

map的使用

map统计次数

operator[]

operator[]底层如何实现?

set和map迭代器封装

红黑树迭代器基本结构

operator++

operator--

operator[]

源代码链接


map、set底层都使用平衡搜索树(即红黑树),容器中的元素是一个有序的序列。

数据结构【红黑树模拟实现】_北方留意尘的博客-CSDN博客

set概念

1.set和map都是关联式容器:

与序列式容器(vector、list、deque)不同的是,关联式容器也是用来存储数据的,其里面存储的是结构的键值对(键值对是用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value表示与key对应的信息,例如找到中文即可一一对应英文),在数据检索时比序列式容器效率更高。

2.在set中,元素的value就是key,是一个key模型(类型为模板参数T),每个value必须是唯一的(默认去重)。 set中的元素不能在容器中修改(否则会破坏树的结构),但是可以从容器中插入或删除它们。set是去重+排序(按照中序遍历是有序的)

3.模板中传compare仿函数,是方便控制“比较”规则,less默认用T对象本身比较大小(调用T的operator<),

如果T是自定义类型,默认不支持比较大小;或者需要指针解应用里面的内容来进行比较,可以自己写仿函数


 set基本使用

set.insert

在set中插入元素x,实际插入的是<x, x>,构成的键值对,如果插入成功,返回pair中<该元素在set中的位置,true>;如果插入失败,说明x在set中已经存在,返回<x在set中的位置,false>

set.erase

直接使用erase删除不存在的值,不会报错

直接erase等于下面find+判断erase

erase配合set中find来删除不存在的值,会导致程序崩溃

标题

原因在于find找不到对应值,返回end位置,erase删除了end位置。

正确做法是应该判断pos != end()再进行删除

set和multiset的区别在于:

multiset中的find查找的是所有重复元素,返回的是中序的第一个重复元素;

multiset直接erase删除的是所有重复元素,multiset中的find+erase是删除一个元素

count

count并不是为set准备的,而是为multiset(允许键值冗余)准备的,用于查找重复元素个数

但是实际也可以用count查找在不在,返回bool

lower_bound/upper_bound

 lower_bound返回的是大于等于这个值的位置

upper_bound是开区间,返回大于这个值的位置(给60返回70)

void test_set1()
{
	std::set<int> myset;
	std::set<int>::iterator itlow, itup;

	for (int i = 1; i < 10; i++) 
		myset.insert(i * 10); // 10 20 30 40 50 60 70 80 90

	itlow = myset.lower_bound(35);                //       ^
	itup = myset.upper_bound(60);      
	cout << *itlow <<" " << *itup << endl;//40 70
}

equal_range:是一种二分查找算法,试图在已排序的[first,last)中寻找value

equal_range结构是pair键值对,给的是一段范围区间

左区间存在pair.first中,右区间存在pair.second中,区间范围是 first <= val < second (左闭右开)

void test_set2()
{
	set<int> myset;

	for (int i = 1; i <= 5; i++) myset.insert(i * 10);// myset: 10 20 30 40 50

	pair<set<int>::const_iterator, set<int>::const_iterator> ret;
	ret = myset.equal_range(30);

	cout << "the lower bound points to: " << *ret.first << '\n';//30
	cout << "the upper bound points to: " << *ret.second << '\n';//40
}


map概念

1.map按照key来比较大小和存储元素,map是由键值key和值value组合,map中的元素总是按照键值key进行比较排序的

2.键值key用于排序和唯一标识元素,而值value中存储与此键值key关联的内容。key与value通过结构体pair绑定在一起

Key就是Key,T就是value

3. map不允许键值冗余,插入是根据key来判断,value改变而key不改变也无法插入,跟value无关

4. map中的key是唯一的,并且不能修改

5.map支持下标访问符,即在[]中放入key,就可以找到与key对应的value

6.仿函数Compare: map中的元素是按照key来比较的,缺省情况下按照小于比较,一般情况下(内置类型元素)该参数不需要传递,如果无法比较时(自定义类型),需要用户自己显式传递比较规则(一般情况下用函数指针或者仿函数来传递)

map的使用

map.insert

在map中插入键值对val,注意val是一个键值对,返回值也是键值对:返回值的iterator代表新插入元素的位置/已经存在元素的位置,bool代表是否插入成功 

map.insert并不是直接insert两个值

void test_map()
{
	map<string, string> dict;
	dict.insert("左边","left");//错误写法
}

而是要求给一个pair,而pair有对应的构造函数

void test_map()
{
	map<string, string> dict;
	
    //第一种写法,麻烦
    pair<string, string> dt("left", "左边");
	dict.insert(dt);

    //第二种写法
    dict.insert(pair<string, string>("right", "右边"));//隐式类型转换+匿名对象构造
}

我们也可以使用make_pair函数,自动帮我们推导模板参数,不需要显示传参

void test_map()
{
	map<string, string> dict;
	//pair<string, string> dt("left", "左边");
	dict.insert(make_pair("left","左边"));
}

make_pair函数模板

迭代器

直接使用*it拿pair,会提示pair不支持流插入

为了可以同时拿到key和value才设置成结构体,*it拿到的是pair,拿key对应it->first,拿value对应it->second(结构体时使用->)

void test_map()
{
	map<string, string> dict;
	//pair<string, string> dt("left", "左边");
	dict.insert(make_pair("left","左边"));
	dict.insert(pair<string, string>("right", "右边"));//隐式类型转换+匿名对象
	dict.insert(pair<string, string>("right", "xx"));//隐式类型转换+匿名对象
	auto it = dict.begin();
	while (it != dict.end())
	{
		//cout << (*it).first<<(*it).second << " ";
		cout << it->first<<it->second << "  ";
		++it;
	}
}

map统计次数

void test_map1()
{
	string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉" };
	map<string, int> CountMap;
	for (auto& s : arr)
	{
		map<string,int>::iterator it = CountMap.find(s);
		if (it != CountMap.end())
		{
			it->second++;//如果map中存在,则++次数
		}
		else
		{
			CountMap.insert(make_pair(s, 1));
            //如果map中不存在,新增对应水果名称,次数为1
		}
	}
	auto it = CountMap.begin();
	while (it != CountMap.end())
	{
		//cout << (*it).first<<(*it).second << " ";
		cout << it->first << it->second <<endl;
		++it;
	}
}

operator[]

如果 k 与容器中元素的键匹配,则该函数返回对其映射值value的引用。 

operator[]介绍:如果 k 与容器中任何元素的键不匹配,则该函数将插入具有该键的新元素,并返回对其映射值的引用。请注意,这始终将容器大小增加 1,如果没有为元素分配映射值,映射元素使用默认构造函数构造。
 

探讨[]

void test_map2()
{
	map<string, string> dict;
	dict.insert(make_pair("left", "左边"));
	dict["left"];
	dict["right"];
	dict["left"] = "right";
}
返回引用可以修改

[]有以下功能:

1.如果map中有存在的key,返回value引用,可以用作查找value、修改value

2.如果map中不存在key,会插入新元素pair<key,value()>,调用其value默认构造函数,返回value引用,可以充当插入+修改

operator[]底层如何实现?

mapped_type& operator[] (const key_type& k)
{
    return (*((this->insert(make_pair(k,mapped_type()))).first)).second;
}

以下解释来自网站:

https://cplusplus.com/reference/map/map/insert/

解释

insert返回值是pair<iterator,bool>

first首先设置一个迭代器,

如果插入key不在map中,将该迭代器指向新插入的元素。则将对中的第二个元素pair设置为true。pair(new_iterator,true);

如果已存在key,将该迭代器指向跟key相等的元素。则将其设置为false。pair(key_iterator,false)

所以insert插入失败还充当一个查找的功能

pair<iterator,bool> insert (const value_type& val);

按照上面说法,insert和operator[]有很多重复功能,模拟实现operator[]直接调用insert

mapped_type& operator[] (const key_type& key)
{    
    pair<iterator,bool> ret = insert(make_pair<key,V()>);
    return ret.first->second;
}

解释:构造一个pair,调用insert函数,如果key在,ret.first就是key位置的迭代器,默认构造函数没起作用,ret.first就是取pair迭代器位置,pair.second就是取对应的value

如果key不在,新建一个key位置迭代器元素,返回新插入元素,取默认构造函数。

利用operator[]实现统计次数

void test_map1()
{
	string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉" };
	map<string, int> CountMap;
	for (auto& s : arr)
	{
		CountMap[s]++;
	}
	auto it = CountMap.begin();
	while (it != CountMap.end())
	{
		//cout << (*it).first<<(*it).second << " ";
		cout << it->first << it->second <<endl;
		++it;
	}
}

 

第一次对象不在map中,插入+修改,使用pair<key,int()>;第二次对象在map中,返回value值++

set和map迭代器封装

set和map的底层共用同一颗红黑树,通过传入不同实例化参数来实现(体现复用性)

底层的RBTree用一个模板接收,上层的set和map通过传入不同的value(set传K,map传键值对pair<K,V>)给RBTree实例化不同的树,其中RBTreeNode节点中只包含了_data,所以在insert等操作时,需要配合仿函数使用(重载)

迭代器begin()与end()代表的是一段前闭后开的区间,而对红黑树进行中序遍历, 可以得到一个有序的序列

因此:begin()可以放在红黑树中最小节点(即最左侧节点)的位置,end()放在最大节点(最右侧节点)的下一个位置

红黑树迭代器基本结构

template<class T,class Ref,class Ptr>
struct __IteratorRBTree
{
	typedef RBTreeNode<T> Node;
	typedef __IteratorRBTree<T, Ref, Ptr> it;
	Node* _node;

	__IteratorRBTree(Node* node)
		:_node(node)
	{}

	Ref operator*()
	{
		return _node->_data;
	}

	Ptr operator->()
	{
		return &_node->_data;
	}

	bool operator!=(const it& r) const
	{
		return _node != r._node;//节点指针比较
	}

	bool operator==(const it& r) const
	{
		return _node == r._node;//节点指针比较
	}
};

operator++

如果在迭代器中嵌套栈辅助完成,消耗巨大。

由于三岔链可以找parent,operator++中序遍历是有序的,我们可以总结出规律:

1.首先需要找到最左节点(为中序遍历的第一个节点),例如此时找到节点1,1所在节点代表其左节点已经全部遍历完,此时只需要看1的右子树,就要分情况讨论:1的右子树不为空,++就是找右子树中序遍历的最左节点,此时为6。

2.此时6左右子树都为空,但是6是1的右子树,代表1也早就访问结束(中序决定),此时需要访问1的parent8。此时++找父亲的parent,总结为++找孩子不是父亲右的那个祖先(持续寻找)

3.找到最后一个节点27后,继续++会沿着三岔链找到13的parent,此时parent为nullptr

    it& operator++()
	{
		if (_node->_right)
		{
			Node* left = _node->_right;
			while (left->_left)//右子树最左节点
			{
				left = left->_left;
			}
			_node = left;
		}
		else//找孩子不是父亲右的那个祖先(持续寻找)
		{
			Node* parent = _node->_parent;
			Node* cur = _node;
			if(_node == parent->_left)
			{
				_node = _node->_parent;
			}
			else
			{
				while (parent && parent->_right == cur)
				{
					parent = parent->_parent;
					cur = cur->_parent;
				}
				_node = parent;
			}
		}
		return *this;
	}

operator--

operator--和operator++反过来:左子树 <-- 根 <--右子树,关注点在于左子树是否为空

1.左子树不为空,访问左子树最右节点

2.左子树为空,找孩子不是父亲左的那个祖先

it& operator--()
	{
		if ( _node->_left)//不需要判断_node是否为空,如果传空begin构造空迭代器
		{
			Node* right = _node->_left;
			while (right->_right)
			{
				right = right->_right;
			}
			_node = right;
		}
		else//左子树为空
		{
			Node* parent = _node->_parent;
			Node* cur = _node;

			while (parent && parent->_left == cur)
			{
				parent = parent->_parent;
				cur = cur->_parent;
			}
			_node = parent;
		}
		return *this;
	}

operator[]

operator[]底层用insert实现,返回值为pair<iterator,bool>

插入时数据存在,insert返回所在迭代器位置,返回false

插入时数据不存在,insert返回新插入位置,返回true

K/V模型才有[],还没实例化时无法确定是否为K/V,因此需要在map封装[]

		pair<iterator, bool> insert(const pair<K, V>& kv)
		{
			return _root.insert(kv);
		}
		
		V& operator[](const K& key)
		{
			pair<iterator, bool> tmp = insert(make_pair(key, V()));
			//传V缺省值,如果是int为0,自定义类型调默认构造函数
			return tmp.first->second;//首先取pair的迭代器iterator,再取K/V的second
		}


源代码链接

刷题+代码: 刷题+代码 - Gitee.com

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

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

相关文章

HanLP 基于朴素贝叶斯 训练 文本分类

一、HanLP 朴素贝叶斯分类器 HanLP 针对文本分类算法已经帮我们实现 朴素贝叶斯法 &#xff0c;用户可以无需关心内部细节&#xff0c;HanLP 也提供了相关自定义训练接口&#xff0c;前提需要将数据集根据分类放到不同的目录中&#xff0c;例如&#xff1a; 官方给出了相关性能…

HanLP 基于SVM支持向量机 训练 文本分类

一、HanLP 基于SVM支持向量机分类器 上篇文章通过朴素贝叶斯文本分类器&#xff0c;训练测试了 搜狗文本分类语料库迷你版 &#xff0c;本篇继续测试SVM支持向量机分类器。 由于HanLP 官方给出的 SVM 分类器依赖了第三方库&#xff0c;没有集成在主项目中&#xff0c;需要拉取…

问题解决(1)——VS中scanf报错怎么解决

目录 方法一&#xff1a; 方法二&#xff1a; 方法三&#xff1a; 各位好&#xff0c;博主新建了个公众号《自学编程村》&#xff0c;拉到底部即可看到&#xff0c;有情趣可以关注看看哈哈&#xff0c;关注后还可以加博主wx呦~~~&#xff08;公众号拉到底部就能看到呦~~&am…

Redis【13】-修改数据库后,如何保证Redis与数据库的数据一致性

一、需求起因 在高并发的业务场景下&#xff0c;数据库大多数情况都是用户并发访问最薄弱的环节。所以&#xff0c;就需要使用redis做一个缓冲操作&#xff0c;让请求先访问到redis&#xff0c;而不是直接访问MySQL等数据库。 这个业务场景&#xff0c;主要是解决读数据从Redi…

ARM 代码重定位实战

前言 任务 在 SRAM 中将代码从 0xd0020010 重定位到 0xd0024000。任务解释&#xff1a;本来代码是运行在0xd0020010的&#xff0c;但是因为一些原因我们又希望代码实际是在0xd0024000位置运行 的。这时候就需要重定位了。注解&#xff1a;本练习对代码本身运行无实际意义&…

你都工作两年半了,还不会RabbitMQ?

What is rabbitMQ &#xff1f; RabbitMQ 是一个由 Erlang 语言开发的 AMQP(高级消息队列协议) 的开源实现。 RabbitMQ 是轻量级且易于部署的&#xff0c;能支持多种消息协议。 RabbitMQ 可以部署在分布式和联合配置中&#xff0c;以满足高规模、高可用性的需求。 具体特点包括…

ADI Blackfin DSP处理器-BF533的开发详解29:TOUCH_LINE(屏幕画线)(含源代码)

硬件准备 ADSP-EDU-BF533&#xff1a;BF533开发板 AD-HP530ICE&#xff1a;ADI DSP仿真器 软件准备 Visual DSP软件 硬件链接 硬件设计原理图 功能介绍 代码实现了读取触摸屏坐标&#xff0c;并将触摸屏坐标换算为液晶屏的显示坐标&#xff0c;将像素点显示到触摸坐标的位…

学习Python中turtle模块的基本用法(4:绘制科赫曲线和谢尔宾斯基三角形)

科赫曲线和谢尔宾斯基三角形是常见的分形图形&#xff08;详细介绍见参考文献1&#xff09;&#xff0c;本文使用turtle库绘制这两类图形。 科赫曲线 科赫曲线的详细介绍见参考文献2&#xff0c;其中的绘图思路是“画正三角形&#xff0c;并把每一边三等分,取三等分后的一边中…

【LeetCode】Day194-超级丑数

题目 313. 超级丑数【中等】 题解 之前做过丑数&#xff0c;规定丑数是质因数只包含2,3,5的正整数&#xff0c;而这道题丑数升级为超级丑数&#xff0c;规定为包含的质因数是在primes数组中的正整数 丑数的题解用动态规划&#xff0c;那么超级丑数也可以利用相同的方法解答…

CSS -- CSS元素显示模式总结(块元素,行内元素,行内块元素)

文章目录CSS 的元素显示模式1 什么是元素显示模式2 块元素3 行内元素4 行内块元素5 元素的显示模式总结CSS 的元素显示模式 1 什么是元素显示模式 作用&#xff1a;网页的标签非常多&#xff0c;在不同地方会用到不同类型的标签&#xff0c;了解他们的特点可以更好的布局我们…

[附源码]JAVA毕业设计-学生宿舍故障报修管理信息系统-(系统+LW)

[附源码]JAVA毕业设计-学生宿舍故障报修管理信息系统-&#xff08;系统LW&#xff09; 项目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff…

开源即巅峰,《Java程序性能优化实战》GitHub三小时标星已超34k

蓦然回首自己做开发已经十年了&#xff0c;这十年中我获得了很多&#xff0c;技术能力、培训、出国、大公司的经历&#xff0c;还有很多很好的朋友。但再仔细一想&#xff0c;这十年中我至少浪费了五年时间&#xff0c;这五年可以足够让自己成长为一个优秀的程序员&#xff0c;…

项目设置分页条件查询接口

一、分页 1、HospPlusConfig中配置分页插件1 /** 2 * 分页插件 3 */ 4 Bean 5 public PaginationInterceptor paginationInterceptor() { 6 return new PaginationInterceptor(); 7 }2、分页Controller方法 HospitalSetController中添加分页方法1 ApiOperation(value "分…

Python学习基础笔记四十二——序列化模块

1、序列化的概念&#xff1a; 序列&#xff1a;就是字符串。 序列化&#xff1a;将原本的字典、列表等内容转换成一个字符串数据类型的过程就叫做序列化。 反序列化&#xff1a;从字符串到数据类型的过程。 2、序列化的目的&#xff1a; 1、以某种存储形式使自定义的数据持…

servlet+Mysql实现的校园论坛管理系统(功能包含登录,首页帖子查看、发帖、个人帖子删除编辑、帖子评论回复、用户管理等)

博客目录servletMysql实现的校园论坛管理系统实现功能截图系统功能使用技术代码完整源码servletMysql实现的校园论坛管理系统 本系统是一个简单的校园论坛系统&#xff0c;学生可以在线发帖并进行帖子评论回复&#xff0c;同同时管理员可以对用户进行管理。 (文末查看完整源码…

win11: cmake+glfw+imgui

下载源码&#xff1a;imgui github地址 将需要的文件拖拽入项目外部库的imgui文件夹 backends文件夹里选择与环境适配的文件&#xff0c;我这里用了glfw和opengl3 目录结构&#xff1a; CMakeLists.txt cmake_minimum_required(VERSION 3.24) project(proforlearn) set(CM…

基于java+springmvc+mybatis+jsp+mysql的实验室计算机故障报修系统

项目介绍 本系统采用java语言开发&#xff0c;后端采用ssm框架&#xff0c;前端采用jsp技术&#xff0c;数据库采用mysql进行数据存储。 前端页面&#xff1a; 功能&#xff1a;首页、设备信息、公告资讯、个人中心、后台管理、联系客服 管理员后台页面&#xff1a; 功能&…

电子学会2020年12月青少年软件编程(图形化)等级考试试卷(二级)答案解析

目录 一、单选题&#xff08;共25题&#xff0c;每题2分&#xff0c;共50分&#xff09; 二、判断题&#xff08;共10题&#xff0c;每题2分&#xff0c;共20分&#xff09; 三、编程题【该题由测评师线下评分】&#xff08;共2题&#xff0c;共30分&#xff09; 青少年软件…

业务流程监控:让多维度监控有了灵魂

需求 《可视化业务流程监控&#xff0c;是解决方案更是运维之道&#xff01;》一文让我们知道可以借助Grafana 两个插件&#xff1a;Diagram、FlowCharting&#xff0c;满足我们对于图形数据业务流程的可视化监控&#xff0c;但是在使用前需要我们做好以下两点工作&#xff1a…

修复自定义标题word题注错误:错误,文档中没有指定样式的文字以及编号无法随章节变化问题

一、单个修复指定章节号 假设采用自定义样式“毕业”&#xff0c;如果出现类似提示&#xff0c;可以具体操作如下&#xff1a; 光标定位与错误题注的位置&#xff0c;按shift F9 {STYLEREF 1 \s} - 2 将内容修改为 图 {STYLEREF "毕业" \s} - 2 右击&#xff0c;更…