【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

相关推荐
Sylvia33.1 小时前
足球数据接口开发实战:如何用火星数据API盘活赛事应用
java·服务器·开发语言·数据库·python
z落落2 小时前
C# WinForm 自定义控件
开发语言·c#
noipp2 小时前
推荐题目:洛谷 P13554 【MX-X15-T1】奶龙龙
c语言·数据结构·c++·算法·编程·洛谷
geovindu2 小时前
go: Enumeration Algorithm
开发语言·后端·算法·golang·枚举算法
库玛西3 小时前
深入剖析 Linux 线程机制与分页式存储管理
linux·服务器·c++·笔记
气概3 小时前
QT集成basler相机
开发语言·数码相机·qt
苦瓜汤补钙3 小时前
Oracle JDK8 环境配置-Win11
开发语言·数据库·笔记·oracle
overmind3 小时前
oeasy Python 102 集合_运算_交集_并集_差集_对称差集
开发语言·python
众少成多积小致巨3 小时前
Android C++ 编码规范指南
android·c++·google