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

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.mzph.cn/web/61594.shtml

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

相关文章

FreeRtos开发之计数信号量

前面介绍过了计数信号量的定义取值只有0与1两种状态的信号量称之为二值信号量 取值大于1的信号量称之为计数信号量 计数信号量的取值也可以为1&#xff0c;但通常大于1&#xff0c;如果取值为1&#xff0c;相当于只有0与1两种状态&#xff0c;用二值信号量即可。 计数信号量应用…

Profinet转EtherNet/IP网关是如何解决西门子S7-1500PLC与AB PLC的通讯问题的

一、 案例背景 在一个工业现场&#xff0c;一端是AB的PLC&#xff0c;IP地址192.168.1.20;另一端西门子是S7-1500系列&#xff0c;IP地址192.168.2.248。AB的PLC内有 B3、N7、F8 三个寄存器文件涉及到通讯&#xff0c;分别对应西门子PLC的M、DB1、DB2三个存储区域。通过捷米特…

【C语言】扫雷游戏(一)

我们先设计一个简单的9*9棋盘并有10个雷的扫雷游戏。 1&#xff0c;可以用数组存放&#xff0c;如果有雷就用1表示&#xff0c;没雷就用0表示。 2&#xff0c;排查(2,5)这个坐标时&#xff0c;我们访问周围的⼀圈8个位置黄色统计周围雷的个数是1。排查(8,6)这个坐标时&#xf…

360笔试题之LINUX和UNIX篇

写出完成以下任务的命令&#xff1a; 1.查看当前所在目录。如若当前在&#xff0c;/home/usr1目录下&#xff0c;查看此目录大小。列出此目录下的所有文件&#xff08;包括隐藏文件&#xff09;。 参考答案&#xff1a; 您可以使用以下命令查看当前所在目录和目录大小&#x…

Unity开发FPS游戏之完结篇

这个系列的前几篇文章介绍了如何从头开始用Unity开发一个FPS游戏&#xff0c;感兴趣的朋友可以回顾一下。这个系列的文章如下&#xff1a; Unity开发一个FPS游戏_unity 模仿开发fps 游戏-CSDN博客 Unity开发一个FPS游戏之二_unity 模仿开发fps 游戏-CSDN博客 Unity开发一个F…

浅析RPC—基础知识

该文章会简单介绍一下 RPC 相关的基础概念。 什么是RPC&#xff1f; RPC&#xff08;Remote Procedure Call&#xff09; 即远程过程调用&#xff0c;通过名字我们就能看出 RPC 关注的是远程调用而非本地调用。 为什么要 RPC &#xff1f; 因为&#xff0c;两个不同的服务器…

mysql数据库varchar截断问题

用了这么多年mysql数据库&#xff0c;才发现varchar是可以截断的&#xff0c;而且是在我们线上数据库。个人觉得dba的这个设置是非常有问题的&#xff0c;用户往数据库里存东西&#xff0c;就是为了以后用的&#xff0c;截断了存放&#xff0c;数据不完整&#xff0c;就用不了了…

数据处理与统计分析——07-Pandas的concat连接、merge()合并、多表查询、内/外/自连接查询操作

pandas数据拼接 (1) DataFrame数据组合-concat连接 概述 连接是指把某行或某列追加到数据中, 数据被分成了多份可以使用连接把数据拼接起来把计算的结果追加到现有数据集&#xff0c;也可以使用连接 df对象与df对象拼接 行拼接参考: 列名, 列拼接参考: 行号 # todo 记忆: con…

EwoMail邮箱服务器软件安装教程

EwoMail是基于Linux的开源邮件服务器软件,集成了众多优秀稳定的组件,是一个快速部署、简单高效、多语言、安全稳定的邮件解决方案,帮助你提升运维效率,降低 IT 成本,兼容主流的邮件客户端,同时支持电脑和手机邮件客户端。 一、系统版本 二、关闭selinux vi /etc/sysconf…

【机器学习】机器学习的基本分类-监督学习-支持向量机(Support Vector Machine, SVM)

支持向量机是一种强大的监督学习算法&#xff0c;主要用于分类问题&#xff0c;但也可以用于回归和异常检测。SVM 的核心思想是通过最大化分类边界的方式找到数据的最佳分离超平面。 1. 核心思想 目标 给定训练数据 &#xff0c;其中 是特征向量&#xff0c; 是标签&#xf…

Linux命令进阶·如何切换root以及回退、sudo命令、用户/用户组管理,以及解决创建用户不显示问题和Ubuntu不显示用户名只显示“$“符号问题

目录 1. root用户&#xff08;超级管理员&#xff09; 1.1 用于账户切换的系统命令——su 1.2 退回上一个用户命令——exit 1.3 普通命令临时授权root身份执行——sudo 1.3.1 为普通用户配置sudo认证 2. 用户/用户组管理 2.1 用户组管理 2.2 用户管理 2.2.1 …

Zero to JupyterHub with Kubernetes中篇 - Kubernetes 常规使用记录

前言&#xff1a;纯个人记录使用。 搭建 Zero to JupyterHub with Kubernetes 上篇 - Kubernetes 离线二进制部署。搭建 Zero to JupyterHub with Kubernetes 中篇 - Kubernetes 常规使用记录。搭建 Zero to JupyterHub with Kubernetes 下篇 - Jupyterhub on k8s。 参考&…

《Python基础》之Python中可以转换成json数据类型的数据

目录 一、JSON简介 JSON有两种基本结构 1、对象&#xff08;Object&#xff09; 2、数组&#xff08;Array&#xff09; 二、将数据装换成json数据类型方法 三、在Python中&#xff0c;以下数据类型可以直接转换为JSON数据类型 1、字典&#xff08;Dictionary&#xff09…

若依项目源码阅读

源码阅读 前端代码分析 代码生成器生成的前端代码有两个&#xff0c;分别是course.js用于向后端发送ajax请求的接口代码&#xff0c;另一个是index.vue&#xff0c;用于在浏览器展示课程管理的视图组件。前端的代码是基于vue3elementplus。 template用于展示前端组件别的标签…

C#tabcontrol如何指定某个tabItem为默认页

// Selects tabPage2 using SelectedTab.this.tabControl1.SelectedTab tabPage2; 参考链接 TabControl.SelectedTab 属性 (System.Windows.Forms) | Microsoft Learnhttps://learn.microsoft.com/zh-cn/dotnet/api/system.windows.forms.tabcontrol.selectedtab?viewnetfr…

速盾:高防 CDN 可以配置客户端请求超时配置?

在高防 CDN&#xff08;Content Delivery Network&#xff0c;内容分发网络&#xff09;的运行管理中&#xff0c;客户端请求超时配置是一项重要的功能设定&#xff0c;它对于优化网络资源分配、保障服务质量以及维护系统稳定性有着关键意义。 一、客户端请求超时配置的概念 …

文件比较和文件流

文件比较和文件流 一、文本比较工具 diff1.基本用法1.1输出格式 2.常用选项 二、文件流1.文件的打开模式2.文件流的分类ifstreamofstreamfstrem区别 3.文件流的函数1. 构造函数2. is_open 用于判断文件是否打开3. open4. getline5. close6. get()7. read8. write9. put10. gcou…

【网络篇】HTTP知识

键入网址到网页显示&#xff0c;期间发生了什么&#xff1f; 浏览器第一步是解析URL&#xff0c;这样就得到了服务器名称和文件的路径名&#xff0c;然后根据这些信息生成http请求&#xff0c;通过DNS查询得到我们要请求的服务器地址&#xff0c;然后添加TCP头、IP头以及MAC头&…

【解决安全扫描漏洞】---- 检测到目标站点存在 JavaScript 框架库漏洞

1. 漏洞结果 JavaScript 框架或库是一组能轻松生成跨浏览器兼容的 JavaScript 代码的工具和函数。如果网站使用了存在漏洞的 JavaScript 框架或库&#xff0c;攻击者就可以利用此漏洞来劫持用户浏览器&#xff0c;进行挂马、XSS、Cookie劫持等攻击。 1.1 漏洞扫描截图 1.2 具体…

互联网基础

TCP/IP协议&#xff08;协议组&#xff09; 分层名称TCP/IP协议应用层HTTP,FTP,mDNS,WebSocket,OSC...传输层TCP&#xff0c;UDP网络层IP链路层&#xff08;网络接口层&#xff09;Ethernet&#xff0c;Wi-Fi... 链路层&#xff08;网络接口层&#xff09; 链路层的主要作用…