【C++】标准模板库STL------set /multiset/map /multimap
我们之前学习了一些容器如vector、list 这些称为序列式容器 ,本篇文章将介绍STL中另一类核心容器------关联式容器 ,其中map和set 是最常用的代表。它们底层基于红黑树 实现,拥有O(logN) 的高效增删查效率
文章目录
- [【C++】标准模板库STL------set /multiset/map /multimap](#【C++】标准模板库STL——set /multiset/map /multimap)
-
- [一、 序列式容器和关联式容器](#一、 序列式容器和关联式容器)
- [二、 set系列的使用](#二、 set系列的使用)
-
- [1、 set的特性](#1、 set的特性)
- 2、set的模板参数
- [3、 set的接口使用](#3、 set的接口使用)
- 三、map系列的使用(map/multimap)
-
- [1、 map的核心特性](#1、 map的核心特性)
- [2、 pair类型](#2、 pair类型)
- [3、 map的模板参数](#3、 map的模板参数)
- [4、 map核心接口使用](#4、 map核心接口使用)
-
- [(1) 构造与遍历](#(1) 构造与遍历)
- (2)四种插入方式
- (3)删查接口
- (4)map统计元素出现次数
- (5)multimap与map的核心差异
- [四、 易错点小贴士](#四、 易错点小贴士)
一、 序列式容器和关联式容器
序列式容器有vector、list、deque、string 等等,底层为线性结构 。按插入顺序存储,元素位置由插入顺序决定 。数据之间无紧密关联,可以随意的交换位置
关联式容器有set/map、unordered_set/unordered_map ,底层是非线性结构 ,按关键字key访问数据,元素位置完全由 key 决定 ,元素位置有紧密的关联关系,交换一下,它的储存结构就被破坏了
| 容器类型 | 存储方式 | 查找方式 | 核心特性 | 代表容器 |
|---|---|---|---|---|
| 序列式容器 | 按插入顺序存储,元素位置由插入顺序决定 | 按索引或迭代器顺序查找,时间复杂度为 O(N) | 元素可任意调整位置,不影响容器正常使用 | vector、list、string |
| 关联式容器 | 不按插入顺序,按关键字自动排序存储 | 按关键字查找,时间复杂度为 O(logN),效率更高 | 元素位置由关键字决定,不可以随意修改关键字,否则会破坏容器底层结构 | set、map、multiset、multimap |
二、 set系列的使用
1、 set的特性
set是单关键字关联式容器,只存一个数据 ,就像我们上一篇文章讲的二叉搜索树,但是set底层是红黑树有自己的特性
- 自动排序 :默认从小到大升序,也可指定排序规则(如降序),set底层用红黑树实现,迭代器遍历走的是中序遍历,所以是有序的
- 自动去重 :容器中不允许存在重复的关键字,重复插入的元素会被忽略
- 元素不可修改 :所有迭代器均为const类型,不可通过迭代器修改容器中的元素,否则会破坏红黑树结构,导致容器异常
- 非常高效 :增删查操作的时间复杂度均为O(logN),远远高于序列式容器
2、set的模板参数

T是关键字类型 ;Compare是排序规则,默认是升序 ,因此T要求支持比较,如果不支持或者按自己的需求走,可以自行实现仿函数传给第二个模板参数;Alloc是空间配置器,默认即可
3、 set的接口使用
使用set需包含 #include 头文件,否则会出现编译错误
(1)构造与遍历

set支持初始化列表构造、迭代器区间构造和拷贝构造,遍历方式主要有迭代器遍历和范围for遍历两种
cpp
#include<iostream>
#include<set>
using namespace std;
void test01()
{
//1、初始化列表构造,自动去重+升序
set<int> s1 = { 5,1,5,3,4,2,6,6 };
//2、降序set构造
set<int, greater<int>> s2 = { 5,1,5,3,4,2,6,6 };
//3、拷贝构造
set<int> s3(s1);
//set<int> s3 = s2; //不允许降序赋值给升序
set<int> s4(s1.begin(), s1.end());
//遍历1,迭代器遍历
set<int>::iterator it = s1.begin();
while (it != s1.end())
{
//*it = 10; //错误迭代器为const类型,不可修改元素
cout << *it << " ";
++it;
}
cout << endl;
//遍历2,范围for遍历,推荐
for (auto& e : s2)
{
cout << e << " ";
}
}
int main()
{
test01();
return 0;
}


遍历仅用于读取元素,不可修改容器中的元素,这是set迭代器的核心限制
(2)核心增删查接口






| 接口格式 | 功能说明 | 注意事项 |
|---|---|---|
| s.insert(值) | 向容器中插入指定值(关键字) | 若值已存在,插入失败,无任何效果 |
| s.erase(值) | 删除容器中指定值的元素 | 删除成功返回 1,未找到目标值返回 0 |
| s.erase(迭代器) | 删除迭代器指向的元素 | 效率高于按值删除,无需先查找目标元素 |
| s.find(值) | 查找容器中是否存在指定值 | 找到返回对应元素迭代器,未找到返回 s.end() |
| s.count(值) | 统计指定值在容器中的个数 | set自动去重,结果仅为 0(不存在)或 1(存在) |
| s.lower_bound(值) | 返回指向大于等于指定值的第一个元素的迭代器 | 与 upper_bound 配合,实现区间元素的查找与删除 |
| s.upper_bound(值) | 返回指向大于指定值的第一个元素的迭代器 | 有效区间 [lower_bound, upper_bound),左闭右开 |
cpp
void test02()
{
//初始化set,自动去重+升序
set<int> s = { 5,1,5,3,4,2,6,6 };
cout << "初始set: ";
for (auto e : s) cout << e << " ";
cout << endl;
//1、插入操作
s.insert(60);
s.insert(3); //3已经存在,插入失败
cout << "插入60之后: ";
for (auto e : s) cout << e << " ";
cout << endl;
//2、按值删除
s.erase(6);
cout << "删除6后: ";
for (auto e : s) cout << e << " ";
cout << endl;
//3、按迭代器删除
auto ret = s.find(3);
if (ret != s.end())
{
cout << "删除3:";
s.erase(ret);
for (auto e : s) cout << e << " ";
cout << endl;
}
//4、count判断元素是否存在
if (s.count(9))
{
cout << "9存在" << endl;
}
//5、区间操作:查找并删除[4,9]区间的元素
auto it1 = s.lower_bound(4); //指向>=4的第一个元素(4)
auto it2 = s.upper_bound(9); // 指向>9的第一个元素(10)
s.erase(it1, it2); // 删除[it1, it2)区间的元素
cout << "删除[4,9]的元素后: ";
for (auto e : s) cout << e << " ";
cout << endl;
}

(3)multiset与set的核心差异
multiset是set的衍生容器,底层同样基于红黑树实现 ,大部分接口与set完全一致,核心差异仅在于"允许关键字重复",无需额外学习新接口,重点掌握两者的差异即可
| 对比维度 | set | multiset |
|---|---|---|
| 关键字重复性 | 不允许重复,重复插入无效 | 允许重复,重复插入可成功 |
| insert 返回值 | pair<iterator, bool>,用于判断插入是否成功 | 仅返回 iterator,插入一定成功 |
| count 接口 | 仅返回 0(不存在)或 1(存在) | 返回关键字的实际出现次数 |
| erase(值) | 删除 0 或 1 个元素,返回删除个数 | 删除所有匹配的元素,返回删除个数 |
| 迭代器特性 | 所有迭代器均为 const,不可修改元素 | 与 set 一致,迭代器不可修改元素 |
cpp
void test03()
{
//初始化multiset,自动排序,但不自动去重(核心差异)
multiset<int> s = { 5,1,5,3,4,2,6,5,5,9,9 };
cout << "初始multiset:";
for (auto e : s) cout << e << " ";
cout << endl;
// 插入重复关键字,可成功(核心差异)
s.insert(5);
cout << "插入5后:";
for (auto e : s) cout << e << " ";
cout << endl;
// 查找:find返回第一个匹配关键字的迭代器
auto pos = s.find(5);
cout << *pos << endl;
// erase:删除所有匹配的元素,返回删除个数(核心差异)
cout << "删除了" << s.erase(5) << "个5" << endl;
cout << "删除后:";
for (auto e : s) cout << e << " ";
cout << endl;
// count:统计关键字实际出现次数(核心差异)
cout << "9出现次数:" << s.count(9);
}

(4)set/multiset的实际使用场景
1) 数组去重并排序
利用set的自动去重+排序特性
cpp
#include<iostream>
#include<vector>
#include<set>
using namespace std;
int main()
{
vector<int> nums = { 5,1,5,3,4,2,6,5 }; // 待"去重排序"的数组
set<int> s(nums.begin(), nums.end()); // 利用迭代器区间构造,自动去重+排序
nums.assign(s.begin(), s.end()); // 将去重排序后的数据放回原数组
// 输出结果:1 2 3 4 5 6
for (auto e : nums) cout << e << " ";
return 0;
}
2) 查找指定元素是否存在
当数据量较大时,set的find接口(O(logN))效率很高
cpp
bool isExist(vector<int>& nums, int num)
{
set<int> s(nums.begin(), nums.end());
auto ret = s.find(num);
return ret != s.end(); // 找到返回true,未找到返回false
}
int main() {
vector<int> nums = { 10,20,30,40,50 };
cout << isExist(nums, 30) << endl; // 输出1(true)
cout << isExist(nums, 60) << endl; // 输出0(false)
return 0;
}
三、map系列的使用(map/multimap)
1、 map的核心特性
map是键值对关联式容器 ,与set的"单关键字"不同,map存储的是<key, value>键值对,有以下特性
- 键值对存储 :每个元素都是一个pair对象pair<const Key, T>,包含key(关键字)和value(映射值),key用于排序和查找,value用于存储具体数据
- 自动排序:默认按key的升序排列,排序仅针对key,与value无关;
- Key的唯一性:不允许存在重复的key,重复插入的键值对会被忽略
- Key不可修改:可通过迭代器修改value,但不可修改key(修改key会破坏红黑树结构)
- 高效操作:增删查操作的时间复杂度均为O(logN),核心用途是实现"关键字到映射值"的快速映射(如字典、统计次数
2、 pair类型

pair是STL中封装的键值对结构体 ,,专门用来存两个绑定在一起的数据(key,value) ,是 map 容器的底层存储单元
- 一个 pair 包含两个成员 :first(对应 key,关键字)、second(对应 value,值)
- 用法 :pair<类型1, 类型2> 变量名(值1, 值2)
- 访问 :用 .first 拿第一个数据,用 .second 拿第二个数据 ,pair 里的 first(key)是 const 不可改,second(value)可以改
cpp
#include<iostream>
#include<map>
//#include<utility> //pair的头文件,map中有包含,可忽略
using namespace std;
int main()
{
//1、直接构建pair
pair<string, string> p1("left", "左边");
//2、make_pair构造,自动推导类型,推荐
auto p2 = make_pair("right", "右边");
//3、初始化列表构造C++11
pair<string, int> p3 = { "apple",5 };
// 访问key和value:通过first(key)和second(value)访问
cout << p1.first << ":" << p1.second << endl;
cout << p2.first << ":" << p2.second << endl;
cout << p3.first << ":" << p3.second << endl;
return 0;
}

3、 map的模板参数

Key是关键字(key)的类型;T是映射值(value)的类型;Compare是排序规则,默认升序
4、 map核心接口使用
(1) 构造与遍历

map支持初始化列表构造(键值对形式)、迭代器区间构造和拷贝构造,遍历方式与set类似
cpp
//1、初始化列表构造
map<string, string> m1 = { {"left","左边"},{"right","右边"} };
//2、拷贝构造
map<string, string> m2(m1);
//3、迭代器区间构造
map<string, string> m3(m1.begin(), m1.end());
//遍历1:迭代器遍历
map<string, string>::iterator it = m1.begin();
while (it != m1.end())
{
//it->first = "test"; key为const类型不能修改
it->second += "_map";
cout << it->first << ":" << it->second << endl;
it++;
}
//遍历2:范围for遍历,推荐
for (auto& kv : m1)
{
cout << kv.first << ":" << kv.second << endl;
}
//遍历3:结构化绑定C++17
for (auto& [k,v] : m1)
{
cout << k << ":" << v << endl;
}
return 0;

(2)四种插入方式

map的insert接口插入的是pair对象(键值对),返回值为pair<iterator, bool>,与set的insert逻辑一致:
- 若key不存在 :插入成功,bool为true,iterator指向新插入的键值对
- 若key已存在:插入失败,bool为false,iterator指向已存在的键值对
cpp
void test01()
{
map<string, string> dict;
//1、直接构造pair插入
dict.insert(pair<string, string>("sort", "排序"));
//2、make_pair构造pair插入
dict.insert(make_pair("const", "常量"));
//3、列表初始化插入C++11,推荐
dict.insert({ "insert", "插入" });
//4、初始化列表直接构造,构造时使用
map<string, string> dict2 = { {"left", "左边"}, {"right", "右边"} };
//插入重复key,失败
//insert返回一个pair,pair<迭代器, bool>
// ret.first:迭代器,指向插入 / 已存在的键值对节点
// ret.second:bool 值
auto ret = dict.insert({ "sort", "排序重复" });
if (!ret.second)
{ // 输出已存在的key对应的value
cout << "sort已存在,当前value:" << ret.first->second << endl;
}
}


(3)删查接口
map的删查接口与set完全一致,唯一区别是:所有操作均针对key,与value无关,时间复杂度均为O(logN)
| 接口格式 | 功能说明 | 注意事项 |
|---|---|---|
| s.find(key) | 查找指定 key 的键值对 | 找到返回对应迭代器,未找到返回 s.end() |
| s.count(key) | 统计指定 key 的个数 | map中key唯一,结果仅为0或1,常用于判断key是否存在 |
| s.erase(key) | 删除指定 key 对应的键值对 | 删除成功返回 1,未找到 key 返回 0 |
| s.erase(迭代器) | 删除迭代器指向的键值对 | 效率高于按 key 删除,无需预先查找 |
(4)map统计元素出现次数
cpp
void test02()
{
string arr[] = { "苹果","西瓜", "苹果", "西瓜", "苹果", "香蕉", "苹果" };
map<string, int> countMap;
for (auto& str : arr)
{
countMap[str]++;
// 原理:key不存在→插入{str, 0},再++→1;key存在→直接++
}
//结构化绑定:C++17
for (auto&[fruit,num] : countMap)
{
cout << fruit << ":" << num << "次" << endl;
}
}

(5)multimap与map的核心差异
multimap是map的衍生容器,与map的关系,和multiset与set的关系一致,核心差异是"允许key重复"
| 对比维度 | map | multimap |
|---|---|---|
| key 重复性 | 不允许重复,重复插入无效 | 允许重复,重复插入可成功 |
| insert 返回值 | pair<iterator, bool>,用于判断插入是否成功 | 仅返回`iterator,插入必定成功 |
| operator\[\] | 支持,兼具插入、查找、修改功能 | 不支持(存在重复key,编译器无法确定返回哪一个value) |
| erase(key) | 删除 0 或 1 个键值对,返回删除个数 | 删除所有匹配 key 的键值对,返回删除个数 |
| count(key) | 返回 0 或 1 | 返回 key 的实际出现次数 |
cpp
void test03()
{
// 初始化multimap,key可重复,自动按key升序排列
multimap<string, string> dict = { {"left","左边"},{"right","右边"} };
//插入重复key,可成功
dict.insert({ "string", "字符串" });
dict.insert({ "string", "字符序列" });
//遍历
for (auto& [k, v] : dict)
{
cout << k << ":" << v << endl;
}
//查到,find返回第一个匹配key的迭代器
auto pos = dict.find("string");
while (pos != dict.end() && pos->first == "string")
{
cout << pos->second << " ";
++pos;
}
cout << endl;
//删除所有匹配key的键值对
int num = dict.erase("string");
cout << "删除string,共删除" << num << "个" << endl;
}

四、 易错点小贴士
-
set的迭代器不可修改元素,map的迭代器不可修改key,仅可修改value,否则会破坏红黑树结构,编译报错
-
map支持operator\[\],multimap不支持,multimap 同一个 key 对应多个 value,编译器无法确定返回哪一个,语法报错
-
查找元素时,优先使用容器自身的find接口,不要使用算法库的find。set/map.find() 底层红黑树查找O(logN);std::find是顺序遍历O(N),大数据量差距极大
-
erase接口:按迭代器删除比按值删除高效,因为按值删除需要先查找元素,按迭代器删除可直接定位
-
multiset/multimap中,erase(key)会删除所有匹配的元素,若只想删除一个,需先find找到迭代器,再按迭代器删除
-
普通对象 /pair 实体:用 .,如ret.second、kv.first
迭代器 / 指针:用 ->,如ret.first->second、it->first