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

吃透C++ STL map/set:从入门到实战,新手也能轻松上手

文章目录

  • 前言
  • 一、先搞懂:map和set是什么?核心区别在哪?
  • 二、set使用详解:去重+排序,一键搞定
  • 三、map使用详解:键值映射,高效查找
  • 四、map和set的常见避坑点(新手必看)
  • 五、map/set vs 其他容器(怎么选?)
  • 六、总结

前言

在C++ STL(标准模板库)中,map和set是最常用的关联式容器,它们底层基于红黑树(一种平衡二叉搜索树)实现,天生具备“自动排序”和“高效检索”的特性,能极大简化我们的代码开发,尤其在需要快速查找、去重、排序的场景中,堪称“神器”。

很多新手刚接触map和set时,常常混淆两者的用途:什么时候用set?什么时候用map?它们的接口有什么区别?今天这篇博客,全程以“实用”为核心,不搞复杂理论,只讲“怎么用、用在哪、避坑点”,搭配极简实战代码,新手跟着敲一遍,就能轻松掌握map和set的核心用法。


一、先搞懂:map和set是什么?核心区别在哪?

map和set同属关联式容器,底层都是红黑树,因此都具备O(log₂N)的插入、查找、删除效率,且元素会自动排序,但两者的核心用途和存储结构完全不同,这是区分它们的关键。

1. 核心定义与存储结构

  • set(集合):仅存储“键(key)”,不存储“值(value)”,且不允许重复key,元素会按照key的默认规则(升序)自动排序。可以理解为“去重+排序”的容器,比如存储一个班级的学号(不重复,且需要有序)。

  • map(映射):存储“键值对(key-value)”,key唯一,value可重复,元素会按照key的默认规则(升序)自动排序。可以理解为“字典”,key是“单词”,value是“释义”,通过key能快速找到对应的value,比如存储学生的学号(key)和姓名(value)。

2. 核心区别(一张表看懂)

特性

set

map

存储内容

仅key(键)

key-value(键值对)

key是否唯一

是(不允许重复)

是(不允许重复)

排序依据

按key自动排序

按key自动排序

核心用途

去重、排序、快速查找某个值是否存在

键值映射、通过key快速获取value、排序

底层结构

红黑树(平衡二叉搜索树)

红黑树(平衡二叉搜索树)

补充:STL还提供了multiset和multimap,允许key重复,用法和set、map基本一致,仅在“key唯一性”上有差异,本文重点讲解最常用的set和map(key唯一)。


二、set使用详解:去重+排序,一键搞定

set的用法非常简单,核心围绕“插入、查找、删除、遍历”四个操作,无需手动排序,插入后元素自动按升序排列,且自动去重。

1. 头文件与定义

使用set必须包含头文件<set>,定义时需指定存储的数据类型(即key的类型),默认排序规则为升序:

#include <iostream> #include <set> // set的头文件 using namespace std; // 定义set,存储int类型,默认升序 set<int> s; // 可选:定义降序set(需指定排序规则) set<int, greater<int>> s_desc; // 降序排列

2. 核心接口(实战常用,必掌握)

set的接口不多,重点掌握以下6个,就能应对大部分场景,搭配代码示例理解更直观:

(1)插入元素:insert()

插入单个元素,自动去重、自动排序;也可插入区间内的元素。

set<int> s; // 插入单个元素 s.insert(5); s.insert(2); s.insert(8); s.insert(2); // 重复元素,插入失败,不会报错 // 插入区间元素(从数组插入) int arr[] = {3, 7, 1}; s.insert(arr, arr + 3); // 此时set中的元素:1, 2, 3, 5, 7, 8(自动升序、去重)
(2)查找元素:find()

查找指定key是否存在,返回迭代器:找到则返回对应元素的迭代器,未找到则返回s.end()(set的末尾迭代器,指向元素后面的位置)。

set<int> s = {1, 2, 3, 5, 7, 8}; // 查找key=5 auto it = s.find(5); if (it != s.end()) { cout << "找到元素:" << *it << endl; // 输出:找到元素:5 } else { cout << "未找到元素" << endl; } // 查找key=4 it = s.find(4); if (it == s.end()) { cout << "未找到元素" << endl; // 输出:未找到元素 }
(3)删除元素:erase()

有三种删除方式:按迭代器删除、按key删除、删除区间内元素,最常用的是前两种。

set<int> s = {1, 2, 3, 5, 7, 8}; // 1. 按key删除 s.erase(3); // 删除key=3的元素,返回删除的个数(0或1) // 2. 按迭代器删除(先找到元素,再删除) auto it = s.find(5); if (it != s.end()) { s.erase(it); // 删除key=5的元素 } // 3. 删除区间元素(删除从begin到end的所有元素,即清空set) // s.erase(s.begin(), s.end()); // 此时set中的元素:1, 2, 7, 8
(4)遍历元素:迭代器/范围for

set支持迭代器遍历和C++11后的范围for遍历,遍历顺序就是元素的排序顺序(默认升序)。

set<int> s = {1, 2, 7, 8}; // 1. 迭代器遍历 for (set<int>::iterator it = s.begin(); it != s.end(); ++it) { cout << *it << " "; // 输出:1 2 7 8 } cout << endl; // 2. 范围for遍历(C++11及以上,更简洁) for (auto& num : s) { cout << num << " "; // 输出:1 2 7 8 } cout << endl;
(5)获取元素个数:size()
set<int> s = {1, 2, 7, 8}; cout << "set元素个数:" << s.size() << endl; // 输出:4
(6)清空元素:clear()
set<int> s = {1, 2, 7, 8}; s.clear(); // 清空所有元素 cout << "清空后元素个数:" << s.size() << endl; // 输出:0

3. set实战场景:去重+排序

最典型的场景:对一组数据去重并排序,无需手动写排序和去重逻辑,set一键搞定。

#include <iostream> #include <set> using namespace std; int main() { // 一组重复、无序的数据 int arr[] = {5, 2, 8, 2, 3, 7, 1, 5, 9}; int n = sizeof(arr) / sizeof(arr[0]); // 用set去重+排序 set<int> s(arr, arr + n); // 输出去重排序后的结果 cout << "去重排序后:"; for (auto& num : s) { cout << num << " "; // 输出:1 2 3 5 7 8 9 } return 0; }

三、map使用详解:键值映射,高效查找

map的核心是“key-value”映射,key唯一且排序,通过key能快速找到对应的value,用法和set类似,但多了对value的操作,重点掌握“键值对的插入、访问、修改”。

1. 头文件与定义

使用map必须包含头文件<map>,定义时需指定key和value的类型,默认按key升序排序:

#include <iostream> #include <map> // map的头文件 using namespace std; // 定义map:key为int(学号),value为string(姓名),默认升序 map<int, string> m; // 可选:定义降序map map<int, string, greater<int>> m_desc; // 按key降序排列

2. 核心接口(实战常用,必掌握)

map的接口和set有很多相似之处,但增加了对value的操作,重点掌握以下7个:

(1)插入键值对:insert() / 下标[]

有两种常用插入方式:insert()插入pair对象,下标[]直接赋值(更简洁),注意:下标[]若key不存在,会自动插入该key,value为默认值(如string为空串)。

map<int, string> m; // 方式1:insert()插入pair对象(推荐,可判断是否插入成功) m.insert(pair<int, string>(101, "张三")); m.insert(make_pair(102, "李四")); // make_pair更简洁 // 插入重复key,返回pair<迭代器, bool>,bool为false表示插入失败 auto ret = m.insert(make_pair(101, "王五")); if (!ret.second) { cout << "key=101已存在,插入失败" << endl; } // 方式2:下标[]插入(简洁,但会自动创建不存在的key) m[103] = "赵六"; // 插入key=103,value="赵六" m[104] = "孙七"; // 此时map中的键值对(按key升序):(101,张三), (102,李四), (103,赵六), (104,孙七)
(2)访问value:下标[] / find() + 迭代器

两种访问方式,下标[]更简洁,但要注意:若key不存在,会自动插入该key(value为默认值);find()更安全,不会自动插入。

map<int, string> m = {{101, "张三"}, {102, "李四"}, {103, "赵六"}}; // 方式1:下标[]访问(简洁) cout << "101号学生:" << m[101] << endl; // 输出:101号学生:张三 cout << "104号学生:" << m[104] << endl; // key=104不存在,自动插入,value为空串,输出空 // 方式2:find() + 迭代器(安全,推荐) auto it = m.find(102); if (it != m.end()) { cout << "102号学生:" << it->second << endl; // 输出:102号学生:李四 } // 访问不存在的key,不会自动插入 it = m.find(105); if (it == m.end()) { cout << "105号学生不存在" << endl; }

注意:map的迭代器指向的是“键值对(pair)”,通过it->first访问key,it->second访问value。

(3)修改value:下标[] / find() + 迭代器

修改value的前提是key已存在,两种方式均可,下标[]更简洁。

map<int, string> m = {{101, "张三"}, {102, "李四"}}; // 方式1:下标[]修改 m[101] = "张三丰"; // 将101号的value改为"张三丰" // 方式2:find() + 迭代器修改 auto it = m.find(102); if (it != m.end()) { it->second = "李世民"; // 将102号的value改为"李世民" } // 输出修改后的值 cout << "101号:" << m[101] << endl; // 输出:101号:张三丰 cout << "102号:" << m[102] << endl; // 输出:102号:李世民
(4)删除元素:erase()

和set用法一致,支持按key删除、按迭代器删除、删除区间元素。

map<int, string> m = {{101, "张三"}, {102, "李四"}, {103, "赵六"}}; // 1. 按key删除 m.erase(102); // 删除key=102的键值对 // 2. 按迭代器删除 auto it = m.find(103); if (it != m.end()) { m.erase(it); // 删除key=103的键值对 } // 3. 删除区间元素(清空map) // m.erase(m.begin(), m.end()); // 此时map中仅剩余:(101, 张三)
(5)遍历元素:迭代器/范围for

遍历顺序按key的排序规则,重点关注键值对的访问方式。

map<int, string> m = {{101, "张三"}, {102, "李四"}, {103, "赵六"}}; // 1. 迭代器遍历 for (map<int, string>::iterator it = m.begin(); it != m.end(); ++it) { // it->first是key,it->second是value cout << "学号:" << it->first << ",姓名:" << it->second << endl; } // 2. 范围for遍历(C++11及以上) for (auto& pair : m) { cout << "学号:" << pair.first << ",姓名:" << pair.second << endl; }
(6)获取元素个数:size()
(7)清空元素:clear()

和set用法完全一致,直接调用接口即可。

map<int, string> m = {{101, "张三"}, {102, "李四"}}; cout << "元素个数:" << m.size() << endl; // 输出:2 m.clear(); cout << "清空后个数:" << m.size() << endl; // 输出:0

3. map实战场景:键值映射

最典型的场景:存储键值对,通过key快速查找value,比如存储学生学号与成绩、单词与释义等。

#include <iostream> #include <map> #include <string> using namespace std; int main() { // 存储学生学号(key)和成绩(value) map<int, int> student_score; // 插入数据 student_score.insert(make_pair(101, 95)); student_score.insert(make_pair(102, 88)); student_score[103] = 92; student_score[104] = 79; // 查找并输出102号学生的成绩 auto it = student_score.find(102); if (it != student_score.end()) { cout << "102号学生成绩:" << it->second << endl; // 输出:88 } // 修改104号学生的成绩 student_score[104] = 85; cout << "修改后104号学生成绩:" << student_score[104] << endl; // 输出:85 // 遍历所有学生成绩(按学号升序) cout << "所有学生成绩:" << endl; for (auto& pair : student_score) { cout << "学号:" << pair.first << ",成绩:" << pair.second << endl; } return 0; }

四、map和set的常见避坑点(新手必看)

  • 误区1:map的下标[]会自动插入不存在的key——这是新手最容易踩的坑!如果只是想“查找”value,不要用下标[],用find()更安全,避免误插入多余的key。

  • 误区2:set和map可以直接修改元素——不可以!set的元素是const类型(不能修改,否则会破坏排序和唯一性);map只能修改value,不能修改key(修改key会破坏排序和唯一性)。

  • 误区3:插入重复key会报错——不会报错!set和map插入重复key时,插入会失败,但程序不会崩溃,可通过insert()的返回值判断是否插入成功。

  • 误区4:set和map的迭代器可以随意加减——不可以!它们的迭代器是“双向迭代器”,只能用++、--操作,不能用+1、-1(和vector的随机迭代器不同)。

  • 误区5:底层是红黑树,所以插入效率一定很高——红黑树插入时会自动平衡,效率是O(log₂N),但比vector的尾插(O(1))慢,适合“频繁查找、删除”的场景,不适合“频繁尾插”的场景。


五、map/set vs 其他容器(怎么选?)

很多新手不知道什么时候用map/set,什么时候用vector/list,这里给出明确的选择依据:

  • 需要去重、排序,优先用set;

  • 需要键值映射、快速通过key找value,优先用map;

  • 需要频繁随机访问、尾插尾删,优先用vector;

  • 需要频繁插入、删除(非尾端),且不需要排序,优先用list;

  • 需要允许key重复,用multiset/multimap(用法和set/map基本一致)。


六、总结

map和set是C++ STL中最实用的关联式容器,核心优势在于“自动排序”和“O(log₂N)的高效操作”,底层红黑树的实现让它们无需我们手动管理排序和平衡,极大提升开发效率。

对于新手来说,重点掌握:

1. 区分map和set:set存key(去重排序),map存key-value(键值映射);

2. 核心接口:insert()、find()、erase()、size()、clear(),以及map的下标访问;

3. 避坑点:map下标[]的自动插入、不可修改set的元素和map的key。

其实map和set的用法并不复杂,多敲几遍实战代码,熟悉接口的使用场景,就能轻松掌握。它们在面试和实际开发中都非常常用,学好它们,能让你的代码更简洁、高效、易维护。

http://www.jsqmd.com/news/774952/

相关文章:

  • 车载诊断架构---解答售后关于Service 19 06疑问带来的反思
  • 3203黄大年茶思屋榜文保姆级全落地解法「32期3题」量子启发式算法|大规模百万节点图平衡最小分割优化
  • 用Python+PuLP搞定钢管运输优化:手把手复现2000年数模国赛B题
  • 大语言模型如何构建创业者认知代理:从特征工程到RAG应用
  • dotnet-skills:让AI助手掌握现代.NET开发最佳实践
  • 欧拉回路(一笔画)
  • “灵语星火”第二阶段团队记录(一)
  • 如何在华为HarmonyOS设备上部署microG服务:解决签名验证的完整技术指南
  • 开源情报实战指南:从工具到体系的OSINT方法论与自动化实践
  • Emacs光标管理库cursory:实现情境感知的自动切换与主题集成
  • 轻量级唤醒词检测:从MFCC特征到CNN模型在边缘设备的实践
  • 基于工作流的低代码AI应用开发:Flock平台核心架构与实战指南
  • 为什么很多人 DFS 写得飞起,一到「矩阵最长递增路径」就彻底懵了?
  • [特殊字符] 数组中的递增三元组:O(n) 时间高效查找,面试必考!
  • “灵语星火”第二阶段团队记录(二)
  • 给Claude Code装个仪表盘 Claude HUD保姆级教程命令行也能直观可控
  • 告别纯寄存器:用STC-ISP工具图形化配置STC8H的PWM,5分钟生成代码
  • CUDA内核优化:从手工调优到AI驱动的自动化实践
  • 如何免费下载TIDAL高品质音乐:tidal-dl-ng完整使用教程
  • 明代裙装形制融入现代中国男装设计研究
  • python系列【仅供参考】:JS的解析与Js2Py使用
  • 通用网页内容提取器xungen:基于示例驱动的自动化数据抓取方案
  • 深度优化:2345清理王系统碎片清理功能详解
  • 在多模型聚合场景下体验 Taotoken 的路由与容灾能力
  • AI编程助手Awesome清单:开发者选型指南与实战评测
  • Godot XR Tools:加速VR/AR开发的模块化工具集与实战指南
  • 从零实现ChatGPT:深入解析Transformer架构与自注意力机制
  • 2026年最佳健身小程序推荐榜单,帮你解锁智能运动新体验
  • 前端响应式设计:最佳实践
  • mysql修改字段类型时如何避免中断业务_inplace与copy算法详解