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

C++ STL set 系列深度解析:从底层原理、核心接口到实战场景

🔥小叶-duck:个人主页

❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
《算法题讲解指南》--优选算法
《算法题讲解指南》--递归、搜索与回溯算法
《算法题讲解指南》--动态规划算法

未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

前言

一、容器分类:序列式容器与关联式容器的本质区别

二、set 系列核心原理:红黑树赋能的高效特性

三、set 核心接口实战:基于实操代码详解

1、初始化与插入:去重 + 自动排序

2、查找与删除:精准操作单个元素

3、区间操作:lower_bound 与 upper_bound

四、multiset:支持重复 key 的关联式容器

五、 set 系列的实战价值:解决实际开发问题

1、环形链表 II

题目链接:

C++算法代码:

2、两个数组的交集(扩展差集思路)

题目链接:

C++算法代码:

结束语


前言

在 C++ STL 容器中,set 系列(set 与 multiset)是基于红黑树实现的关联式容器,核心特性是“自动排序 + 高效查找”。它既能解决“数据去重 + 排序”的基础需求,也能在复杂场景中(如大量数据的查找、区间删除、频率统计)提供O(log N)级别的高效操作效率。本文将结合实战测试代码,从 set 的核心概念入手,讲解其构造、增删查、区间操作等关键接口,同时对比 set 与 multiset 的差异,帮你彻底掌握这一高频使用容器。

一、容器分类:序列式容器与关联式容器的本质区别

STL 容器的设计围绕 “数据如何存储与访问” 展开,序列式与关联式容器的核心差异体现在存储逻辑与访问方式上,具体对比如下:

特性序列式容器(如 vector、list)关联式容器(如 set、map)
存储逻辑按插入顺序存储,元素位置由插入时机决定按键(key)的内在规则存储(如排序规则)
访问方式通过下标/迭代器位置访问(如 vec[2] )通过键值匹配访问(如 set.find(3) )
底层结构动态数组(vector)、双向链表(list)等平衡二叉搜索树(红黑树,set/map)、哈希表(unordered_set)
核心优势插入顺序稳定,适合频繁增删尾部元素,并且可以对已有结构内的数据进行修改(如 vec[1] = 1 )自动排序,查找/删除效率高 ( O(log N) 或O(1) )
典型使用场景存储连续数据、需要按插入顺序遍历、动态扩容需求去重排序、快速查找、键值映射、区间操作

补充说明

  • 序列式容器强调“位置”,元素的价值在于其存储的内容本身;
  • 关联式容器强调“关联关系”,元素的价值在于通过键值 key 快速定位(如 “根据 ID 查找用户信息”);
  • set 作为 “key” 型关联式容器,仅存储键值,核心功能是 “基于 key 的排序与查找”。

二、set 系列核心原理:红黑树赋能的高效特性

set 与 multiset 底层均基于红黑树(一种自平衡二叉搜索树)实现,这一结构赋予它们以下核心特性:

  • 自动排序:红黑树的中序遍历结果为有序序列,因此 set 插入元素后会自动按 key 的默认规则(less升序)排序,无需手动调用排序函数,如果需要按自己的需求比较可以自行实现仿函数传给第二个模板参数。
  • 去重与允许重复:set 不允许存储重复 key(multiset 支持重复 key);
  • 不可修改 key:set 的迭代器为const_iterator,无法通过迭代器修改 key(修改会破坏红黑树结构);
  • 高效操作:增删查操作的时间复杂度均为O(log N),远优于 vector 的O(N)

参考文档:set - C++ Reference

set 的声明

template < class T, // set::key_type/value_type class Compare = less<T>, // set::key_compare/value_compare class Alloc = allocator<T> // set::allocator_type > class set; //set的声明如上,T就是 set 底层关键字的类型 //set默认要求T支持小于比较,如果不支持或者想按自己的需求走可以自行实现仿函数传给第二个模版参数 //set底层存储数据的内存是从空间配置器申请的,如果需要可以自己实现内存池,传给第三个参数。 //一般情况下,我们都不需要传后两个模版参数。 //set底层是用红黑树实现,增删查效率是O(logN),迭代器遍历是走的搜索树的中序,所以是有序的。

set 的构造相关接口

// empty (1) ⽆参默认构造 explicit set (const key_compare& comp = key_compare(), const allocator_type& alloc = allocator_type()); // range (2) 迭代器区间构造 template <class InputIterator> set (InputIterator first, InputIterator last, const key_compare& comp = key_compare(), const allocator_type& = allocator_type()); // copy (3) 拷⻉构造 set (const set& x); // initializer list (5) initializer 列表构造 set (initializer_list<value_type> il, const key_compare& comp = key_compare(), const allocator_type& alloc = allocator_type()); // 迭代器是⼀个双向迭代器 iterator -> a bidirectional iterator to const value_type // 正向迭代器 iterator begin(); iterator end(); // 反向迭代器 reverse_iterator rbegin(); reverse_iterator rend();

set 的构造我们关注以上几个接口即可。
set 的支持正向和反向迭代遍历,遍历默认按升序顺序,因为底层是二叉搜索树,迭代器遍历走的中序;支持迭代器就意味着支持范围 for,set 不管是 iterator 还是 const_iterator 都不支持迭代器修改数据,因为修改关键字数据,破坏了底层搜索树的结构,这是不允许的。

set 的增删查相关接口

Member types key_type -> The first template parameter (T) value_type -> The first template parameter (T) // 单个数据插⼊,如果已经存在则插⼊失败 pair<iterator,bool> insert (const value_type& val); // 列表插⼊,已经在容器中存在的值不会插⼊ void insert (initializer_list<value_type> il); // 迭代器区间插⼊,已经在容器中存在的值不会插⼊ template <class InputIterator> void insert (InputIterator first, InputIterator last); // 查找val,返回val所在的迭代器,没有找到返回end() iterator find (const value_type& val); // 查找val,返回Val的个数 size_type count (const value_type& val) const; // 删除⼀个迭代器位置的值,返回的是下一个位置的迭代器 iterator erase (const_iterator position); // 删除val,val不存在返回0,存在返回1 size_type erase (const value_type& val); // 删除⼀段迭代器区间的值 iterator erase (const_iterator first, const_iterator last); // 返回第一次出现⼤于等于val位置的迭代器 iterator lower_bound (const value_type& val) const; // 返回第一次出现⼤于val位置的迭代器 iterator upper_bound (const value_type& val) const;

set 的增删查关注以上几个接口即可,下面我会为大家一一讲解实现。

三、set 核心接口实战:基于实操代码详解

下面会通过多个测试函数覆盖 set 的核心操作,我们结合代码解析其使用方法与注意事项。

1、初始化与插入:去重 + 自动排序

set 支持多种插入方式,插入后自动去重并按默认按升序排列。

代码示例(注意看注释)

#include <iostream> #include <set> using namespace std; //初始化与插入:去重 + 自动排序 void test_set1() { //测试set的插入与遍历(去重 + 自动排序) set<int> s1; // 方式1:单个元素插入 s1.insert(3); s1.insert(1); s1.insert(2); s1.insert(5); s1.insert(3); s1.insert(5); s1.insert(6); // 遍历方式1:迭代器遍历(注意:*it不可修改) // 遍历结果: 去重+有序 auto it = s1.begin(); while (it != s1.end()) { //*it1 = 1;//不能修改 cout << *it << " "; it++; } cout << endl; // 方式2:初始化列表批量插入 set<int> s2; s2.insert({ 3,1,2,5,3,5,6 }); // 遍历方式2:范围for循环 for (auto& e : s2) { cout << e << " "; } cout << endl; //若需降序排序,可指定排序仿函数:set<int, greater<int>> s -> 降序排序 set<int, greater<int>> s3({ 3,1,2,5,3,5,6 }); //列表构造 for (auto& e : s3) { cout << e << " "; } cout << endl; } int main() { test_set1(); }

2、查找与删除:精准操作单个元素

set 提供 find(查找)、count(统计)、erase(删除)接口,支持按 key 或迭代器操作:

// 测试set的查找与删除 void test_set2() { set<int> s1; s1.insert({ 3,1,2,5,3,5,6 });// 去重后:1 2 3 5 6 // 删除最小值 s1.erase(s1.begin());//iterator erase (const_iterator position); for (auto& e : s1) { cout << e << " "; } cout << endl; // 直接删除x int x; cin >> x; int num = s1.erase(x); //删掉了几个返回值就是几,在set里就是1,没删掉就是0 //size_type erase(const value_type & val); if (num == 0) { cout << x << "不存在!" << endl; } else { cout << x << "删除成功!" << endl; } for (auto e : s1) { cout << e << " "; } cout << endl; cout << endl; // 直接查找在利用返回值是迭代器删除x set<int> s2({ 3,1,2,5,3,5,6 }); for (auto e : s2) { cout << e << " "; } cout << endl; cin >> x; auto pos = s2.find(x); if (pos != s2.end()) //如果返回的结果是end()则说明没有找到 { s2.erase(pos); //cout << *pos << endl; //删除后该位置的迭代器就失效了,不能再进行访问! } else { cout << x << "不存在!" << endl; } for (auto e : s2) { cout << e << " "; } cout << endl; cout << endl; // 算法库的查找 O(N) auto pos1 = find(s2.begin(), s2.end(), x); // set⾃⾝实现的查找 O(logN) auto pos2 = s2.find(x); // 利用count间接实现快速查找 cin >> x; if (s2.count(x)) { cout << x << "在!" << endl; } else { cout << x << "不在!" << endl; } } int main() { test_set2(); }

关键说明

  • erase 的返回值是删除元素的个数,在 set 里要么是0要么是1,multiset 删了几个就是几
  • count 在 set 中主要用于判断元素是否存在,在 multiset 中返回实际个数。

3、区间操作:lower_bound 与 upper_bound

set 的区间操作依赖 lower_bound 和 upper_bound,可用于快速定位边界,并且结合 erase 可高效删除区间元素:

// 测试set的区间操作 void test_set3() { set<int> s; s.insert({ 3,1,2,5,3,5,6,7,9 }); for (auto& e : s) { cout << e << " "; } cout << endl; // 需求:删除[3, 8]区间的元素(即 >=3 且 <=8 ) // lower_bound(val):返回第一个 >= val 的迭代器(此处指向3) auto it1 = s.lower_bound(3); // upper_bound(val):返回第一个 > val 的迭代器(此处指向9) auto it2 = s.upper_bound(8); // 按迭代器区间删除:删除[it1, it2)内的元素(左闭右开) s.erase(it1, it2); //所以迭代器it2位置不会删除而是删除到左边一个位置结束 for (auto& e : s) { cout << e << " "; } cout << endl; } int main() { test_set3(); }

关键说明

  • lower_bound 与 upper_bound 的时间复杂度均为O(log N),是区间操作的核心;
  • 区间 [it1, it2) 为 “左闭右开”,这样符合 STL 迭代器区间的通用设计(如 [begin, end) )。

四、multiset:支持重复 key 的关联式容器

multiset 与 set 接口一致,核心差异是允许重复 key,适用于需要存储相同元素并统计频率的场景:

// 测试multiset(支持重复key) void test_multiset() { multiset<int> s1; // 插入重复元素(不会去重) s1.insert({ 3,1,2,5,3,5,6,3,3 }); // 1. 遍历:有序并且保留重复元素 auto it = s1.begin(); while (it != s1.end()) { //*it = 1;//不能修改 cout << *it << " "; ++it; } cout << endl; // 相比set不同的是,x可能会存在多个,find查找中序的第一个 int x; cin >> x; auto pos = s1.find(x); //打印所有的x while (pos != s1.end() && *pos == x) { cout << *pos << " "; pos++; } cout << endl; // 相比set不同的是,count会返回x的实际个数 cout << x << ":" << s1.count(x) << "个" << endl; //删除:按 key 删除所有匹配元素 // 相比set不同的是,erase给值时会删除所有的x cout << x << "删除的个数:" << s1.erase(x) << endl;//删掉所有的3,并返回删掉的3的个数 for (auto e : s1) { cout << e << " "; } cout << endl; } int main() { test_multiset(); }

set 与 multiset 的核心差异总结

操作setmultiset
插入重复元素自动去重,重复元素插入失败不去重,保留所有重复元素,插入成功
find(key)返回唯一匹配元素的迭代器,未找到返回end()返回中序遍历中第一个匹配元素的迭代器
count(key)返回 0 或 1(仅用于判断元素是否存在)返回元素在容器中实际出现的次数
erase(key)若元素存在则删除 1 个,不存在则删除 0 个删除容器中所有与key匹配的元素

五、 set 系列的实战价值:解决实际开发问题

set 系列的“自动排序 + 高效查找”特性在算法与工程中应用广泛,以下是两个典型题目

1、环形链表 II

题目链接

142. 环形链表 II - 力扣(LeetCode)

思路:用 set 存储遍历过的节点,若插入节点时发现已存在,该节点即为入口。

C++算法代码

class Solution { public: ListNode *detectCycle(ListNode *head) { //解法一:容器set:判断结点地址是否存在过 set<ListNode*> s; while(head) { if(s.count(head) == 0) { s.insert(head); } else { return head; } head = head->next; } return nullptr; } };

2、两个数组的交集(扩展差集思路)

题目链接

349. 两个数组的交集 - 力扣(LeetCode)

思路:用 set 对两个数组去重 + 排序,再用双指针遍历两个 set,找到共同元素。

C++算法代码

class Solution { public: vector<int> intersection(vector<int>& nums1, vector<int>& nums2) { // 1. 用set对两个数组去重+排序 set<int> s1(nums1.begin(), nums1.end()); set<int> s2(nums2.begin(), nums2.end()); vector<int> ret; auto it1 = s1.begin(); auto it2 = s2.begin(); while(it1 != s1.end() && it2 != s2.end()) { if(*it1 > *it2) { it2++; } else if(*it1 < *it2) { it1++; } else { ret.push_back(*it1); it1++; it2++; } } return ret; } };

差集和交集的实战使用:多端文件同步的逻辑

通过 “差集识别新增 / 缺失文件,交集比对时间戳确定更新方向”,避免全量传输,大幅减少带宽消耗和同步时间,是云存储、多端协作工具(如网盘、协同办公软件)的常见底层逻辑之一。

结束语

到此,C++ STL 中的 set 容器我们就讲解完了。set 系列作为 STL 关联式容器的核心,以红黑树为支撑,实现了自动排序、高效查找与去重(或允许多重复)的能力,是处理 “有序数据管理” 场景的利器。希望对大家学习C++能有所收获!

C++参考文档:
https://legacy.cplusplus.com/reference/
https://zh.cppreference.com/w/cpp
https://en.cppreference.com/w/

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

相关文章:

  • Raft算法在大数据系统中的自动化运维实践
  • FLAC3D 钢筋混凝土梁四点弯破坏过程数值模拟
  • 商用煲仔饭机常见问题解答(2026最新专家版) - 速递信息
  • ComfyUI-Manager启动项管理深度解析:如何解决AI绘画扩展依赖冲突与启动故障
  • 基于深度学习的花朵识别系统演示与介绍(YOLOv12/v11/v8/v5模型+Django+web+训练代码+数据集)
  • 基于多控制策略的车辆路径跟踪仿真研究
  • 金融市场流动性风险度量
  • 从API消费者到贡献者:我在RapidAPI和国内平台(聚合数据/幂简集成)发布与管理API的实战心得
  • Token限制下的ChatGPT高效对话:如何优化Prompt长度与内容(含计算工具推荐)
  • 搞定芯片设计后仿:手把手教你在Linux上为Cadence配置QRC寄生参数提取工具
  • 大数据领域数据中台的元数据管理策略
  • 基于MATLAB的电流跟踪PWM控制三相逆变器系统设计:设计报告与仿真程序
  • 探索风光储微电网并网模型:技术与实践
  • Swift面试必备:10个高频问题解析与实战避坑指南
  • 终极指南:Apollo Save Tool - 简单高效的PS4游戏存档管理解决方案
  • CPFEM晶体塑性孪晶滑移子程序及视频
  • 技术分享】CarSim与Simulink联合仿真,实现超车换道的动态规划路径控制【附视频演示
  • leetcode 1457. Pseudo-Palindromic Paths in a Binary Tree 二叉树中的伪回文路径
  • Hackintool终极指南:从零开始轻松配置完美黑苹果系统
  • Gradle 7.1.1构建Flink项目报错?可能是你的IDEA版本太老了!
  • 从GMT到UTC:时间标准的演进与计算机系统的应用
  • COMSOL 光学 手性 BIC 仿真 光子晶体板中连续域束缚态 BIC 赋予的手性。 包含正...
  • leetcode 困难题 1458. Max Dot Product of Two Subsequences 两个子序列的最大点积
  • 用Go写个命令行AI客户端,到底值不值?
  • 告别Elasticsearch!用SkyWalking 10.0.1 + BanyanDB + Docker搭建新一代链路监控(含IDEA/Java-Jar双启动配置)
  • 基于同步旋转坐标系的高效无位置传感器永磁同步电机控制策略——采用三相电压重构,告别传统电压采集...
  • leetcode 1460. Make Two Arrays Equal by Reversing Subarrays 通过翻转子数组使两个数组相等-耗时100
  • 智能汽车视觉导航(4)——基于动态阈值的赛道中线精准定位
  • 国产电车的意外惊喜,油价将重回9元拯救电车,但无法指望海外
  • 告别普通CardView!用MaterialCardView这5个属性,让你的Android应用卡片颜值飙升