c++复习:map和set的使用

一、关联式容器的介绍

1、在学习map和set之前,我们接触到的容器有:vector 、list、stack、queue、priority_queue、这些容器统称为序列式容器,因为其底层为线性序列的数据结构,里面存储的是元素本身。

2、关联式容器也是用来存储数据的,但是它的不同之处在于它是以键值对的方式来存储的,<key,value>,而且检索的效率是要比序列式容器要高的

在STL中,总共实现了两种类型的关联性容器:树形结构与哈希结构,树型结构的关联式容器主要有四种:map、set、multimap、multiset。 这四种容器的共同点是:底层使用的是平衡搜索树(即红黑树)去实现的,容器中的元素是一个有序的序列。

有序关联容器的共同特点:

容器内部自动按 key 升序排列

查找:O(logN)

不支持随机访问迭代器(不能 iter + n),仅双向迭代器

插入 / 删除不会使其他迭代器失效

二、键值对的介绍

用来表示一一对应关系的一种结构,一般由两部分构成,一个是key,另一个是value,key是唯一的标识,用来查找,排序和匹配,value是我们真正存放的数据,依附于键而存在,序列式容器靠的是位置访问,而关联式容器靠的是键值对访问。比如说现在要建立一个英文词典,那么在字典中我们需要根据英文单词去查找对应的中文单词,那么英语单词与中文含义是一一对应的关系,通过键值对存储起来他们之间的一个映射关系,这样在词典中就可以找到对应的中文信息。而原生键值对就是pair<key,value>,下面我们对pair做一个剖析

先看代码结构:

cpp 复制代码
#include<iostream>
#include<string>
using namespace std;
namespace whx
{
	template<class T1, class T2>
	struct pair
	{
	public:

		typedef T1 first_type;
		typedef T2 second_type;
		first_type _first;
		second_type _second;
		//构造函数
		pair() :_first(T1()), _second(T2())
		{
		}
		//有参构造函数
		pair(const T1& a, const T2& b) :_first(a), _second(b)
		{
		}
		//拷贝构造函数
		pair(const pair<T1, T2>& p)
		{
			_first = p._first;
			_second = p._second;
		}
	};
}
	

//make_pair
template<class T1, class T2>
whx::pair<T1, T2> make_pair(const T1& a, const T2& b)
{
	return whx::pair<T1, T2>(a, b);
}
//比较相等还是不相等
template<class T1, class T2>
bool operator==(const whx::pair<T1, T2>& p1, const whx::pair<T1, T2>& p2)
{
	return p1._first == p2._first && p1._second == p2._second;
}
template<class T1, class T2>
bool operator!=(const whx::pair<T1, T2>& p1, const whx::pair<T1, T2>& p2)
{
	return !(p1 == p2);
}
//比较
template<class T1, class T2>
bool operator<(const whx::pair<T1, T2>& p1, const whx::pair<T1, T2>& p2)
{
	return p1._first < p2._first || (!(p2._first < p1._first) && p1._second < p2._second);
}
template<class T1, class T2>
bool operator>(const whx::pair<T1, T2>& p1, const whx::pair<T1, T2>& p2)
{
	return p2 < p1;
}
template<class T1, class T2>
bool operator>=(const whx::pair<T1, T2>& p1, const whx::pair<T1, T2>& p2)
{
	return !(p1 < p2);
}
template<class T1, class T2>
bool operator<=(const whx::pair<T1, T2>& p1, const whx::pair<T1, T2>& p2)
{
	return !(p1 > p2);
}

int main()
{
	pair<int, std::string> p1;
	pair<int, std::string> p2(1, "hello");
	auto p3 = make_pair(2, "C++");
	auto p4 = make_pair(1, "hello");
	std::cout << "p2 == p4 : " << (p2 == p4) << std::endl;
	std::cout << "p2 < p3  : " << (p2 < p3) << std::endl;
	std::cout << "p3 > p4  : " << (p3 > p4) << std::endl;
	std::cout << "p1 >= p2 : " << (p1 >= p2) << std::endl;
	return 0;
}

这是完整的pair,更加贴合标准库

三、set

3.1set的介绍

set的文档介绍:

  1. set是按照一定次序存储元素的关联性容器,关联容器依靠键值查找元素,区别于 vector、deque 这类顺序容器;容器内所有元素会持续维持有序状态元素插入、删除后依旧自动保持排序,不依赖插入顺序。底层依托平衡二叉搜索树管理数据,不能通过下标随机访问元素。

  2. 在set中,元素的value也标识它(value就是key,类型为T),并且每个value必须是唯一的。

set中的元素不能在容器中修改(否则会破坏搜索的规则,因为set 迭代器指向的元素被视作 const****),但是可以从容器中插入或删除它们。

  1. 在内部,set中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则(不允许存在相同的关键字)进行排序。

  2. set容器通过key访问单个元素的速度通常比unordered_set容器慢,但它们允许根据顺序对子集进行直接迭代。

std::set底层红黑树,查找、插入、删除复杂度 \(O(log_2n)\);

std::unordered_set底层哈希表,理想情况下平均复杂度 \(O(1)\)。

  1. set在底层是用二叉搜索树(红黑树)实现的。

注意:

  1. 与map/multimap不同,map/multimap中存储的是真正的键值对<key, value>,set中只放value,但在底层实际存放的是由<value, value>构成的键值对。

  2. set中插入元素时,只需要插入value即可,不需要构造键值对。

  3. set中的元素不可以重复(因此可以使用set进行去重)。

  4. 使用set的迭代器遍历set中的元素,可以得到有序序列

  5. set中的元素默认按照小于来比较(可以通过仿函数去改变)

  6. set中查找某个元素,时间复杂度为:log2n

3.2set的使用

第一个模版参数是key和value的类型,第二个是用来比较的仿函数,默认是升序,如果想要降序,可以用greater<T>,当然,也可以自己去写一个仿函数来改变逻辑。

3.2.1 构造函数

1.空的set:

2.迭代器的区间构造:

默认是升序

可以用仿函数将它变为降序!!!

3.拷贝构造

这里需要注意的是拷贝构造的前提是两者的set传入的模版一定要相同。

4.初始化列表构造

3.2.2 迭代器

vector和list一样的迭代器,但是要注意的是:

(1)该迭代器是一个双向迭代器(支持随机迭代器和双向迭代器进行迭代器区间构造)。

(2)我们看到的iterator其实本质上也是const_iterator (这里是为了保持接口一致性),因为set是不允许被修改的,否则会破坏搜索规则!!

(3)set 的迭代器是【双向迭代器】 只支持:++it--it不支持随机访问 不能:it + 5it += 2it - it2 这类运算!

底层红黑树链表式节点,没有连续内存下标,不存在随机偏移。

(4)set 元素自带 const 属性

如果试图去修改,是会报错的。

(5)在 set 中 iteratorconst_iterator 使用行为几乎一致,不能通过 iterator 修改数据。

3.2.3 insert的使用

1.首先就是参数为key值的

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

我们前面说明了,set是不允许键值冗余的,也就是说我们的set可能会插入失败,如上图的后两个1就是插入失败的例子,bool就是为了区分插入成功还是失败。 也就是说这里设置的规则就是,无论你是插入成功还是插入失败,都会返回这个新插入元素(或者原先就有的元素)的迭代器iterator,但是通过bool能够帮助我们判断究竟是插入成功还是插入失败。

3.iterator insert(iterator position,const value_type&val)

第一个参数是iterator,第二个参数是key值,这个就有点类似前面容器里的指定位置插入(无论插入成功还是失败都会返回对应的迭代器,区别就是如果待插入元素的相邻位置正好就是我们传进去的迭代器,此时可以实现高效的插入)。但是虽然说是指定位置插入,但是本身的插入逻辑也必须符合搜索的规则。 比如说1 2 3 4 5 8 9 10,如果我们想插入一个7,那么如果我们传的是5或者8的迭代器,此时就是一个高效的插入(因为相邻),但是如果我们传的迭代器是9,那么此时就不是一个高效的插入,因为找到9之后,还隔着一个8。

4.迭代器区间构造

3.2.4 find的使用

find的就是去set容器中找到对应键值并返回他对应的迭代器,如果找不到,就会返回end( )迭代器

3.2.5 count

count的作用本质上是为了参数对应的这个键值在set中有几个,但是因为set是不允许键值冗余的,所以其实其可能的结果要么是0要么是1,对应的就是有或者没有。

你可能会觉得,既然要么有要么没有,那么为什么参数不用bool而是返回个数呢??原因是因为下面的multiset,因为multiset是允许键值冗余的。

3.2.6 erase

1、传一个迭代器去删除。

2、直接通过传一个我们想要删除的键值去删除

3、删除set中指定的一块区间。

四、multiset

4.1multiset的介绍

  1. multiset是按照特定顺序存储元素的容器,其中元素是可以重复的。

  2. 在multiset中,元素的value也会识别它(因为multiset中本身存储的就是<value, value>组成

的键值对,因此value本身就是key,key就是value,类型为T). multiset元素的值不能在容器

中进行修改(因为元素总是const的),但可以从容器中插入或删除。

  1. 在内部,multiset中的元素总是按照其内部比较规则(类型比较)所指示的特定严格弱排序准则

进行排序。

  1. multiset容器通过key访问单个元素的速度通常比unordered_multiset容器慢,但当使用迭

代器遍历时会得到一个有序序列。

  1. multiset底层结构为二叉搜索树(红黑树)。

注意:

  1. multiset中再底层中存储的是<value, value>的键值对

  2. mtltiset的插入接口中只需要插入即可

  3. 与set的区别是,multiset中的元素可以重复,set中value是唯一的

  4. 使用迭代器对multiset中的元素进行遍历,可以得到有序的序列

  5. multiset中的元素不能修改

  6. 在multiset中找某个元素,时间复杂度为log2n

  7. multiset的作用:可以对元素进行排序

总而言之就是multiset相比set允许键值冗余,其他的基本上和set是一样的

4.2multiset的使用

因为之前我们介绍过了set的使用了,两者大差不差,我们这里只介绍set与multiset不同的地方

4.2.1 insert

因为允许键值冗余,所以是可以插入多个相同的键值的。set是排序+去重,multiset没有去重。

4.2.2 find和erase

既然允许键值冗余,那么就存在一个问题,如果我们查找或者是删除的键值是重复的,那么我们删除的究竟是哪一个的呢???

erase(val):删除的是所有值为val的值

erase(iterator position):删除的是position指向的那个值

find:搜索树的迭代器本身是利用中序去实现的,所以多个key的情况,返回的是中序的第一个key

4.2.3 count

五、map

5.1map的介绍

文档介绍:

  1. map是关联容器,它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元

素。

扩充完善: std::map<K,T> 属于有序关联式容器 ,和 set 同属红黑树实现;区别于顺序容器 vector、list。 存储单元不再是单一数值,而是键值二元组合 pair<key, value> 。 排序唯一依据是 key,和 value 无关;容器严格根据 key 的比较规则维持全局有序。 底层红黑树保证增删查效率稳定 (O(log_2n));迭代遍历得到按 key 升序(默认)有序序列。

  1. 在map中,键值key通常用于排序和惟一地标识元素,而值value中存储与此键值key关联的

内容。键值key和值value的类型可能不同,并且在map的内部,key与value通过成员类型

value_type绑定在一起,为其取别名称为pair:

typedef pair<const key, T> value_type;

扩充完善:

  1. key 具有唯一性 :map 不允许存在两个等价的 key;插入相同 key 时,新元素无法覆盖旧元素。 如果需要一个 key 对应多个 value → 使用multimap

  2. value_type 是 map 内置别名:pair<const K, T> ⚠️ 重点:key 是 const 常量! 不能通过迭代器修改 key!修改 key 会直接破坏红黑树有序性,树结构失效。value 可以自由修改。

  3. 在内部,map中的元素总是按照键值key进行比较排序的。

扩充完善: map 模板第二个参数为比较器 Compare = less<K>,只针对 key 进行大小比较;value 不参与排序判断。 默认规则:从小到大升序;可传入仿函数greater<K>自定义降序。 所有查找、相等判定全部依赖 key 的比较规则;两个 key 满足!comp(a,b) && !comp(b,a)判定为等价,无法共存。 对比 set:set 是元素自身充当 key;map 是独立 key + 附属 value。

  1. map中通过键值访问单个元素的速度通常比unordered_map容器慢,但map允许根据顺序

对元素进行直接迭代(即对map中的元素进行迭代时,可以得到一个有序的序列)。

扩充完善:

std::map:底层红黑树,查找复杂度稳定 \(O(log_2n)\);元素全局有序。

std::unordered_map:底层哈希表,平均查找 O (1) ,最坏退化 O (n);元素无序,遍历顺序不可预测。

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

扩充完善:

operator[] 不止是查找!它具备查找 + 插入双重逻辑

  1. 如果key("apple")已经存在 → 返回对应 value 的引用,可以读写;

  2. 如果key不存在 → 自动插入这个 key!value 使用默认构造初始化(int 初始化为 0,string 空串等)

  3. map通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树))。

这是map

key: 键值对中key的类型

T: 键值对中value的类型

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

5.2 map的使用

5.2.1 构造函数

(1)默认构造:

是一个空的map。

(2)迭代器区间构造map

(3)拷贝构造map

(4)初始化列表构造

5.2.2 insert

1、pair<iterator,bool> insert(const_value_type&val)

我们用一个字典来具体阐述它

因为map里面的元素都是一个个键值对,所以我们要直接插入的话,要用一个pair的匿名构造。但是c++中提供了一个make_pair的接口

我们之前在介绍键值对的时候写过,这里不做过多介绍

(2)和之前的set一样,参数传进去的迭代器相当于是一个暗示在该迭代器附近,如果相邻的话,可以实现最高效率的插入,如果不相邻的话就不存在。

5.2.3 erase

(1)删除指定迭代器位置的键值对

(2)删除键值为k的键值对,返回是否删除成功

(3)删除一段迭代器区间。

5.2.4 find

通过键值去找到并返回其对应的迭代器。如果没找到,就返回end()的迭代器。

5.2.5 count

在map中判断有无,在multimap中可以判断key对应键值对的个数。

5.2.6 重载\[\](重点!)

我们要想统计水果的个数

首先用一个string数组定义好这些水果的个数,然后定义mp1,在遍历整个arr的过程中,我们去找有没有这个水果,如果没有,我们直接插入,有的话,我们让他的个数++,但是这样是不是会很麻烦呢?我想是的,如果有\[\]呢?

众所周知,\[\]用的时候传的是key的值返回的是value的值

我们来探究一下其底层。

若键 k 存在:返回对应 value 的引用; 若键 k 不存在自动插入新元素 ,value 使用默认构造函数初始化,然后返回 value 引用;容器大小一定会 +1;

总结:mapped_type()构造的是一个临时空value,然后k是我们传入的key值,先用make_pair创建好pair<k,value>对象,当尝试插入这个键值对时(此时这个insert的类型是pair<iterator,bool>),如果说key值存在,insert不会修改容器,而是返回pair<迭代器,false>;如果说不存在,那我们就插入新节点,返回pair<新元素迭代器,true>;然后.first拿到指向元素的迭代器,最后解引用pair<const Key,T>,最后.second返回value的引用,也就是operator\[\]的返回值。

总结:重载 的方括号可以帮助我们完成4个任务,我们用之前的中英字典来验证。

1、插入(value是默认构造)

2、插入+修改(value是默认构造,但是返回后可以直接进行修改)

3、修改(找到对应键值的value并修改)

4、查找

六、multimap

6.1 multimap的介绍
  1. Multimaps是关联式容器,它按照特定的顺序,存储由key和value映射成的键值对<key,

value>,其中多个键值对之间的key是可以重复的。

扩充完善: std::multimap 属于有序关联式容器 ,存储元素类型为 pair<const Key, T>; 和 std::map 最核心区别:允许容器内存在多个拥有相同 key 的键值对。 当插入多个相同 key 的元素时,不会像 map 那样插入失败,而是允许全部保存。

示例:multimap<int, string> mm; mm.insert({1, "A"}); mm.insert({1, "B"}); 合法,容器同时保留两组 key=1 的数据。

  1. 在multimap中,通常按照key排序和唯一标识元素,而映射的value存储与key关联的内

容。key和value的类型可能不同,通过multimap内部的成员类型value_type组合在一起,

value_type是组合key和value的键值对:

typedef pair<const Key, T> value_type;

扩充完善: 容器内存储的每一个元素本质都是 pair<const Key, T>

  • const Key:键 key 不允许被修改!一旦修改 key 会破坏红黑树有序结构;
  • T:映射值 value,可以修改; 不能通过迭代器修改first(key),但可以修改second(value)。 value_type 是容器内置别名,在泛型编程、模板代码中经常用来代表容器存储的真实元素类型。
  1. 在内部,multimap中的元素总是通过其内部比较对象,按照指定的特定严格弱排序标准对

key进行排序的。

扩充完善: 默认使用 std::less<Key> 对 key 升序排序;开发者也可以自定义比较仿函数,修改排序规则。 容器底层红黑树的组织、查找、插入、删除全部依靠 key 的大小关系; 有序是永久特性 :无论怎么插入元素,遍历迭代器时,元素永远按 key 有序输出。 ⚠️ 注意:相同 key 的多个元素,C++ 标准只保证相邻存放,但不规定相同 key 元素之间的相对先后顺序(C++11 起部分标准库保证插入相对顺序,但不能依赖)。

  1. multimap通过key访问单个元素的速度通常比unordered_multimap容器慢,但是使用迭代

器直接遍历multimap中的元素可以得到关于key有序的序列。

扩充完善:

  • multimap:底层红黑树;查找、插入、删除时间复杂度 \(O(logN)\),天然有序;
  • unordered_multimap:底层哈希表;平均情况下操作复杂度 \(O(1)\) ,性能更快,但是元素无序
  1. multimap在底层用二叉搜索树(红黑树)来实现。

注意:multimap和map的唯一不同就是:map中的key是唯一的,而multimap中key是可以

重复的。

6、multimap没有重载 ,因为在map中key和value是一对一的关系,而在multimap中key和value可能是一对多的关系。

回顾map::operator[]作用:传入 key,返回唯一对应 value 的引用 ; 而 multimap 一个 key 对应若干个 value,编译器无法确定 mm[key] 应该返回哪一个 value 的引用,语法逻辑矛盾,因此标准直接禁止提供operator[]

6.2 multimap的使用

multimap的insert可以支持键值冗余,find和erase如果涉及到多个key会返回中序的第一个。和multiset基本一样,这里就不做过多介绍。重点还是其可以键值冗余并且没有重载

相关推荐
必须得开心呀1 小时前
qt生成dump文件并定位异常
开发语言·qt
fpcc1 小时前
跟我学C++中级篇—内存流
开发语言·c++
Cicada1281 小时前
ccvt:一个用 Rust 写的中国地图坐标系互转命令行工具
开发语言·后端·rust
1001101_QIA2 小时前
工控机网络配置
开发语言·数据库·php
程序员雷欧2 小时前
ThreadPoolExecutor 深度解析:从核心参数到源码实现的全面剖析
java·开发语言·jvm
catchadmin2 小时前
对标 npx 的 CPX PHP 的 Composer 包执行器
开发语言·php·composer
一位狮子座的程序员2 小时前
如何用RAG解决AI智能体的知识盲区?
开发语言·c#
吹什么轩3 小时前
c++复习:模拟实现list来更好的理解和应用list
开发语言·c++