✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
文章目录
前言
在C++编程的世界里,容器是非常重要的部分,它们为高效地处理数据提供了强大的支持。其中,set
(集合)和map
(映射)这两种容器有着独特且广泛的用途。无论是新手在初步探索C++ 数据结构的奥秘,还是经验丰富的程序员想要优化代码中的数据管理部分,深入了解set
和map
的使用都是非常必要的。本篇文章将详细介绍set
和map
的使用方法和一些常用的技巧,帮助读者更好地将它们融入到自己的编程实践中。
一.set
在c++中,
set
是一种标准模板库(STL)容器,用于存储唯一元素的集合。set容器会自动对元素进项排序,并且不允许有重复的元素,set的底层是红黑树--搜索树,是key的搜索模型。下面将会对set的使用进行重点讲解。
1.引入头文件
要使用set容器,需要包含头文件<set>
cpp
#include<set>
2.定义和初始化
-
直接定义空
set
对象cpp//定义一个存储int类型元素的set set<int> myset;
-
使用列表初始化
set
对象cpp//使用列表初始化set对象 //注意set会自动进行排序和去重 set<int> myset={1,2,3,4,5};
-
使用迭代器区间来初始化set对象
cpp//使用数组v的迭代器区间来初始化set对象 vector<int> v={1,2,3,4,5}; set<int> myset(v.begin(),v.end());
-
使用插入操作来初始化
set
cpp//调用插入函数依次插入1,2,3 set<int> myset; myset.insert(1); myset.insert(2); myset.insert(3);
3.元素访问
由于set的底层是红黑树,一种搜索树,地址空间并不连续所以不支持使用索引来访问元素(也就是myset[0]
这样的操作),但可以通过迭代器来访问元素
set
容器提供了以上几种迭代器,和string
类似,包含正向迭代器和反向迭代器,下面通过例子来演示如何使用
cpp
//使用迭代器来遍历
void test1(){
set<int> myset={1,2,3,4,5};
set<int>::iterator it=myset.begin();
//正向遍历
while(it!=myset.end()){
cout<<*it<<" ";
++it;
}
cout<<endl;
set<int>::reverse_iterator rit=myset.rbegin();
//反向遍历
while(rit!=myset.rend()){
cout<<*rit<<" ";
++rit;
}
cout<<endl;
}
当然也可以使用范围for循环:
cpp
void test2(){
set<int> myset={1,2,3,4,5};
for(auto e:myset){
cout<<e<<" ";
}
cout<<endl;
}
**注意:**set对象中的元素都不能进行更改,只允许访问,因为opeartor*
重载函数中返回类型是const
类型,不能进行修改
4.容量大小
set关于容量大小的函数有下面三个
其中最为常用的要是上面两个
-
size()
函数返回set中有效元素的个数 -
empty()
函数判断set是否为空cppvoid test3(){ set<int> myset={1,2,3,4,5,6}; //输出有效的元素个数 cout<<myset.size()<<endl; //判断是否为空 cout<<myset.empty()<<endl; }
5.元素查找
set提供了多种元素查找的方式:
-
find(x)
函数,查找x的位置,返回x的迭代器 -
lower_bound(x)
函数,查找>=x
的值,返回查找到值的迭代器 -
upper_bound(x)
函数,查找>x
的值,返回查找到值的迭代器cppvoid test4(){ set<int> myset={5,6,4,2,3,7,1,8,0,9}; //迭代器it1指向7 set<int>::iterator it1=myset.find(7); //遍历打印7之后的元素 while(it1!=myset.end()){ cout<<*it1<<" "; ++it1; } cout<<endl; //迭代器it2指向>=2的元素 set<int>::iterator it2=myset.lower_bound(2); //迭代器it3指向>6的元素 set<int>::iterator it3=myset.upper_bound(6); //输出迭代器,it2和it3指向的元素 cout<<*it2<<" "<<*it3<<endl; //遍历打印it2和it3区间的元素 while(it2!=it3){ cout<<*it2<<" "; it2++; } cout<<endl; }
对于剩下的两个count()
函数和epual_range()
函数,这两个函数在set中没有太大的使用意义,因为set不允许存储相同的元素,所以这两个函数可以理解为是专门给另一个容器multiset
使用的,在后面会讲到这一点,这里就不在讲解关于这两个函数的使用。
6.元素插入和删除
-
插入元素
insert()
函数:set的insert()函数和之前我们学过的其他容器不同的是,返回的是一个pair类型的对象,其中,pair对象的第一个元素是指向插入元素的迭代器,第二个元素表示是否插入成功,如果第一次插入元素时,first表示插入元素的迭代器,second表示插入成功true;
如果插入已经存在的,first还是表示插入元素的迭代器,second表示插入失败false;
cppvoid test5(){ set<int> myset={1,2,3,4,5}; //插入单个元素,返回pair类型的对象 pair<set<int>::iterator,bool> it1=myset.insert(100); pair<set<int>::iterator,bool> it2=myset.insert(2); //输出插入结果 cout<<it1.second<<" "<<it2.second<<endl; //使用迭代器区间插入,插入数组v vector<int> v={10,20,30,40,50}; myset.insert(v.begin(),v.end()); //遍历打印myset for(auto e:myset){ cout<<e<<" "; } cout<<endl; }
-
删除元素
erase()
函数:相比于插入函数
insert()
,删除函数erase()
就较为简单,这里直接通过例子来展示cppvoid test6(){ set<int> myset={1,2,3,4,5,6,7,9,10}; //删除7 myset.erase(7); for(auto e:myset){ cout<<e<<" "; } cout<<endl; set<int>::iterator it1=myset.lower_bound(2); set<int>::iterator it2=myset.upper_bound(5); //删除区间[2,6) myset.erase(it1,it2); for(auto e:myset){ cout<<e<<" "; } cout<<endl; }
7.总结
- set的底层实现通常是红黑树或者其他平衡二叉搜索树,因此插入,删除和查找操作的时间复杂度都是
O(log n)
- set中的元素默认按照升序排列,若果需要自定义排序规则,可以通过提供自定义的比较函数或仿函数对象作为模版参数来定义set
- 由于set不允许有重复元素,并且元素自动排序,因此他非常适合用于需要快速查找,插入和删除操作且元素唯一的场景
二.multiset
在c++中,multiset和set容器都是c++标准模版库中的关联容器,他们存储的元素都是唯一的(对于multiset而言,这里的唯一性不是指不是元素值的唯一性,而是指单个元素位置的唯一性),并且默认按照升序排列
1.multiset
和set
的区别
-
是否允许重复元素:
- set容器不允许存储重复的元素。如果尝试插入一个已经存在的元素,set容器会忽略该插入操作。
- multiset容器则允许存储重复的元素。如果尝试插入一个已经存在的元素,multiset容器会将该元素再次插入到容器中。
-
用途和特性:
- set容器主要用于实现去重的功能,确保集合中的元素是唯一的。
- multiset容器则允许元素重复,并自动对元素进行排序,非常适合需要快速查找元素且允许重复的场景。
2.函数使用
multiset
和set
的成员函数相同,且大多数用法都相同,比如定义初始化,元素访问,容量大小,插入和删除等(具体用法直接看set
就行,这里就不在一一讲解),而唯一有点不同的就是查找等函数,因为,multiset
允许有相同的元素存储,所以导致对于查找元素等函数有所区别。
-
find(x)
函数返回的是中序第一个x的迭代器 -
equal_range(x)
函数,当有相同元素时,返回的是pair对象的,表示一个迭代器区间:第一个x~最后一个x,只有一个元素x时,就相当于find(x)
函数 -
count(x)
函数返回相同元素x的个数cppvoid test7(){ multiset<int> myset = {4,2,6,1,2,3,5,2,1,7}; //查找元素2的个数 size_t i=myset.count(2); cout<<i<<endl; //查找元素2的区间并全部删除 pair<multiset<int>::iterator,multiset<int>::iterator> ipair=myset.equal_range(2); myset.erase(ipair.first,ipair.second); for(auto e : myset){ cout<<e<<" "; } cout<<endl; }
因此可以看出对于equal_range()
和count()
函数,multiset使用起来更有意义。
三.map
在C++中,map
是一种关联容器(associative container),它存储的是键值对(key-value pairs)。每个键(key)在map
中是唯一的,并且每个键都映射到一个值(value)。map
内部通常通过红黑树(或其他平衡二叉搜索树)实现,因此它提供了高效的查找、插入和删除操作。以下是对C++中map
的详细讲解:
1. 引入头文件
要使用map
,首先需要包含头文件<map>
:
cpp
#include <map>
2. 定义与初始化
map
可以通过多种方式定义和初始化:
- 直接定义空
map
:
cpp
// 定义一个键key为int类型,值value为string类型的空map
map<int, string> myMap;
-
使用列表初始化
map
:c++11多参数构造函数隐式类型转换
cpp
// 使用列表初始化map(隐式类型转换)
map<int,string> myMap = {{1, "one"}, {2, "two"}, {3, "three"}};
- 使用插入操作初始化
map
:
cpp
map<int, string> myMap;
//insert函数中参数可以使用函数make_pair()
myMap.insert(make_pair(1, "one"));
//也可以直接使用pair对象
myMap.insert(pair<int, string>(2, "two"));
3. 元素访问
map
提供了多种访问元素的方法:
和set
不同的是,map
不仅可以使用迭代器来访问元素,还可以使用下标加[]
,迭代器还是和set
一样,有以下几种,但在使用上有一点不一样。
-
使用迭代器访问元素:
cppvoid test2(){ map<int,string> myMap={{1,"set"},{2,"multiset"},{3,"map"},{4,"multimap"}}; //正向迭代器遍历,使用*访问元素 map<int,string>::iterator it1=myMap.begin(); while(it1!=myMap.end()){ cout<<(*it1).first<<" "<<(*it1).second<<" "; it1++; } cout<<endl; //正向迭代器遍历,使用->访问元素 map<int,string>::iterator it2=myMap.begin(); while(it2!=myMap.end()){ //注意这里其实是->->,两个箭头符号,编译器自动优化,省略了一个-> cout<<it2->first<<" "<<it2->second<<" "; it2++; } cout<<endl; //反向迭代器遍历,使用*访问元素 map<int,string>::reverse_iterator rit1=myMap.rbegin(); while(rit1!=myMap.rend()){ cout<<(*rit1).first<<" "<<(*rit1).second<<" "; rit1++; } cout<<endl; //反向迭代器遍历,使用->访问元素 map<int,string>::reverse_iterator rit2=myMap.rbegin(); while(rit2!=myMap.rend()){ cout<<rit2->first<<" "<<rit2->second<<" "; rit2++; } cout<<endl; }
-
使用下标运算符
[]
访问元素opeartor[]
接受一个键key作为参数,并返回与该键相关联的值value的引用,如果该键再map中已经存在,则返回对应值的引用;如果不存在,map会插入一个新的键值对,其中键值key是传入的参数,而值value则会被初始化为该类型的默认值,返回这个新值的引用。
注意事项:
-
插入新元素:
当使用
operator[]
访问一个不存在的键时,map
会自动插入一个新的键值对,因此,如果只是想检查某个键是否存在,而不想改变时,最好使用find()
或者count()
函数来检查 -
值的类型:
由于
operator[]
会返回值的引用,因此可以直接通过这个引用来修改值。但是,如果值的类型是复杂对象或需要特殊初始化的对象(可以理解为自定义类型),需要确保构造函数正确调用来完成初始化。 -
异常安全性:
如果map的键或者值的类型可能会抛出异常(例如,在构造或复制过程中),那么在使用
operator[]
时,需要特别小心。因为一旦异常抛出,map的状态可能会变的不确定。 -
效率:
虽然
operator[]
在大多数情况下都提供了高效的键值对访问,但如果应用场景中涉及到大量的查找操作,并且不希望因为不存在的键而自动插入新元素时,那么使用find()
函数可能会更方便。
cpp
void test5(){
map<int,string> myMap;
myMap[1]="one";
myMap[2]="two";
myMap[3]="three";
//访问元素
cout<<myMap[2]<<endl;
cout<<myMap[3]<<endl;
//修改元素
myMap[2]="map";
cout<<myMap[2]<<endl;
//尝试访问不存在的值,将插入新的键值队,键key是参数4,值value是默认初始化为空字符串
cout<<myMap[4]<<endl;
//遍历打印
for(const auto& e: myMap){
cout<<e.first<<" "<<e.second<<endl;
}
cout<<endl;
}
4.元素查找
-
查找函数
find()
:cppvoid test(){ map<int,string> myMap={{1,"set"},{2,"multiset"},{3,"map"},{4,"multimap"}}; //查找Key值为3的节点,返回该节点的迭代器 map<int,string>::iterator it=myMap.find(3); }
5. 元素插入和删除
-
插入
insert()
函数:
和set一样,map的插入函数如果插入的只是一个pair对象,pair<key,value>
,最后返回的是一个其他类型的pair对象,如果key已经在树中,返回pair<树里面key所在节点的iterator,flase>
,如果key不在树里面,返回的是pair<新插入key所在节点的iterator,true>
。
cpp
void test3(){
map<int,string> myMap={{1,"set"},{2,"multiset"},{3,"map"},{4,"multimap"}};
//插入key值为3,value值为newmap
pair<map<int,string>::iterator,bool> it1=myMap.insert(make_pair(3,"newmap"));
//输出插入结果和返回的节点中的key和value值
cout<<it1.second<<" "<<it1.first->first<<" "<<it1.first->second<<endl;
//插入key值为5,value值为string
pair<map<int,string>::iterator,bool> it2=myMap.insert(make_pair(5,"string"));
//输出插入结果和返回的节点中的key和value值
cout<<it2.second<<" "<<it2.first->first<<" "<<it2.first->second<<endl;
//使用范围for遍历打印
for(const auto& e: myMap){
cout<<e.first<<" "<<e.second<<" ";
}
cout<<endl;
}
-
删除
erase()
函数:
cpp
void test4(){
map<int,string> myMap={{1,"set"},{2,"multiset"},{3,"map"},{4,"multimap"}};
//删除Key值为3的节点
auto it=myMap.find(3);
myMap.erase(it);
//遍历打印
for(const auto& e: myMap){
cout<<e.first<<" "<<e.second<<" ";
}
cout<<endl;
//查找区间2到4,并删除区间[2,4)
auto it1=myMap.find(2);
auto it2=myMap.find(4);
myMap.erase(it1,it2);
//遍历打印
for(const auto& e: myMap){
cout<<e.first<<" "<<e.second<<" ";
}
cout<<endl;
}
6.总结
- 性能和复杂度:
map
的插入、删除和查找操作的时间复杂度都是O(log n),其中n是map
中元素的数量。- 由于
map
内部使用平衡二叉搜索树实现,因此它保持了元素的排序状态,这使得它非常适合用于需要快速查找、插入和删除操作且元素需要按键排序的场景。
2.注意事项:
- 在使用下标运算符
[]
访问map
中的元素时,如果键不存在,则会插入一个具有该键的新元素。因此,在检查键是否存在之前,最好使用find
或count
方法。 map
中的键是唯一的,因此不能有两个键相同的元素。如果尝试插入一个已经存在的键,则操作会失败(对于insert
方法)或覆盖旧值(对于[]
运算符)。
四.multimap
容器multimap
和map
在C++ STL(标准模板库)中都是关联式容器,它们存储的是键值对(key-value pairs),但有一些关键的不同点:
-
键的唯一性:
map
中的键是唯一的,即每个键只能映射到一个值。如果尝试插入一个已经存在的键,那么该操作会失败(对于insert
函数)或者覆盖原有的值(对于operator[]
操作符)。multimap
允许键的重复,即同一个键可以映射到多个值。因此,在multimap
中,一个键可以有多个键值对与之对应。
-
操作符支持:
map
支持使用operator[]
来访问元素,如果键不存在,则会插入一个具有该键的新元素(其值通常被初始化为默认值)。multimap
不支持operator[]
,因为键不是唯一的,使用下标访问没有意义且可能导致混淆。相反,multimap
提供了其他方法来查找和访问元素,如find
、equal_range
等。
-
元素顺序:
- 两者都是基于红黑树(一种自平衡二叉搜索树)实现的,因此它们的元素都是按键的序排列的。这意味着可以高效地执行查找、插入和删除操作。
-
迭代器:
- 两者都提供双向迭代器,允许在容器中前后遍历元素。
- 由于
multimap
允许键的重复,其迭代器在遍历具有相同键的元素时可能会连续返回多个具有相同键的键值对。
-
性能:
- 在平均情况下,两者的查找、插入和删除操作的时间复杂度都是O(log n),其中n是容器中元素的数量。这是因为它们都是基于红黑树实现的。
-
使用场景:
map
适用于需要唯一键映射到值的场景。multimap
适用于需要允许键重复的场景,例如,当你想要存储一个键对应多个值的列表时。
总结来说,map
和multimap
的主要区别在于键的唯一性和对operator[]
操作符的支持。选择使用哪一个取决于具体需求。
以上就是关于容器set和map的使用讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!