全面解析 C++ STL 中的 set 和 map

news2025/1/14 4:54:29

C++ 标准模板库(STL)中的关联式容器以其强大的功能和高效性成为开发者解决复杂数据组织问题的重要工具。其中,setmap 是最常用的两类关联容器。本篇博客将从基本特性、底层实现、用法详解、高级案例以及性能优化等多个角度,详细解读它们的设计与使用。


目录

1. 什么是关联式容器

关联式容器的核心特性

2. set 容器详解

2.1 基本概念与特性

2.2 底层实现:红黑树

红黑树的特性

红黑树的操作

2.3 构造函数

2.4 常用操作与复杂度分析

插入操作

查找操作

删除操作

遍历

2.5 特殊操作与技巧

(1) 自定义排序规则

(2) 范围删除

(3) 应用:求两个数组的交集

2.6 multiset 的区别与应用



1. 什么是关联式容器

关联式容器是一类根据关键字组织和管理数据的容器。与序列式容器(如 vectorlist)相比,关联式容器的主要区别如下:

特性关联式容器(set/map序列式容器(vector/list
数据存储顺序按关键字排序按插入顺序
数据访问复杂度O(log⁡N)O(\log N)O(logN)O(1)O(1)O(1) 或 O(N)O(N)O(N)
是否支持随机访问
是否支持按索引访问

关联式容器分为有序和无序两类:

  1. 有序容器:如 setmap,基于平衡二叉树(红黑树)实现,数据按排序规则组织。
  2. 无序容器:如 unordered_setunordered_map,基于哈希表实现,提供更高效的查找速度,但不保证元素顺序。

关联式容器的核心特性

  • 键值对:关联式容器通过关键字对元素进行组织,set 中的关键字即为数据本身,而 map 则以键值对形式存储数据。
  • 自动排序:有序容器会自动对数据进行排序(升序或自定义规则)。
  • 高效操作:插入、删除、查找的平均时间复杂度为 O(log⁡N)O(\log N)O(logN)(红黑树实现)。

2. set 容器详解

2.1 基本概念与特性

set 是一种集合数据结构,用于存储唯一且自动排序的元素。它的主要特点如下:

  • 数据唯一性:同一元素不能重复插入。
  • 自动排序:默认按升序排序,可通过自定义比较器更改排序规则。
  • 迭代器类型set 支持双向迭代器,不支持随机访问。
  • 底层实现:使用红黑树作为存储结构。

2.2 底层实现:红黑树

红黑树的特性

红黑树是一种平衡二叉搜索树,满足以下性质:

  1. 每个节点是红色或黑色。
  2. 根节点是黑色。
  3. 每个叶子节点(nullptr 或 NIL 节点)是黑色。
  4. 如果一个节点是红色,则其子节点必须是黑色(即红色节点不能相邻)。
  5. 从任意节点到其每个叶子节点的路径都包含相同数量的黑色节点。
红黑树的操作
  • 插入:通过旋转和重新着色,确保平衡性和红黑性质。
  • 删除:比插入更复杂,同样通过旋转和着色维护树的性质。
  • 查找:沿树遍历,时间复杂度为 O(log⁡N)O(\log N)O(logN)。

setmap 中,红黑树用来高效实现元素的有序存储和快速查找。


2.3 构造函数

set 提供以下几种构造方式:

  1. 默认构造:创建一个空集合。
    set<int> s;
    
  2. 初始化列表构造:直接用 {} 初始化集合。
    set<int> s = {3, 1, 4, 1, 5, 9};  // 重复元素自动去重
    
  3. 迭代器区间构造:从其他容器的元素构造集合。
    vector<int> v = {1, 2, 3, 4};
    set<int> s(v.begin(), v.end());
    
  4. 自定义比较规则
    set<int, greater<int>> s = {3, 1, 4};  // 按降序排序
    


2.4 常用操作与复杂度分析

操作函数复杂度说明
插入insert(value)O(log⁡N)O(\log N)O(logN)插入元素,若已存在则插入失败
删除erase(value)O(log⁡N)O(\log N)O(logN)删除指定元素
查找find(value)O(log⁡N)O(\log N)O(logN)返回迭代器,指向目标元素
统计count(value)O(log⁡N)O(\log N)O(logN)判断元素是否存在,结果为 0 或 1
遍历begin(), end()O(N)O(N)O(N)正向迭代访问所有元素
下界/上界lower_bound()/upper_bound()O(log⁡N)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 的区别与应用

multisetset 的区别在于:

  1. multiset 允许存储重复元素。
  2. 插入、删除和查找操作的接口与 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(log⁡N)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(log⁡N)O(\log N)O(logN)。


3.2 构造与初始化

map 提供了多种构造方法,以适应不同的使用场景:

  1. 默认构造:创建一个空 map

    map<int, string> m;
    
  2. 初始化列表构造:通过初始化列表直接创建 map

    map<int, string> m = {{1, "apple"}, {2, "banana"}, {3, "cherry"}};
    
  3. 范围构造:从另一个容器(如 setvector 等)构造 map

    vector<pair<int, string>> v = {{1, "apple"}, {2, "banana"}};
    map<int, string> m(v.begin(), v.end());
    
  4. 自定义比较器:通过传入自定义比较器,指定键的排序方式。

    map<int, string, greater<int>> m;  // 降序排序
    m[2] = "banana";
    m[1] = "apple";
    

3.3 常用操作与复杂度分析

操作函数复杂度说明
插入insert(pair)O(log⁡N)O(\log N)O(logN)插入一个键值对,若已存在则插入失败
插入或修改operator[]O(log⁡N)O(\log N)O(logN)插入新元素或修改已有元素的值
查找find(key)O(log⁡N)O(\log N)O(logN)查找指定键,返回键值对的迭代器
统计count(key)O(log⁡N)O(\log N)O(logN)查找指定键是否存在(map 中为 0 或 1)
删除erase(key)O(log⁡N)O(\log N)O(logN)删除指定键及其对应的值
遍历begin(), end()O(N)O(N)O(N)正向遍历所有元素
下界/上界lower_bound(key)/upper_bound(key)O(log⁡N)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 提供了多种遍历方法:

  1. 范围 for

    for (const auto& [key, value] : m) {
        cout << key << ": " << value << endl;
    }
    
  2. 传统迭代器

    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) 自定义排序规则

如同 setmap 也可以通过自定义比较器来实现不同的排序规则。

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 的区别与应用

multimapmap 的扩展,允许相同的键有多个值(即支持键的冗余)。与 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. 高级案例:综合利用 setmap

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_mapunordered_set

在很多查找密集型的应用中,unordered_mapunordered_set 基于哈希表实现,提供常数时间复杂度 O(1)O(1)O(1) 的查找和插入操作。它们的性能优势适用于不需要保持元素顺序的场景。

5.2 避免不必要的拷贝

当插入大量数据时,可以使用 emplace() 来避免不必要的对象拷贝。emplace() 可以直接构造元素,而无需创建临时对象。

map<int, string> m;
m.emplace(1, "apple");  // 不会发生拷贝

5.3 避免频繁修改键

map 不支持修改键,修改键会导致数据结构破坏。因此,避免频繁修改键,而应使用新的键值对进行插入和删除。


6. 总结

通过本文的详细解析,我们全面了解了 C++ 中 setmap 容器的使用、底层实现以及高效操作技巧。掌握这些基本知识后,开发者可以灵活使用 setmap 来处理各种复杂的关联数据问题,从而提高程序的效率和可读性。

在实际开发中,选择合适的容器(如 mapunordered_mapsetunordered_set)可以帮助我们应对不同的数据处理需求,避免性能瓶颈。希望通过本文的学习,你能够深入掌握这些强大的容器,提升 C++ 编程技能。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2252257.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python语法1

Python语法1 作者&#xff1a;王珂 邮箱&#xff1a;49186456qq.com 文章目录 Python语法1[TOC] 前言一、环境搭建1.1 安装Python解释器1.2 安装第三方包1.3 安装Pycharm1.4 虚拟环境 二、Python语法2.1 基础语法2.1.1 注释2.1.2 变量2.1.3 数据类型2.1.4 关键字和标识符 2.2…

普及组集训--图论最短路径

定义&#xff1a;表示顶点u到顶点v的一条边的权值&#xff08;边权&#xff09; 最短路径算法有常见的四种&#xff1a;floyd&#xff0c;dijkstra&#xff0c;Bellman-Ford&#xff0c;SPFA 不过Bellman-Ford并不常用&#xff0c;所以本文不提&#xff1b; 重点在于dijkstr…

蓝桥杯第 23 场 小白入门赛

一、前言 好久没打蓝桥杯官网上的比赛了&#xff0c;回来感受一下&#xff0c;这难度区分度还是挺大的 二、题目总览 三、具体题目 3.1 1. 三体时间【算法赛】 思路 额...签到题 我的代码 // Problem: 1. 三体时间【算法赛】 // Contest: Lanqiao - 第 23 场 小白入门赛 …

前缀和篇——繁星斗斗数字交织中,觅得效率明月辉光(1)

前言 在这片无边无际的数字海洋中&#xff0c;如何从中提取出有价值的讯息&#xff0c;成为了计算机科学中的一项重要课题。前缀和算法&#xff0c;作为一种巧妙的技术&#xff0c;恰如其名——通过计算序列中各个元素的前缀和&#xff0c;能够为我们提供一种高效的查询方式&a…

STM32 HAL库 + LM2904运算放大器 + ADC + VDO温度传感器:电路设计及代码实现

本文将详细介绍如何使用STM32F407的HAL库&#xff0c;实现通过单通道ADC采集VDO温度传感器的信号&#xff0c;并通过串口将采集到的温度值打印输出。具体流程包括&#xff1a;通过分压电阻将获得VDO温度传感器的分压电压&#xff0c;再利用运算放大器LM2904对信号进行放大&…

Python酷库之旅-第三方库Pandas(252)

目录 一、用法精讲 1191、pandas.tseries.offsets.BusinessMonthBegin.n属性 1191-1、语法 1191-2、参数 1191-3、功能 1191-4、返回值 1191-5、说明 1191-6、用法 1191-6-1、数据准备 1191-6-2、代码示例 1191-6-3、结果输出 1192、pandas.tseries.offsets.Busine…

【Linux服务器】CPU问题排查

概述 总体排查思路 总体观察&#xff0c;htop命令观察系统负载程度 检查CPU占用高的进程检查CPU硬件信息是否为问题根源从宏观到细节分析系统IO情况最后检查系统日志 排查思路 系统负载 通过htop命令查看系统负载&#xff0c;通过系统负载可以判断系统是否繁忙&#xff0c;主…

详解MyBatis之篇一

目录 MyBatis 定义 使用MyBatis操作数据库 创建项目 配置 演示 UserInfo.java UserInfoMapper UserInfoMapperTest 数据准备 自动生成测试类 运行结果 MyBatis 定义 MyBatis 是一个优秀的持久层框架&#xff0c;它支持定制化 SQL、存储过程以及高级映射。MyBatis 避…

编译器优化技术

方法内联 逃逸分析 公共子表达式消除 数据边界检查消除

汽车IVI中控开发入门及进阶(三十五):架构QML App Architecture Best Practices

在Qt/QML工程的架构中,架构很重要,虽然本身它有分层,比如QML调用资源文件(图片等)显示GUI界面,后面的CPP文件实现界面逻辑,但是这个分类还有点粗。在实际开发中,界面逻辑也就是基于类cpp的实现,也开始使用各种面向对象的设计模式,实现更加优秀的开发架构,这点尤其在…

本地Docker部署个人在线音乐平台Melody结合内网穿透远程访问听音乐

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

异常--C++

文章目录 一、异常的概念及使用1、异常的概念2、异常的抛出和捕获3、栈展开4、查找匹配的处理代码5、异常重新抛出6、异常安全问题7、异常规范 二、标准库的异常 一、异常的概念及使用 1、异常的概念 异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并…

字符串p型编码

字符串p型编码 C 语言实现C 实现Java 实现Python 实现 &#x1f490;The Begin&#x1f490;点点关注&#xff0c;收藏不迷路&#x1f490; 给定一个完全由数字字符&#xff08;‘0’,‘1’,‘2’,…,‘9’&#xff09;构成的字符串str&#xff0c;请写出str的p型编码串。例如&…

UIlicious - 自动化端到端测试

在现代软件开发中&#xff0c;测试自动化已然成为产品交付质量的基石。而端到端测试&#xff08;E2E&#xff09;&#xff0c;作为验证整个应用流畅运行的关键&#xff0c;常常是测试工作中最具挑战性的一环。这时&#xff0c;一款简单高效的自动化测试工具——UIlicious&#…

机器学习:机器学习项目的完整周期

建立一个有价值的机器学习系统时&#xff0c;需要考虑和计划哪些步骤&#xff1f; 以语音识别为例演示机器学习项目的全周期&#xff1a;机器学习项目的第一步是对项目进行范围划分&#xff0c;即决定什么是项目和你想做什么&#xff0c;然后是收集数据&#xff0c;所以决定需…

浪潮X86服务器NF5280、8480、5468、5270使用inter VROC Raid key给NVME磁盘做阵列

Inter VROC技术简介 Intel Virtual RAID on CPU (Intel VROC) 简单来说就是用CPU的PCIE通道给NVME硬盘做Raid 更多信息可以访问官方支持页面 Raid Key 授权&#xff0c;即VROC SKU 授权主要有用的有2个标准和高级&#xff0c;仅Raid1的授权我暂时没见过。 标准 VROCSTANMOD …

ROS基本框架2——在ROS开发中创建并使用自定义消息(C++版本)

ROS基本框架2——在ROS开发中创建并使用自定义消息(C++版本) code review! 参考笔记 1.ROS基本框架1——编写简单的发布者和订阅者(C++和Python版本) 2.ROS基本框架2——在ROS开发中创建并使用自定义消息(C++版本) 文章目录 ROS基本框架2——在ROS开发中创建并使用自定义…

鸿蒙征文|鸿蒙技术分享:使用到的开发框架和技术概览

目录 每日一句正能量前言正文1. 开发环境搭建关键技术&#xff1a;2. 用户界面开发关键技术&#xff1a;3. 应用逻辑开发关键技术&#xff1a;4. 应用测试关键技术&#xff1a;5. 应用签名和打包关键技术&#xff1a;6. 上架流程关键技术&#xff1a;7. 后续维护和更新关键技术…

(长期更新)《零基础入门 ArcGIS(ArcMap) 》实验二----网络分析(超超超详细!!!)

相信实验一大家已经完成了&#xff0c;对Arcgis已进一步熟悉了&#xff0c;现在开启第二个实验 ArcMap实验--网络分析 目录 ArcMap实验--网络分析 1.1 网络分析介绍 1.2 实验内容及目的 1.2.1 实验内容 1.2.2 实验目的 2.2 实验方案 2.3 实验流程 2.3.1 实验准备 2.3.2 空间校正…

go语言 Pool实现资源池管理数据库连接资源或其他常用需要共享的资源

go Pool Pool用于展示如何使用有缓冲的通道实现资源池&#xff0c;来管理可以在任意数量的goroutine之间共享及独立使用的资源。这种模式在需要共享一组静态资源的情况&#xff08;如共享数据库连接或者内存缓冲区&#xff09;下非 常有用。如果goroutine需要从池里得到这些资…