【C++】标准模板库STL——set /multiset/map /multimap

【C++】标准模板库STL------set /multiset/map /multimap

我们之前学习了一些容器如vector、list 这些称为序列式容器 ,本篇文章将介绍STL中另一类核心容器------关联式容器 ,其中map和set 是最常用的代表。它们底层基于红黑树 实现,拥有O(logN) 的高效增删查效率

文章目录

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

序列式容器有vector、list、deque、string 等等,底层为线性结构 。按插入顺序存储,元素位置由插入顺序决定 。数据之间无紧密关联,可以随意的交换位置

关联式容器有set/map、unordered_set/unordered_map ,底层是非线性结构 ,按关键字key访问数据,元素位置完全由 key 决定 ,元素位置有紧密的关联关系,交换一下,它的储存结构就被破坏了

容器类型 存储方式 查找方式 核心特性 代表容器
序列式容器 按插入顺序存储,元素位置由插入顺序决定 按索引或迭代器顺序查找,时间复杂度为 O(N) 元素可任意调整位置,不影响容器正常使用 vector、list、string
关联式容器 不按插入顺序,按关键字自动排序存储 按关键字查找,时间复杂度为 O(logN),效率更高 元素位置由关键字决定,不可以随意修改关键字,否则会破坏容器底层结构 set、map、multiset、multimap

二、 set系列的使用

set和multiset参考文档

1、 set的特性

set是单关键字关联式容器,只存一个数据 ,就像我们上一篇文章讲的二叉搜索树,但是set底层是红黑树有自己的特性

  1. 自动排序 :默认从小到大升序,也可指定排序规则(如降序),set底层用红黑树实现,迭代器遍历走的是中序遍历,所以是有序的
  2. 自动去重 :容器中不允许存在重复的关键字,重复插入的元素会被忽略
  3. 元素不可修改 :所有迭代器均为const类型,不可通过迭代器修改容器中的元素,否则会破坏红黑树结构,导致容器异常
  4. 非常高效 :增删查操作的时间复杂度均为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)

map/multimap参考文档

1、 map的核心特性

map是键值对关联式容器 ,与set的"单关键字"不同,map存储的是<key, value>键值对,有以下特性

  1. 键值对存储 :每个元素都是一个pair对象pair<const Key, T>,包含key(关键字)和value(映射值),key用于排序和查找,value用于存储具体数据
  2. 自动排序:默认按key的升序排列,排序仅针对key,与value无关;
  3. Key的唯一性:不允许存在重复的key,重复插入的键值对会被忽略
  4. Key不可修改:可通过迭代器修改value,但不可修改key(修改key会破坏红黑树结构)
  5. 高效操作:增删查操作的时间复杂度均为O(logN),核心用途是实现"关键字到映射值"的快速映射(如字典、统计次数

2、 pair类型

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;
}

四、 易错点小贴士

  1. set的迭代器不可修改元素,map的迭代器不可修改key,仅可修改value,否则会破坏红黑树结构,编译报错

  2. map支持operator\[\],multimap不支持,multimap 同一个 key 对应多个 value,编译器无法确定返回哪一个,语法报错

  3. 查找元素时,优先使用容器自身的find接口,不要使用算法库的find。set/map.find() 底层红黑树查找O(logN);std::find是顺序遍历O(N),大数据量差距极大

  4. erase接口:按迭代器删除比按值删除高效,因为按值删除需要先查找元素,按迭代器删除可直接定位

  5. multiset/multimap中,erase(key)会删除所有匹配的元素,若只想删除一个,需先find找到迭代器,再按迭代器删除

  6. 普通对象 /pair 实体:用 .,如ret.second、kv.first
    迭代器 / 指针:用 ->,如ret.first->second、it->first

相关推荐
Chen_harmony7 小时前
一、C++入门基础
开发语言·c++
学逆向的7 小时前
远程线程注入
开发语言·jvm·网络安全·win32
水饺编程8 小时前
第1章,[Win32 章节]:Windows 的方方面面
c语言·c++·windows·visual studio
阡陌数智8 小时前
RAG 系统的召回退化:向量库、分块、重排全链路问题排查与优化实践
开发语言·人工智能·语言模型·自然语言处理·推荐算法
geovindu8 小时前
rust: Flyweight Pattern
开发语言·后端·设计模式·rust·享元模式·结构型模式
Escalating_xu8 小时前
【C 语言】字符函数和字符串函数:从 ctype 到 strtok、strerror 全面解析
java·c语言·开发语言
悠仁さん8 小时前
【C++】auto
java·前端·c++
郝学胜-神的一滴9 小时前
C++ Templates 07:编译报错和调试手段与实战技巧
开发语言·c++·后端·软件工程·visual studio
害人终害己9 小时前
Redisson 连接 Redis 失败 报错解决方案
java·开发语言
何老师的GESP C++进阶课堂9 小时前
1、数组知识点总结
c++·青少年编程·gesp c++