当前位置: 首页 > news >正文

STL的map和set(关联式容器深度解析)

本节目标

1.关联式容器

2.键值对

3.树形结构的关联式容器

4.底层结构

关联式容器

序列式容器和关联式容器是 C++ 标准模板库(STL)中的两种基本容器类型,它们的主要区别在于元素的存储方式和访问方式。

序列式容器按照元素插入的顺序存储元素,每个元素都有固定的位置,除非被显式删除或移动。例如我们前面学的vector  /  list  /  deque  /  forward_list  /array等,其底层为线性序列的数据结构,里面存储的是元素本身

关联式容器按照键(key)来存储和访问元素,键通常是唯一的,并且会根据特定的排序准则自动排序,在数据检索时比序列式容器效率更高,常见的容器有map/set/multiset/multimap

键值对:key-value

用来表示具有一一对应的结构,该结构中一般包含两个成员变量key和value,key代表键值,value表示与key对应的信息。比如用身份证号码存储一个人的基本信息,你通过身份证号码和信息对应,把它存储起来,查一个人时只需要查找它的身份证号码,后续在把号码对应的信息查出来

这是STL对于pair的底层实现 

可以发现就是封装了一个pair类,类里面有两个成员变量,也就是键值对,一般第一个是key,第二个是value

 这是STL头文件<utility>中的pair,看它的构造函数,与底层实现的一样 

也就是有三种构造方式,第一个是构造空的,第二个是拷贝构造函数,第三个是传参构造

 树形结构的关联式容器

根据应用场景不同,STL总共实现了两种不同结构的管理式容器:树型结构和哈希结构。树型结构的关联式容器主要有四种:map、set、multimap、multiset。这四种容器的共同点是:使用平衡搜索树(即红黑树)作为底层结构,容器中的元素是一个有序的序列。下面依次介绍每个容器的使用方法。

set

set的介绍

集合(Set)是一种按照特定顺序存储唯一元素的容器。

在集合中,元素的值本身即作为其标识(每个元素的值就是键,类型为 T),且每个值必须唯一。一旦元素被放入集合,其值不能被修改(元素在容器中始终为常量),但可以插入新元素或移除已有元素。

从内部实现来看,集合中的元素始终按照其内部比较对象(类型为 Compare)所定义的严格弱序准则进行排序。

与无序集合(unordered_set)相比,集合容器通过键访问单个元素的速度通常较慢,但它们允许基于元素顺序直接对子集进行迭代。

集合通常以二叉搜索树(红黑树)的形式实现。

注意:

1.与map/multimap不同,map/multimap中存储的是真正的键值对<key,value>,在set中只放value,但在底层实际存放的是由<value,value>的键值对,也就是它以value为标识,所以每个值必须唯一,并且不能修改

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

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

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

5.set中的元素默认按照小于来比较

6.set中查找某个元素,时间复杂度时log以2为底的N次

7.set中的元素不允许被修改(为什么?因为它底层是一颗红黑树,如果你进行了修改就改变了本来树的结构,元素值就是红黑树的键,修改键会破坏树的有序性和平衡性)

8.set中的底层使用二叉搜索树(红黑树)来实现

set的使用

T:set中存放的元素的类型,实际在底层存储<value,value>的键值对

Compare:set中元素默认按照小于来比较

Alloc:set中元素空间的管理方式,使用STL提供的空间配置器管理 

set的构造: 

第一个构造空的set

第二个用[first,last)区间中的原色构成set

第三个拷贝构造 

set的迭代器 

这与我们之前学的一样 

 

set的容量 

学会empty和size即可,跟之前一样的

empty():是空就返回true,非空返回false

size():返回set中有效元素的个数

set修改操作

 第一个插入可以插入任意元素,只要和set里面存的元素的类型一样即可

注意:这里的返回值,返回一个键值对,键值对的第一个元素是迭代器类型,第二个是布尔类型

为什么???

因为可能你插入重复的元素,如果你插入重复的元素就返回那个元素的位置,并且返回false,用那个元素的位置和false创建一个键值对类,然后在返出去

第二个插入是给一个位置,在这个位置后插入一个元素,如果这个位置正确就不调整,否则还要进行调整,因为你底层是一颗红黑树

第三个插入时给一段区间,把区间的元素进行插入

最常用的是第一种

第一个删除pos位置的元素

第二个如果你知道元素是什么,可以直接指定,返回值是无符号整形,返回0表示未找到,返回1表示成功删除

第三个删除,删除一段区间

 

交换两个set对象

 清空里面的元素

操作函数:

这里重点学习find即可

 查找某个元素,找到返回它所在的迭代器位置,找不到返回end()位置 

map

map的介绍

映射(Maps)属于关联式容器,其存储的元素由键值(key value)和映射值(mapped value)组合而成,并且这些元素会按照特定顺序排列。

在映射里,键值的主要作用是对元素进行排序以及唯一标识元素,而映射值则用于存储与该键相关联的内容。键和映射值的类型可以不一样,它们被组合在成员类型 value_type 中,这是一种将两者结合起来的 pair 类型:

typedef pair<const Key, T> value_type;

从内部实现来看,映射中的元素始终依据键,按照特定的严格弱序准则(由其内部比较对象(类型为 Compare)来确定)进行排序。

和无序映射(unordered_map)容器相比,映射容器通过键来访问单个元素的速度通常要慢一些。不过,映射容器能够基于元素的顺序直接对子集进行迭代。

映射中的映射值可以利用方括号运算符(operator[]),通过其对应的键直接进行访问。

映射一般是用二叉搜索树(红黑树)来实现的。

map的使用

map的模板参数说明

template < class Key,                        // map::key_type           
class T,                                     // map::mapped_type          
class Compare = less<Key>,                   // map::key_compare          
class Alloc = allocator<pair<const Key,T> >  // map::allocator_type           

key:键值对中key的类型

T:键值对中value的类型

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

Alloc:通过空间配置器来申请底层空间,不需要用户传递,除非用户不选使用标准库提供的空间配置器 

map的构造


empty (1)	
explicit map (const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());
range (2)	
template <class InputIterator>  
map (InputIterator first, InputIterator last, const key_compare& comp = key_compare(),       const allocator_type& alloc = allocator_type());
copy (3)	
map (const map& x);

第一个在使用缺省的情况下可以直接map<int,int>m:构造一个空的map

第二个也就是区间构造

第三个就是map的拷贝构造函数

map的迭代器 

跟之前的学习一样

map的容量

empty():判空

size():大小

map中的元素的修改

这里对swap和clear不讲解,只针对insert和erase

 于set一样的,就是三个,一般常用第一个,其中value_type是下面的typedef过的

value_typepair<const key_type,mapped_type>

 

 与set一样三个,第一个pos位置删除,第二个删除某个key,第三个删除一段区间

find函数:

 就是查找某个key,然后返回它的迭代器位置即可

make_pair()

它是一个全局的函数,作用就是用来构造pair这个类的

以后构造pair的时候可以不要pair<int,int>(1,2)

可以直接make_pair(1,2) 

重点学习operator[]: 

 与之前的set对比,map支持[],而set不支持

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

如果k与容器中任何元素的键都不匹配,则函数会插入一个带有该键的新元素,并返回对其映射值的引用。请注意,这总会使容器大小增加 1,即使没有为该元素分配映射值(元素会使用其默认构造函数进行构造)。

类似的成员函数map::at在键存在的元素上具有相同的行为,但在键不存在时会抛出异常。

这个函数就等价于在调用

(*((this->insert(make_pair(k,mapped_type()))).first)).second

总结: 就是如果这个key在就返回对其映射值得引用,如果不在就会插入

 _Tp& operator[](const key_type& __k) {iterator __i = lower_bound(__k);// __i->first is greater than or equivalent to __k.if (__i == end() || key_comp()(__k, (*__i).first))__i = insert(__i, value_type(__k, _Tp()));return (*__i).second;}

这个是源码,不好看,所以我们看它的等价式,就等价于调用 

(*((this->insert(make_pair(k,mapped_type()))).first)).second

可以自行看一下括号的分配情况

讲解:

这里的mapped_type()是value的typedef过的,也就是value的类型,为什么是有个括号呢?因为如果不存在的话要调用它的构造函数,其实内置类型也可以看作类,也就是int(),它会调int的默认构造函数,也就会是0,一般这种都是0,如果你是自定义类型,就会调自定义类型的默认构造函数 

第二个是make_pair(k,mapped_type()),k就是你传入的参数,构造一个pair对象

第三个insert(make_pair(k,mapped_type())) 

第四个就是this->insert(make_pair(k,mapped_type())) ,this就是你的对象的地址嘛,->找到insert在进行插入

第五个((this->insert(make_pair(k,mapped_type()))).first)),当你调完你this的insert后,根据上面对insert的学习,会返回一个pair对象嘛,pair里面第一个存的是迭代器类型,第二个存的是bool类型,相当于pair.first,那就会拿到迭代器位置

然后对迭代器位置进行解引用,也就是你那个真实存的的pair,然后.second拿到value,返回它的引用,外部就可以进行更改。(迭代器解引用就是pair对象)

为什么operator[]不用find实现

用find,我们回去找到find的返回值,是迭代器类型,对比一下insert的返回,是pair对象

如果用find,map中没有k,如何返回?

用insert,如果k不在map中,则插入,在返回对value的引用

如果k在map中,则插入失败,返回k所在结点中的映射对象的引用

返回引用外部就可以修改,反观find,find的返回是iterator,k不在就返回end(),k在才返回

map的operator[]的应用场景

1.查找

2.修改

3.统计次数:比如你可以每次插入时,如果相同,对应得value就++,因为你不能插入相同得元素,你返回value的引用了就可以++

总结

1.map中的元素是键值对

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

3.默认按照小于的方式对key进行比较

4.map中的元素如果用迭代器去遍历,可以得到一个有序的序列

5.map的底层为平衡搜索树(红黑树),查找效率比较高O(logN)

6.支持[]操作符,operator[]中实际进行插入查找 

multiset和multimap

这两个与set和map不同的是,set和map的key是唯一的,但这两个可以重复,也就是你可以存多个相同的key

map:可通过 operator[] 或 at() 访问键对应的值(键不存在时,operator[] 会插入默认值)。

multimap不支持 operator[] 和 at(),因为同一个键可能对应多个值。若需访问,需通过迭代器遍历。

multimap的insert的返回值不是pair是iterator,因为相不相同都可以插入

实际中这两个也不常用,注意一下key是可以重复的就可以

总结:

  • set 适合处理唯一元素集合,强调元素的存在性和排序。
  • map 适合处理键值映射关系,强调通过键快速查找值。
  • 两者的插入、删除、查找操作均为 O (log n),适用于数据量较大且需要频繁操作的场景。
  • set示例
    • 存储用户 ID 集合,确保每个 ID 只出现一次。
    • 对输入的单词列表去重并按字典序排序。
  • map示例
    • 配置文件解析(键 = 值)。
    • 单词频率统计(单词→次数)。

 底层结构的实现

对于红黑树的讲解,我们放在数据结构那一章节,感兴趣的可以进行前往了解

http://www.xdnf.cn/news/642151.html

相关文章:

  • 2025第三届黄河流域网络安全技能挑战赛--Crypto--WriteUp
  • 网络原理入门详解:从零理解互联网如何工作
  • Modbus协议原理
  • 【Hive 开发进阶】窗口函数深度解析:OVER/NTILE/RANK 实战案例与行转列高级技巧
  • Day02
  • springboot日志
  • NotePad++编辑Linux服务器文档
  • 安全权限管理:从零到精通Android动态权限请求机制
  • CV中常用Backbone-3:Clip/SAM原理以及代码操作
  • Spring Boot 项目中常用的 ORM 框架 (JPA/Hibernate) 在性能方面有哪些需要注意的点?
  • 2025年- H50-Lc158 --25. k个一组翻转链表(链表,双指针,虚拟头节点)--Java版
  • Muduo网络库流程分析
  • quill 富文本多张图片排序
  • SRS流媒体服务器之RTC播放环境搭建
  • 揭开C语言指针的神秘面纱:地址、变量与“指向”的力量
  • systemverilog的单精度浮点和双精度浮点
  • AI测试怎么做投入产出比分析以及人员分配?
  • YOLOV8涨点技巧之DSS模块(一种轻量化火灾检测模型)
  • Unity引擎源码-物理系统详解-其三
  • C++23 std::out_ptr 和 std::inout_ptr:提升 C 互操作性
  • 锁与死锁的诊断:如何通过 SHOW ENGINE INNODB STATUS 解锁瓶颈
  • 加密货币投资亏损后,能否以“欺诈”或“不当销售”索赔?
  • 如何在 Windows 11 上安装 Ubuntu 20.04 WSL2
  • 《红警2000》游戏信息
  • YOLOv8源码修改(5)- YOLO知识蒸馏(下)设置蒸馏超参数:以yolov8-pose为例
  • Karakeep | 支持Docker/NAS 私有化部署!稍后阅读工具告别云端依赖,让知识收藏更有序
  • 机器学习---特征降维
  • C++指针与引用:const修饰的奥秘
  • 视频剪辑SDK定制开发技术方案与报价书优雅草卓伊凡
  • pinia状态管理使用