全面解析 C++ STL 中的 set 和 map
C++ 标准模板库(STL)中的关联式容器以其强大的功能和高效性成为开发者解决复杂数据组织问题的重要工具。其中,set 和 map 是最常用的两类关联容器。本篇博客将从基本特性、底层实现、用法详解、高级案例以及性能优化等多个角度,详细解读它们的设计与使用。
目录
1. 什么是关联式容器
关联式容器的核心特性
2. set 容器详解
2.1 基本概念与特性
2.2 底层实现:红黑树
红黑树的特性
红黑树的操作
2.3 构造函数
2.4 常用操作与复杂度分析
插入操作
查找操作
删除操作
遍历
2.5 特殊操作与技巧
(1) 自定义排序规则
(2) 范围删除
(3) 应用:求两个数组的交集
2.6 multiset 的区别与应用
1. 什么是关联式容器
关联式容器是一类根据关键字组织和管理数据的容器。与序列式容器(如 vector 和 list)相比,关联式容器的主要区别如下:
| 特性 | 关联式容器(set/map) | 序列式容器(vector/list) |
|---|---|---|
| 数据存储顺序 | 按关键字排序 | 按插入顺序 |
| 数据访问复杂度 | O(logN)O(\log N)O(logN) | O(1)O(1)O(1) 或 O(N)O(N)O(N) |
| 是否支持随机访问 | 否 | 是 |
| 是否支持按索引访问 | 否 | 是 |
关联式容器分为有序和无序两类:
- 有序容器:如
set和map,基于平衡二叉树(红黑树)实现,数据按排序规则组织。 - 无序容器:如
unordered_set和unordered_map,基于哈希表实现,提供更高效的查找速度,但不保证元素顺序。
关联式容器的核心特性
- 键值对:关联式容器通过关键字对元素进行组织,
set中的关键字即为数据本身,而map则以键值对形式存储数据。 - 自动排序:有序容器会自动对数据进行排序(升序或自定义规则)。
- 高效操作:插入、删除、查找的平均时间复杂度为 O(logN)O(\log N)O(logN)(红黑树实现)。
2. set 容器详解
2.1 基本概念与特性
set 是一种集合数据结构,用于存储唯一且自动排序的元素。它的主要特点如下:
- 数据唯一性:同一元素不能重复插入。
- 自动排序:默认按升序排序,可通过自定义比较器更改排序规则。
- 迭代器类型:
set支持双向迭代器,不支持随机访问。 - 底层实现:使用红黑树作为存储结构。
2.2 底层实现:红黑树
红黑树的特性
红黑树是一种平衡二叉搜索树,满足以下性质:
- 每个节点是红色或黑色。
- 根节点是黑色。
- 每个叶子节点(
nullptr或 NIL 节点)是黑色。 - 如果一个节点是红色,则其子节点必须是黑色(即红色节点不能相邻)。
- 从任意节点到其每个叶子节点的路径都包含相同数量的黑色节点。
红黑树的操作
- 插入:通过旋转和重新着色,确保平衡性和红黑性质。
- 删除:比插入更复杂,同样通过旋转和着色维护树的性质。
- 查找:沿树遍历,时间复杂度为 O(logN)O(\log N)O(logN)。
在 set 和 map 中,红黑树用来高效实现元素的有序存储和快速查找。
2.3 构造函数
set 提供以下几种构造方式:
- 默认构造:创建一个空集合。
set<int> s; - 初始化列表构造:直接用
{}初始化集合。set<int> s = {3, 1, 4, 1, 5, 9}; // 重复元素自动去重 - 迭代器区间构造:从其他容器的元素构造集合。
vector<int> v = {1, 2, 3, 4}; set<int> s(v.begin(), v.end()); - 自定义比较规则:
set<int, greater<int>> s = {3, 1, 4}; // 按降序排序
2.4 常用操作与复杂度分析
| 操作 | 函数 | 复杂度 | 说明 |
|---|---|---|---|
| 插入 | insert(value) | O(logN)O(\log N)O(logN) | 插入元素,若已存在则插入失败 |
| 删除 | erase(value) | O(logN)O(\log N)O(logN) | 删除指定元素 |
| 查找 | find(value) | O(logN)O(\log N)O(logN) | 返回迭代器,指向目标元素 |
| 统计 | count(value) | O(logN)O(\log N)O(logN) | 判断元素是否存在,结果为 0 或 1 |
| 遍历 | begin(), end() | O(N)O(N)O(N) | 正向迭代访问所有元素 |
| 下界/上界 | lower_bound()/upper_bound() | O(logN)O(\log N)O(logN) | 返回 >= / > 某值的第一个元素的迭代器 |
插入操作
set<int> s;
auto res = s.insert(10); // 插入 10
if (res.second) {cout << "插入成功" << endl;
} else {cout << "插入失败" << endl;
}
查找操作
if (s.find(20) != s.end()) {cout << "找到元素 20" << endl;
}
删除操作
s.erase(10); // 删除值为 10 的元素
遍历
for (int x : s) {cout << x << " "; // 正向遍历
}
for (auto it = s.rbegin(); it != s.rend(); ++it) {cout << *it << " "; // 反向遍历
}
2.5 特殊操作与技巧
(1) 自定义排序规则
set 默认按升序排序,使用仿函数或 std::greater 可修改排序规则:
set<int, greater<int>> s = {3, 1, 4};
(2) 范围删除
删除值在 [low, high) 范围内的所有元素:
s.erase(s.lower_bound(10), s.upper_bound(50));
(3) 应用:求两个数组的交集
vector<int> intersection(const vector<int>& nums1, const vector<int>& nums2) {set<int> s1(nums1.begin(), nums1.end());set<int> s2(nums2.begin(), nums2.end());vector<int> result;for (int x : s1) {if (s2.count(x)) result.push_back(x);}return result;
}
2.6 multiset 的区别与应用
multiset 与 set 的区别在于:
multiset允许存储重复元素。- 插入、删除和查找操作的接口与
set相同,但返回的结果会包含重复项。
multiset<int> ms = {1, 2, 2, 3};
ms.insert(2); // 再次插入 2
3. map 容器详解
3.1 基本概念与特性
map 是一个关联式容器,用于存储键值对(key-value)。与 set 相比,map 不仅存储键(key),还存储与每个键关联的值(value)。map 的主要特点包括:
- 键唯一性:每个键在
map中都是唯一的。 - 自动排序:默认按照键的升序排序,也可以通过自定义比较器来更改排序规则。
- 底层实现:基于红黑树实现,操作复杂度为 O(logN)O(\log N)O(logN)。
- 支持随机访问:与
set不同,map中存储的键值对支持通过键快速查找对应的值。
map<int, string> m;
m[1] = "apple"; // 插入键值对 (1, "apple")
m[2] = "banana"; // 插入键值对 (2, "banana")
m[3] = "cherry"; // 插入键值对 (3, "cherry")
内部存储结构
map 使用红黑树存储数据,保证了所有元素按键值自动排序。在 map 中,每个节点存储一个 pair<const Key, T>,其中 const Key 表示键,T 表示值。红黑树的特点确保了查找、插入和删除操作的时间复杂度都为 O(logN)O(\log N)O(logN)。
3.2 构造与初始化
map 提供了多种构造方法,以适应不同的使用场景:
-
默认构造:创建一个空
map。map<int, string> m; -
初始化列表构造:通过初始化列表直接创建
map。map<int, string> m = {{1, "apple"}, {2, "banana"}, {3, "cherry"}}; -
范围构造:从另一个容器(如
set、vector等)构造map。vector<pair<int, string>> v = {{1, "apple"}, {2, "banana"}}; map<int, string> m(v.begin(), v.end()); -
自定义比较器:通过传入自定义比较器,指定键的排序方式。
map<int, string, greater<int>> m; // 降序排序 m[2] = "banana"; m[1] = "apple";
3.3 常用操作与复杂度分析
| 操作 | 函数 | 复杂度 | 说明 |
|---|---|---|---|
| 插入 | insert(pair) | O(logN)O(\log N)O(logN) | 插入一个键值对,若已存在则插入失败 |
| 插入或修改 | operator[] | O(logN)O(\log N)O(logN) | 插入新元素或修改已有元素的值 |
| 查找 | find(key) | O(logN)O(\log N)O(logN) | 查找指定键,返回键值对的迭代器 |
| 统计 | count(key) | O(logN)O(\log N)O(logN) | 查找指定键是否存在(map 中为 0 或 1) |
| 删除 | erase(key) | O(logN)O(\log N)O(logN) | 删除指定键及其对应的值 |
| 遍历 | begin(), end() | O(N)O(N)O(N) | 正向遍历所有元素 |
| 下界/上界 | lower_bound(key)/upper_bound(key) | O(logN)O(\log N)O(logN) | 查找大于等于某值或大于某值的第一个元素 |
插入与查找操作
-
插入:可以通过
insert方法插入新的键值对,也可以通过operator[]插入或修改键值对。map<int, string> m; m.insert({1, "apple"}); m[2] = "banana"; // 插入或修改 -
查找:
find方法返回一个迭代器,指向指定键的键值对,若未找到则返回end()。auto it = m.find(1); if (it != m.end()) {cout << "Found: " << it->second << endl; // 输出 "apple" }
删除操作
删除某个键值对:
m.erase(1); // 删除键为 1 的元素
3.4 遍历与修改
map 提供了多种遍历方法:
-
范围 for:
for (const auto& [key, value] : m) {cout << key << ": " << value << endl; } -
传统迭代器:
for (auto it = m.begin(); it != m.end(); ++it) {cout << it->first << ": " << it->second << endl; }
修改值
可以通过迭代器直接修改值,operator[] 也支持修改已有键的值:
m[2] = "grape"; // 修改键为 2 的值为 "grape"
auto it = m.find(2);
if (it != m.end()) {it->second = "orange"; // 通过迭代器修改值
}
3.5 特殊操作与进阶技巧
(1) 下界与上界
通过 lower_bound() 和 upper_bound() 方法,可以获取某个键的下界和上界,常用于区间查找。
lower_bound(key):返回第一个大于等于key的元素。upper_bound(key):返回第一个大于key的元素。
map<int, string> m = {{1, "apple"}, {2, "banana"}, {3, "cherry"}};
auto lb = m.lower_bound(2); // 返回键为 2 或大于 2 的第一个元素
cout << lb->second << endl; // 输出 "banana"
(2) 自定义排序规则
如同 set,map 也可以通过自定义比较器来实现不同的排序规则。
map<int, string, greater<int>> m = {{1, "apple"}, {3, "cherry"}, {2, "banana"}};
for (const auto& [key, value] : m) {cout << key << ": " << value << endl;
} // 输出:3: cherry 2: banana 1: apple
(3) 范围删除
删除某个键值范围内的元素,常用于清除一段区间:
map<int, string> m = {{1, "apple"}, {2, "banana"}, {3, "cherry"}};
m.erase(m.lower_bound(2), m.upper_bound(3)); // 删除键为 2 和 3 的元素
3.6 multimap 的区别与应用
multimap 是 map 的扩展,允许相同的键有多个值(即支持键的冗余)。与 map 的区别在于,multimap 在插入重复键时不会丢失数据,而 map 会自动覆盖原有键。
multimap<int, string> mm;
mm.insert({1, "apple"});
mm.insert({1, "banana"});
mm.insert({2, "cherry"});for (const auto& [key, value] : mm) {cout << key << ": " << value << endl; // 输出:1: apple 1: banana 2: cherry
}
multimap 在某些场景下非常有用,例如存储学生成绩时,可能有多个学生取得相同的分数。
4. 高级案例:综合利用 set 和 map
4.1 查找两个数组的交集
vector<int> intersection(const vector<int>& nums1, const vector<int>& nums2) {set<int> s1(nums1.begin(), nums1.end());set<int> s2(nums2.begin(), nums2.end());vector<int> result;for (int x : s1) {if (s2.count(x)) result.push_back(x);}return result;
}
4.2 构建词频统计表
map<string, int> wordCount(const vector<string>& words) {map<string, int> wordMap;for (const string& word : words) {wordMap[word]++;}return wordMap;
}
4.3 高效查找链表中的环
bool hasCycle(ListNode* head) {set<ListNode*> visited;while (head != nullptr) {if (visited.find(head) != visited.end()) {return true; // 找到环}visited.insert(head);head = head->next;}return false;
}
5. 性能优化与注意事项
5.1 使用 unordered_map 和 unordered_set
在很多查找密集型的应用中,unordered_map 和 unordered_set 基于哈希表实现,提供常数时间复杂度 O(1)O(1)O(1) 的查找和插入操作。它们的性能优势适用于不需要保持元素顺序的场景。
5.2 避免不必要的拷贝
当插入大量数据时,可以使用 emplace() 来避免不必要的对象拷贝。emplace() 可以直接构造元素,而无需创建临时对象。
map<int, string> m;
m.emplace(1, "apple"); // 不会发生拷贝
5.3 避免频繁修改键
map 不支持修改键,修改键会导致数据结构破坏。因此,避免频繁修改键,而应使用新的键值对进行插入和删除。
6. 总结
通过本文的详细解析,我们全面了解了 C++ 中 set 和 map 容器的使用、底层实现以及高效操作技巧。掌握这些基本知识后,开发者可以灵活使用 set 和 map 来处理各种复杂的关联数据问题,从而提高程序的效率和可读性。
在实际开发中,选择合适的容器(如 map 与 unordered_map,set 与 unordered_set)可以帮助我们应对不同的数据处理需求,避免性能瓶颈。希望通过本文的学习,你能够深入掌握这些强大的容器,提升 C++ 编程技能。
相关文章:
全面解析 C++ STL 中的 set 和 map
C 标准模板库(STL)中的关联式容器以其强大的功能和高效性成为开发者解决复杂数据组织问题的重要工具。其中,set 和 map 是最常用的两类关联容器。本篇博客将从基本特性、底层实现、用法详解、高级案例以及性能优化等多个角度,详细…...
css:怎么设置div背景图的透明度为0.6不影响内部元素
目录 1.前言 2.解决思路 3.具体实例 4.另外一种实例 5.总结 1.前言 div背景图为project-bg.png,设置div透明度为0.6;div内的名称、数值受透明度影响颜色显示不正常;怎么设置背景图的透明度为0.6不影响内部元素; 2.解决思路 …...
Kubernetes ConfigMaps
文章目录 简介创建ConfigMaps通过命令行使用字面值创建 ConfigMap。从文件创建ConfigMaps从多个文件创建 ConfigMap从目录创建 ConfigMap使用 YAML 创建 ConfigMap 使用ConfigMaps使用 ConfigMaps作为环境变量使用 ConfigMap 作为卷挂载使用 ConfigMap 中的特定的key ConfigMap…...
前端热门面试题目[一](HTML、CSS、Javascript、Node、Vue、React)
如何设计一个前端页面,实现PC端访问展示Web应用,移动端访问展示H5应用? 为了实现这一功能,通常需要使用响应式设计或者服务器端检测用户设备并返回相应的页面。以下是一些实现方法: 响应式设计:通过CSS媒…...
Swift 宏(Macro)入门趣谈(五)
概述 苹果在去年 WWDC 23 中就为 Swift 语言新增了“其利断金”的重要小伙伴 Swift 宏(Swift Macro)。为此,苹果特地用 2 段视频(入门和进阶)颇为隆重的介绍了它。 那么到底 Swift 宏是什么?有什么用&…...
ES6 Set、Map、WeakSet、WeakMap 四者辨析与实战应用详解
在 ES6 中,Set 和 Map 是两种非常重要的新增数据结构,它们都具有独特的特性和用途,能够帮助开发者更高效地处理和管理数据。除此之外,WeakSet 和 WeakMap 作为这两种数据结构的变种,也具有一些特殊的功能。下面我会从 Set 数据结构、Map 数据结构、WeakSet 和 WeakMap 对比…...
【数据结构】哈希表实现
前言 在本篇博客中,作者将会带领你使用C语言来实现一个哈希表。 一.什么是哈希表 在实现哈希表之前,我们先来学习一下什么是哈希表。 在传统的数据结构中,例如数组,链表和二叉平衡树等数据结构,这些数据结构的元素关键…...
Verilog的线与类型与实例化模块
1、线与类型 在Verilog中,线与(wire-AND)类型通常用于描述多个信号进行逻辑与(AND)操作的电路行为。虽然Verilog本身没有直接定义一种名为“线与”的数据类型,但可以通过使用wire类型结合特定的逻辑操作来…...
芯片测试-RF中的S参数,return loss, VSWR,反射系数,插入损耗,隔离度等
RF中的S参数,return loss, VSWR,反射系数,插入损耗,隔离度 💢S参数💢💢S11与return loss,VSWR,反射系数💢💢S21,插入损耗和增益&#…...
强化学习的几个主要方法(策略梯度、PPO、REINFORCE实现等)(上)
本笔记有大量参考蘑菇书EasyRL https://datawhalechina.github.io/easy-rl/#/ 包括其配图和部分文本。 1. 基本概念 1.1 基本流程 强化学习是一种学习框架,其中智能体(Agent) 通过与 环境(Environment) 的交互&#…...
Git远程仓库操作
文章目录 远程仓库连接Gitee克隆代码 多人协同问题说明 🏡作者主页:点击! 🤖Git专栏:点击! ⏰️创作时间:2024年12月1日13点10分 远程仓库 Git 是分布式版本控制系统,同一个 Git …...
GAGAvatar: Generalizable and Animatable Gaussian Head Avatar 学习笔记
1 Overall GAGAvatar(Generalizable and Animatable Gaussian Avatar),一种面向单张图片驱动的可动画化头部头像重建的方法,解决了现有方法在渲染效率和泛化能力上的局限。 旋转参数 现有方法的局限性: 基于NeRF的方…...
什么是VISUAL STUDIO CODE (V S CODE)
Visual Studio Code(简称VS Code)是由微软开发的一个免费的、开源的源代码编辑器。它是一个轻量级但功能强大的工具,支持多种编程语言和框架,广泛用于开发各种应用程序,尤其是Web开发。VS Code具备以下特点:…...
2024年09月中国电子学会青少年软件编程(Python)等级考试试卷(三级)答案 + 解析
青少年软件编程(Python)等级考试试卷(三级) 分数:100 题数:38 一、单选题(共25题,共50分) 1. 以下表达式的值为True的是?( ) A. all( ,1,2,3) B. any([]) C. bool(abc) D. divmod(6,0)...
C++初阶——动态内存管理
目录 1、C/C内存区域划分 2、C动态内存管理:malloc/calloc/realloc/free 3、C动态内存管理:new/delete 3.1 new/delete内置类型 3.2 new/delete自定义类型 4、operator new与operator delete函数 5、new和delete的实现原理 5.1 内置类型 5.2 自定…...
如何查看阿里云ddos供给量
要查看阿里云上的 DDoS 攻击量,你可以通过阿里云的 云盾 DDoS 防护 服务来进行监控和查看攻击数据。阿里云提供了详细的流量监控、攻击日志以及攻击趋势分析工具,帮助用户实时了解 DDoS 攻击的情况。以下是九河云总结的查看 DDoS 攻击量的步骤࿱…...
MySQL中的事务隔离全详解
第一部分:MySQL事务的特性与并行事务引发的问题 1. 什么是事务及其四大特性(ACID)? 事务(Transaction)是数据库操作的基本单位,它将一组操作组合在一起,以确保这些操作作为一个整体…...
异常--C++
文章目录 一、异常的概念及使用1、异常的概念2、异常的抛出和捕获3、栈展开4、查找匹配的处理代码5、异常重新抛出6、异常安全问题7、异常规范 二、标准库的异常 一、异常的概念及使用 1、异常的概念 异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并…...
SeggisV1.0 遥感影像分割软件【源代码】讲解
在此基础上进行二次开发,开发自己的软件,例如:【1】无人机及个人私有影像识别【2】离线使用【3】变化监测模型集成【4】个人私有分割模型集成等等,不管是您用来个人学习还是公司研发需求,都相当合适,包您满…...
锁-读写锁-Swift
实现一 pthread_mutex_t: ReadWriteLock/Sources/ReadWriteLock at main SomeRandomiOSDev/ReadWriteLock GitHub https://swiftpackageindex.com/reers/reerkit/1.0.39/documentation/reerkit/readwritelock/ // // Copyright © 2022 reers. // // Pe…...
利用ngx_stream_return_module构建简易 TCP/UDP 响应网关
一、模块概述 ngx_stream_return_module 提供了一个极简的指令: return <value>;在收到客户端连接后,立即将 <value> 写回并关闭连接。<value> 支持内嵌文本和内置变量(如 $time_iso8601、$remote_addr 等)&a…...
STM32+rt-thread判断是否联网
一、根据NETDEV_FLAG_INTERNET_UP位判断 static bool is_conncected(void) {struct netdev *dev RT_NULL;dev netdev_get_first_by_flags(NETDEV_FLAG_INTERNET_UP);if (dev RT_NULL){printf("wait netdev internet up...");return false;}else{printf("loc…...
【第二十一章 SDIO接口(SDIO)】
第二十一章 SDIO接口 目录 第二十一章 SDIO接口(SDIO) 1 SDIO 主要功能 2 SDIO 总线拓扑 3 SDIO 功能描述 3.1 SDIO 适配器 3.2 SDIOAHB 接口 4 卡功能描述 4.1 卡识别模式 4.2 卡复位 4.3 操作电压范围确认 4.4 卡识别过程 4.5 写数据块 4.6 读数据块 4.7 数据流…...
华为OD机试-食堂供餐-二分法
import java.util.Arrays; import java.util.Scanner;public class DemoTest3 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseint a in.nextIn…...
深度学习水论文:mamba+图像增强
🧀当前视觉领域对高效长序列建模需求激增,对Mamba图像增强这方向的研究自然也逐渐火热。原因在于其高效长程建模,以及动态计算优势,在图像质量提升和细节恢复方面有难以替代的作用。 🧀因此短时间内,就有不…...
探索Selenium:自动化测试的神奇钥匙
目录 一、Selenium 是什么1.1 定义与概念1.2 发展历程1.3 功能概述 二、Selenium 工作原理剖析2.1 架构组成2.2 工作流程2.3 通信机制 三、Selenium 的优势3.1 跨浏览器与平台支持3.2 丰富的语言支持3.3 强大的社区支持 四、Selenium 的应用场景4.1 Web 应用自动化测试4.2 数据…...
python爬虫——气象数据爬取
一、导入库与全局配置 python 运行 import json import datetime import time import requests from sqlalchemy import create_engine import csv import pandas as pd作用: 引入数据解析、网络请求、时间处理、数据库操作等所需库。requests:发送 …...
深入理解Optional:处理空指针异常
1. 使用Optional处理可能为空的集合 在Java开发中,集合判空是一个常见但容易出错的场景。传统方式虽然可行,但存在一些潜在问题: // 传统判空方式 if (!CollectionUtils.isEmpty(userInfoList)) {for (UserInfo userInfo : userInfoList) {…...
webpack面试题
面试题:webpack介绍和简单使用 一、webpack(模块化打包工具)1. webpack是把项目当作一个整体,通过给定的一个主文件,webpack将从这个主文件开始找到你项目当中的所有依赖文件,使用loaders来处理它们&#x…...
Xcode 16 集成 cocoapods 报错
基于 Xcode 16 新建工程项目,集成 cocoapods 执行 pod init 报错 ### Error RuntimeError - PBXGroup attempted to initialize an object with unknown ISA PBXFileSystemSynchronizedRootGroup from attributes: {"isa">"PBXFileSystemSynchro…...
