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

【C++详解】STL-set和map的介绍和使用样例、pair类型介绍、序列式容器和关联式容器

文章目录

  • 一、序列式容器和关联式容器
  • 二、set系列的使用
    • set类的介绍
    • set的构造和迭代器
    • set的增删查
    • lower_bound/upper_bound
    • multiset和set的差异
  • 三、map系列的使用
    • map类的介绍
    • pair类型介绍
    • map的构造及遍历
    • map的增删查
    • map的[]功能样例
    • multimap和map的差异


一、序列式容器和关联式容器

前⾯我们已经接触过STL中的部分容器如:string、vector、list、deque、array、forward_list等,这些容器统称为序列式容器,因为逻辑结构为线性序列的数据结构,两个位置存储的值之间⼀般没有紧密的关联关系,⽐如交换⼀下,他依旧是序列式容器。顺序容器中的元素是按他们在容器中的存储位置来顺序保存和访问的。
关联式容器也是⽤来存储数据的,与序列式容器不同的是,关联式容器逻辑结构通常是⾮线性结构,两个位置有紧密的关联关系,交换⼀下,他的存储结构就被破坏了。顺序容器中的元素是按关键字来保存和访问的。关联式容器有map/set系列和unordered_map/unordered_set系列。
本节讲解的map和set底层是红⿊树,红⿊树是⼀颗平衡⼆叉搜索树。set是key搜索场景的结构,map是key/value搜索场景的结构。
map和set都不支持单独一个变量值的构造函数,要么创建一个空的map或set,挨个插入变量,要么用initializer_list构造。

二、set系列的使用

set类的介绍

set的声明如下,T就是set底层关键字的类型。

template < class T, // set::key_type/value_type
class Compare = less<T>, // set::key_compare/value_compare
class Alloc = allocator<T> // set::allocator_type> class set;
  • set默认要求T⽀持⼩于⽐较,如果不⽀持或者想按⾃⼰的需求⾛可以⾃⾏实现仿函数传给第⼆个模版参数
  • set底层存储数据的内存是从空间配置器申请的,如果需要可以⾃⼰实现内存池,传给第三个参数。⼀般情况下,我们都不需要传后两个模版参数。
  • set底层是⽤红⿊树实现,增删查效率是O(logN) ,迭代器遍历是⾛的搜索树的中序,所以是有序的。
  • 前⾯部分我们已经学习了vector/list等容器的使⽤,STL容器接⼝设计,⾼度相似,所以这⾥我们就不再⼀个接⼝⼀个接⼝的介绍,挑⽐较重要的接⼝进⾏介绍。

仿函数less< T >默认是左边比根小,右边比根大,我们更爱仿函数为Greator< T >后不会影响查找逻辑,因为查找是的判断也要依赖仿函数的。

set的构造和迭代器

set的⽀持正向和反向迭代遍历,遍历默认按升序顺序,因为底层是⼆叉搜索树,迭代器遍历走的中序;⽀持迭代器就意味着⽀持范围for,set的iterator和const_iterator都不⽀持迭代器修改数据,因为修改关键字数据,破坏了底层搜索树的结构。
使用举例:

int main()
{// 去重+升序排序set<int> s = { 12, 6, 4, 8 };// 去重+降序排序(给⼀个⼤于的仿函数)//set<int, greater<int>> s;s.insert(5);s.insert(2);s.insert(7);s.insert(5);//set<int>::iterator it = s.begin();auto it = s.begin();while (it != s.end()){// error C3892: “it”: 不能给常量赋值// *it = 1;cout << *it << " ";++it;}cout << endl;// 插⼊⼀段initializer_list列表值,已经存在的值插⼊失败s.insert({ 2,8,3,9 });for (auto e : s){cout << e << " ";}cout << endl;set<string> strset = { "sort", "insert", "add" };// 遍历string⽐较ascll码⼤⼩顺序遍历的for (auto& e : strset){cout << e << " ";}cout << endl;
}

set的增删查

find:
在这里插入图片描述

既然算法库已经有find了,为什么set里还要实现find呢?原因是算法库的find是暴力查找,不管什么容器都是从头到尾遍历一遍,效率很低,而set里的find是利用了搜索树的结构特点,从根节点开始查找,效率更高。
find返回值还是和以前一样,找到了返回指定位置迭代器,没找到返回end()。

// 直接删除x
int x;
cin >> x;
int num = s.erase(x);
if (num == 0)
{cout << x << "不存在!" << endl;
}
for (auto e : s)
{cout << e << " ";
}
cout << endl;// 直接查找在利⽤迭代器删除x
cin >> x;
auto pos = s.find(x);
if (pos != s.end())
{s.erase(pos);
}
else
{cout << x << "不存在!" << endl;
}for (auto e : s)
{cout << e << " ";
}
cout << endl;

如果只想单纯判断某个值在不在,用count更方便:

在这里插入图片描述

cin >> x;
if (s.count(x))
{cout << x << "在!" << endl;
}
else
{cout << x << "不存在!" << endl;
}

erase:
在这里插入图片描述

erase可以指定key删除,也可以指定某个迭代器删除。注意三种erase返回值的区别:
删单个元素 / 区间 → 返回 “下一个迭代器”,方便接续遍历。
删值 → 返回删除个数,方便判断结果。
因为这里需要结合multset使用,因为multset支持重复键值,所以当multset调用erase它会一次性把容器里等于指定值的元素都删除。

set<int> s = { 4,2,7,2,8,5,9 };
for (auto e : s)
{cout << e << " ";
}
cout << endl;// 删除最⼩值
s.erase(s.begin());
for (auto e : s)
{cout << e << " ";
}
cout << endl;

lower_bound/upper_bound

在这里插入图片描述
在这里插入图片描述

这两个接口一般配合使用,用法如下:
auto it1 = lower_bound(val):找第一个 ≥val 的元素迭代器。
auto it2 = upper_bound(val):找第一个 >val 的元素迭代器。
erase(it1, it2):删除 [it1, it2) 区间的元素,左闭右开。

	std::set<int> myset = { 10, 20, 30, 40, 50, 60, 70, 80, 90 };for (auto e : myset){cout << e << " ";}cout << endl;// 实现查找到的[itlow,itup)包含[30, 60]区间// 返回 >= 30auto itlow = myset.lower_bound(30);// 返回 > 60auto itup = myset.upper_bound(60);// 删除这段区间的值myset.erase(itlow, itup);for (auto e : myset){cout << e << " ";}cout << endl;

结果:

在这里插入图片描述

multiset和set的差异

multiset和set的使⽤基本完全类似,主要区别点在于multiset⽀持值冗余,那么
insert/find/count/erase都围绕着⽀持值冗余有所差异,具体参看下⾯的样例代码理解。
(find查找中序的第⼀个值是为了方便把后面相同的值一并找到)

int main()
{// 相⽐set不同的是,multiset是排序,但是不去重multiset<int> s = { 4,2,7,2,4,8,4,5,4,9 };auto it = s.begin();while (it != s.end()){cout << *it << " ";++it;}cout << endl;// 相⽐set不同的是,x可能会存在多个,find查找中序的第⼀个int x;cin >> x;auto pos = s.find(x);while (pos != s.end() && *pos == x){cout << *pos << " ";++pos;}cout << endl;// 相⽐set不同的是,count会返回x的实际个数cout << s.count(x) << endl;// 相⽐set不同的是,erase给值时会删除所有的xs.erase(x);for (auto e : s){cout << e << " ";}cout << endl;return 0;
}

三、map系列的使用

map类的介绍

map就是小编在二次搜索树介绍的key/value版本。map的声明如下,Key就是map底层关键字的类型,T是map底层value的类型,set默认要求Key⽀持⼩于⽐较,如果不⽀持或者需要的话可以⾃⾏实现仿函数传给第⼆个模版参数,map底层存储数据的内存是从空间配置器申请的。⼀般情况下,我们都不需要传后两个模版参数。map底层是⽤红⿊树实现,增删查改效率是 O(logN) ,迭代器遍历是⾛的中序,所以是按key有序顺序遍历的。

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> class map;

pair类型介绍

pair是专门为map准备的一个类模板。
map底层的红⿊树节点中的数据,使⽤pair<Key, Value>存储键值对数据。所以map里存的值本质就是pair。pair定义如下:
(最后的拷贝构造函数允许从不同类型的pair<U, V>构造当前pair<T1, T2>,前提是U类型可以转换为T1类型,V类型可以转换为T2类型。)

template <class T1, class T2>
struct pair
{//默认构造pair() : first(T1()), second(T2()){}//带参构造pair(const T1& a, const T2& b) : first(a), second(b){}//泛化的拷贝构造函数template<class U, class V>pair(const pair<U, V>& pr) : first(pr.first), second(pr.second){}//类型重定义typedef T1 first_type;typedef T2 second_type;//成员变量T1 first;T2 second;
};

pair没有重载operator<<,所以要打印map值只能取pair的成员依次打印,如下所示:

    //范围forfor (auto e : dict){cout << e.first << " " << e.second << endl;}//迭代器
auto it = dict.begin();
while(it != dict.end())
{cout << (*it).first << " " << (*it).second << endl;cout << it->first << " " << it->second << endl;it++;
}

这里迭代器示例可以反向说明为什么map要用pair来存储值,而不是每个结点直接存两个值一个key一个value,这样方便迭代器解引用取里面的值,如果存两个值迭代器解引用无法返回两个值(标准不支持),若想返回两个值或者多个值必须要用一个结构包装起来整体返回。

map的构造及遍历

map的构造小编就只介绍一下initializer_list构造,如下所示,内层花括号是隐式类型转换,外层花括号代表initializer_list。

map<string, string> dict = { {"left", "左边"}, {"right", "右边"},{"insert", "插⼊"},{ "string", "字符串" } };

范围for遍历时最好加上&,如果遍历时不会修改值最好再把const加上。

// 范围for遍历
for (const auto& e : dict)
{cout << e.first << ":" << e.second << endl;
}
cout << endl;

map的增删查

在介绍map接口之前小编先说明一点,map的value_type不再是单纯的key的类型了,而是pair,因为插入操作需要将 key/value 整体插入,也就是这里的pair。

在这里插入图片描述
insert:

插入也和set一样,不会插入重复的key,就算key的value不相同也不会重复插入key。
插入我们有四种方法,前三种是C++98支持的,一种是有名对象,一种是匿名对象,还有一种是make_pair函数模板,不用我们显示写数据类型,它可以自动推导类型。(它是内联函数,和匿名对象一样,不会有额外开销)
最后一种C++11是依靠多参数隐式类型转换实现的,因为C++11才开始支持多参数隐式类型转换,之前只有单参数隐式类型转换。

make_pair:

template <class T1,class T2>
inline pair<T1,T2> make_pair (T1 x, T2 y)
{
return ( pair<T1,T2>(x,y) );
}

示例:

map<string, string> dict;
//有名对象
pair<string, string> kv1("sort", "排序");
dict.insert(kv1);
//匿名对象
dict.insert(pair<string, string>("string", "字符串"));
//make_pair
dict.insert(make_pair("array", "数组"));
//多参数隐式类型转换
dict.insert({"list", "链表"});

erase:

erase和find都只能指定key操作,例子和set差不多,小编就不做演示了。

在这里插入图片描述
find:
在这里插入图片描述

map的[]功能样例

利⽤find和iterator修改功能,统计⽔果出现的次数:

string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
map<string, int> countMap;
for (auto e : arr)
{auto it = countMap.find(e);if (it == countMap.end()){countMap.insert({e, 1});}else{it->second++;}
}for (const auto& e : countMap)
{cout << e.first << ":" << e.second << endl;
}
cout << endl;

这里用operator[ ]也可以实现一样的效果,但是这里的operator[ ]显然和之前的容器介绍的区别很大了,跟着小编一起来了解一下map的operator[ ]吧。

string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
map<string, int> countMap;
for (auto e : arr)
{countMap[e]++;
}for (const auto& e : countMap)
{cout << e.first << ":" << e.second << endl;
}
cout << endl;

在这里插入图片描述

我们看文档可以知道它的功能是拿到key的值,返回value值的引用,所以简单来说就是查找+修改。
当value不存在时就会插入这个不存在的值,那么这是怎么做到的呢?原因就是operator[ ]底层会去调用insert,看上面文档小编用红色方框圈起来的部分。所以我们现在的首要目标是先把insert的返回值搞明白。

在这里插入图片描述

首先明确一点insert插入的是pair对象。然后看insert的返回值,它返回是是迭代器和bool值构成的pair,当新拆的key在map里没有则插入成功返回true,iterator返回新插入进来的元素的迭代器,否则插入失败返回false,iterator返回已经在map里存在的元素的迭代器。

我们理解了insert过后再来尝试理解operator[ ]是实现细节。operator[ ]一开始先insert,insert需要传key和value,但是operator[ ]的参数没有value的值,所以只能传value 类型的默认构造V(),类似于缺省值。(内置类型也有默认构造)
再看第二行代码,不管key在map里存不存在,反正insert返回的pair里的iterator都会指向key所在的结点,(因为不存在会插入含key的新节点,存在直接返回含key的结点)这时候我们再取insert返回值里的iterator(ret.first)就得到了指向key所在的结点的迭代器,然后再通过迭代器访问结点里pair的第二个成员(ret.first->second)就成功实现了operator[ ]的功能。(返回值这里的first和second分属两个不同的pair)
下面小编写的代码只是把operator[]是大致功能展示了一下,真正的实现细节等小编介绍到map的底层实现时再细讲。

V& operator[](const K& key)
{pair<iterator, bool> ret = insert({ key, V() });//ret.first返回key所在的迭代器,然后->second返回key所在迭代器里的valuereturn ret.first->second; 
}

下面小编展示几个使用operator[ ]的例子:

map<string, string> dict;
//插入
dict["left"];
//插入+修改
dict["right"] = "右边";
//修改
dict["left"] = "左边";
//查找
cout << dict["left"] << endl;

所以这个时候我们再回过去理解operator[ ]统计水果是不是就非常直观了,当map里没有对应水果就是插入+修改,当map里有就只是修改。

multimap和map的差异

multimap和map的使⽤基本完全类似,主要区别点在于multimap⽀持关键值key冗余,那么insert/find/count/erase都围绕着⽀持关键值key冗余有所差异,这⾥跟set和multiset完全⼀样,⽐如find时,有多个key,返回中序第⼀个。还有两点不同就是multimap不⽀持[],因为⽀持key冗余,无法确定[]返回哪个key。其次find的返回值只有iterator了,因为允许重复插入就没有插入成功与否的概念了。

以上就是小编分享的全部内容了,如果觉得不错还请留下免费的关注和收藏如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~

在这里插入图片描述

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

相关文章:

  • 蓝桥杯----锁存器、LED、蜂鸣器、继电器、Motor
  • RN项目环境搭建和使用-Mac版本(模拟器启动不起来的排查)
  • 软件定义汽车 --- 电子电气架构的驱动
  • 【pytorch(02)】Tensor(张量)概述、如何创建、常见属性,切换设备
  • AI学习之大话transformer架构
  • 2025年08月 GitHub 热门项目推荐
  • Spring选择哪种方式代理?
  • 电子电气架构 ---如何焕新升级为 48V 电气架构
  • 无人机航拍数据集|第4期 无人机太阳光伏板红外目标检测YOLO数据集10945张yolov11/yolov8/yolov5可训练
  • OpenHarmony源码解析之init进程
  • Android Activity webView页面视频悬浮小窗播放效果及技术难点
  • apache-tomcat-11.0.9安装及环境变量配置
  • 聊一聊RPC接口测试工具及方法
  • MonoFusion 与 Genie 3
  • Apollo中三种相机外参的可视化分析
  • Javascript/ES6+/Typescript重点内容篇——手撕(待总结)
  • W3D引擎游戏开发----从入门到精通【22】
  • 【科研绘图系列】R语言绘制瀑布图
  • sqli-labs靶场less40-less45
  • 012 网络—基础篇
  • 医疗AI中GPU部署的“非对等全节点架构“方案分析(上)
  • 如何创建一个vue项目
  • 5G随身WiFi怎么选?实测延迟/网速/续航,中兴V50适合商务,格行MT700适合短租、户外党~避坑指南+适用场景全解析
  • Git 分支管理:从新开发分支迁移为主分支的完整指南
  • 【数据结构初阶】--排序(四):归并排序
  • Linux基础命令的生产常用命令及其示例简单解释
  • 对接钉钉审批过程记录(C#版本)
  • C++与C语言实现Stack的对比分析
  • 基于 kubeadm 搭建 k8s 集群
  • Go语言数据类型深度解析:位、字节与进制