C++进阶:(四)set系列容器的全面指南
目录
前言
一、容器分类核心:序列式容器与关联式容器的本质区别
1.1 序列式容器:按存储位置有序访问
1.2 关联式容器:按关键字有序访问
二、set 系列容器底层实现与核心特性
2.1 红黑树:set 系列的底层基石
2.2 set 容器的核心特性
2.3 multiset 容器的核心特性
三、set 系列容器核心接口详解与实战示例
3.1 构造函数与初始化
3.1.1 构造接口
3.1.2 示例:多种初始化方式
3.2 迭代器与遍历
3.2.1 迭代器接口
3.2.2 示例:多种遍历方式
3.3 插入操作(insert)
3.3.1 插入接口
3.3.2 接口细节说明
3.3.3 示例:插入操作
3.4 查找操作(find/count/lower_bound/upper_bound)
3.4.1 查找接口
3.4.2 接口差异说明(set vs multiset)
3.4.3 示例:查找操作
3.5 删除操作(erase)
3.5.1 删除接口
3.5.2 接口差异说明(set vs multiset)
3.5.3 示例:删除操作
四、set 与 multiset 的核心差异总结
五、set 系列容器的实战场景与 LeetCode 例题解析
5.1 场景一:数组交集(去重 + 有序特性)
5.2 场景二:环形链表检测(快速查找特性)
5.3 场景三:统计元素出现次数(multiset 的冗余特性)
六、set 系列容器的使用注意事项与性能优化
6.1 使用注意事项
6.2 性能优化技巧
总结
前言
在 C++ 编程中,STL(Standard Template Library)容器是提升开发效率的核心工具之一。容器作为数据存储的载体,根据底层实现和逻辑结构的不同,主要分为序列式容器和关联式容器两大类。其中 set 系列作为关联式容器的重要成员,凭借其有序性、去重特性和高效的增删查操作,在实际开发中应用广泛。本文将从容器分类的本质区别入手,深入剖析序列式容器与关联式容器的核心特性,再全面详解 set 系列(set、multiset)的底层实现、接口使用、核心差异及实战场景。下面就让我们正式开始吧!
一、容器分类核心:序列式容器与关联式容器的本质区别
STL 容器的分类并非随意划分,而是基于数据的逻辑结构、存储方式和访问规则的本质差异。理解这两类容器的核心区别,是后续灵活选择容器的关键。
1.1 序列式容器:按存储位置有序访问
序列式容器是我们接触最早的 STL 容器类型,常见的包括 string、vector、list、deque、array、forward_list 等。其核心特征是围绕 “线性序列” 和 “位置依赖” 展开的:
- 逻辑结构:数据以线性序列的形式组织,每个元素的位置由其插入顺序决定,元素之间仅存在 “前后相邻” 的关系,没有额外的关联约束。
- 存储与访问规则:元素按插入时的存储位置顺序保存和访问,访问方式依赖于索引(如 vector、array)或迭代器遍历(如 list、forward_list)。
- 核心特性:元素的 “位置” 是其唯一的标识,交换任意两个元素的位置后,容器的逻辑结构不会被破坏,仅元素的顺序发生改变。
- 效率特点:
- 随机访问效率:vector、array 支持 O (1) 时间复杂度的随机访问,forward_list、list 不支持随机访问。
- 增删效率:vector 在尾部增删效率 O (1),中间插入删除效率 O (N);list 在任意位置增删效率 O (1),但需遍历找到目标位置。
1.2 关联式容器:按关键字有序访问
关联式容器是 STL 中用于高效查找场景的容器,主要包括 map/set 系列和 unordered_map/unordered_set 系列。其核心特征围绕 “关键字关联” 和 “有序性” 展开:
- 逻辑结构:底层通常基于非线性结构(如红黑树)实现,元素之间通过 “关键字” 建立紧密关联,而非依赖存储位置。
- 存储与访问规则:元素按关键字的大小关系有序存储,访问时无需依赖位置索引,而是通过关键字直接查找。
- 核心特性:关键字是元素的核心标识,交换两个元素的位置会破坏底层非线性结构的有序性,导致容器功能异常。
- 效率特点:增删查操作的时间复杂度均为 O (log N),源于底层红黑树的平衡特性,确保查找路径长度稳定。
- 两大系列差异:
- map/set 系列:底层基于红黑树实现,元素按关键字有序排列,支持范围查找(如 lower_bound、upper_bound)。
- unordered_map/unordered_set 系列:底层基于哈希表实现,元素无序排列,增删查平均效率 O (1),但最坏情况 O (N)。
二、set 系列容器底层实现与核心特性
set 系列容器是关联式容器中专注于 “关键字查找” 场景的实现,包括 set 和 multiset 两个核心成员。它们的底层均基于红黑树(平衡二叉搜索树)实现,因此具备有序性和高效的增删查能力,核心差异仅在于是否支持关键字冗余。
在这为大家提供了set和multiset的参考文档:https://legacy.cplusplus.com/reference/set/
2.1 红黑树:set 系列的底层基石
红黑树是一种自平衡的二叉搜索树,其核心特性确保了树的高度始终保持在 O (log N) 级别,从而为 set 系列提供稳定的 O (log N) 时间复杂度操作:
- 红黑树的 5 大规则:
- 每个节点要么是红色,要么是黑色。
- 根节点是黑色。
- 所有叶子节点(NIL 节点)是黑色。
- 如果一个节点是红色,其两个子节点必须是黑色。
- 从任意节点到其所有后代叶子节点的路径上,黑色节点的数量相同。
这些规则保证了红黑树不会出现极端不平衡的情况,每次插入、删除操作后,通过旋转和颜色调整维持平衡,确保查找、插入、删除操作的时间复杂度稳定在 O (log N)。后续还会为大家详细介绍红黑树的底层原理和实现。
2.2 set 容器的核心特性
set 容器的定位是 “有序去重的关键字集合”,其特性完全由底层红黑树和自身设计规则决定:
- 关键字唯一:set 中不允许存在重复的关键字,插入已存在的关键字会失败。
- 有序性:元素按关键字的升序(默认)排列,遍历顺序为红黑树的中序遍历结果。
- 迭代器特性:
- 支持双向迭代器(iterator、reverse_iterator),可正向和反向遍历。
- iterator 和 const_iterator 均为只读迭代器,不允许通过迭代器修改元素(修改会破坏红黑树的有序性)。
- 模板参数:
template <class T, // 关键字类型(同时也是元素类型)class Compare = less<T>, // 比较仿函数(默认升序)class Alloc = allocator<T> // 空间配置器(默认使用STL提供的alloc)> class set;- T:set 的关键字类型,也是容器中存储的元素类型(set 中 key_type 与 value_type 相同);
- Compare:用于定义关键字的比较规则,默认使用 less<T>(升序),可自定义仿函数实现降序或自定义比较逻辑;
- Alloc:空间配置器,负责内存分配与释放,默认情况下无需手动指定。
2.3 multiset 容器的核心特性
multiset 与 set 的底层实现完全一致(均为红黑树),核心差异仅在于支持关键字冗余:
- 关键字可重复:multiset 允许插入多个相同的关键字,容器会保留所有重复元素并维持有序。
- 迭代器特性:与 set 一致,支持双向迭代器,且迭代器只读。
- 模板参数:与 set 完全相同,无需额外配置即可支持关键字冗余。
- 核心限制:由于支持关键字重复,部分接口的行为与 set 存在差异(如 find、count、erase),后续将详细说明。
三、set 系列容器核心接口详解与实战示例
STL 容器的接口设计具有高度一致性,set 系列的接口与 vector、list 等序列式容器有诸多相似之处,但也存在因底层红黑树特性导致的独特接口。本节将重点讲解 set 和 multiset 的核心接口(构造、迭代器、增删查),并结合实战示例说明使用场景。
3.1 构造函数与初始化
set 和 multiset 的构造函数完全一致,支持 4 种常见的初始化方式:
3.1.1 构造接口
// 1. 无参构造:创建空set
explicit set (const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());// 2. 迭代器区间构造:用[first, last)区间的元素初始化
template <class InputIterator>
set (InputIterator first, InputIterator last,const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());// 3. 拷贝构造:用另一个set对象初始化
set (const set& x);// 4. 初始化列表构造:用初始化列表中的元素初始化
set (initializer_list<value_type> il,const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());
3.1.2 示例:多种初始化方式
#include <iostream>
#include <set>
#include <vector>
using namespace std;int main() {// 1. 无参构造set<int> s1;// 2. 初始化列表构造(最常用)set<int> s2 = {4, 2, 7, 2, 8, 5}; // 自动去重,结果:2,4,5,7,8cout << "s2初始化结果:";for (auto e : s2) cout << e << " "; // 输出:2 4 5 7 8cout << endl;// 3. 迭代器区间构造(用vector的元素初始化)vector<int> vec = {9, 3, 6, 3, 1};set<int> s3(vec.begin(), vec.end()); // 去重后:1,3,6,9cout << "s3初始化结果:";for (auto e : s3) cout << e << " "; // 输出:1 3 6 9cout << endl;// 4. 拷贝构造set<int> s4(s3);cout << "s4拷贝构造结果:";for (auto e : s4) cout << e << " "; // 输出:1 3 6 9cout << endl;// 5. 自定义比较规则(降序)set<int, greater<int>> s5 = {4, 2, 7, 2, 8, 5}; // 去重+降序:8,7,5,4,2cout << "s5降序初始化结果:";for (auto e : s5) cout << e << " "; // 输出:8 7 5 4 2cout << endl;return 0;
}
3.2 迭代器与遍历
set 系列支持双向迭代器,遍历方式包括迭代器遍历和范围 for 遍历(C++11 及以上),遍历顺序由比较仿函数决定(默认升序)。
3.2.1 迭代器接口
// 正向迭代器:指向第一个元素,遍历至end()(尾后迭代器)
iterator begin();
const_iterator begin() const;// 正向尾后迭代器:不指向任何元素,作为遍历结束标志
iterator end();
const_iterator end() const;// 反向迭代器:指向最后一个元素,遍历至rend()(反向尾后迭代器)
reverse_iterator rbegin();
const_reverse_iterator rbegin() const;// 反向尾后迭代器:作为反向遍历结束标志
reverse_iterator rend();
const_reverse_iterator rend() const;
3.2.2 示例:多种遍历方式
#include <iostream>
#include <set>
using namespace std;int main() {set<string> strSet = {"sort", "insert", "add", "erase", "find"}; // 按ASCII码升序排列// 1. 正向迭代器遍历cout << "正向迭代器遍历:";set<string>::iterator it = strSet.begin();while (it != strSet.end()) {// *it = "modify"; // 错误:迭代器只读,不允许修改cout << *it << " "; // 输出:add erase find insert sort(ASCII码升序)++it;}cout << endl;// 2. 反向迭代器遍历cout << "反向迭代器遍历:";set<string>::reverse_iterator rit = strSet.rbegin();while (rit != strSet.rend()) {cout << *rit << " "; // 输出:sort insert find erase add(反向升序=降序)++rit;}cout << endl;// 3. 范围for遍历(最简洁,推荐使用)cout << "范围for遍历:";for (const auto& e : strSet) { // 用const auto&避免拷贝,提高效率cout << e << " "; // 输出:add erase find insert sort}cout << endl;return 0;
}
注意事项:
- set 的迭代器是只读的,无论使用 iterator 还是 const_iterator,都不能修改元素的值,否则会破坏红黑树的有序性,导致容器行为异常。
- 遍历顺序由比较仿函数决定,默认使用 less<T>,按关键字升序排列;若使用 greater<T>,则按降序排列。
3.3 插入操作(insert)
插入操作是 set 系列的核心接口之一,负责向红黑树中添加元素,同时维持容器的有序性和去重特性(set)或冗余特性(multiset)。
3.3.1 插入接口
// 1. 插入单个元素:返回pair<iterator, bool>
pair<iterator, bool> insert (const value_type& val);// 2. 插入初始化列表中的元素
void insert (initializer_list<value_type> il);// 3. 插入[first, last)区间的元素
template <class InputIterator>
void insert (InputIterator first, InputIterator last);
3.3.2 接口细节说明
- 单个元素插入(set):
- 返回值为
pair<iterator, bool>,其中:- first:指向插入的元素(若插入成功)或已存在的元素(若插入失败)的迭代器。
- second:布尔值,true 表示插入成功(元素不存在),false 表示插入失败(元素已存在)。
- 返回值为
- 单个元素插入(multiset):
- 无返回值(或返回 iterator,C++11 后统一为 iterator),因为支持关键字冗余,插入一定成功。
- 批量插入(初始化列表 / 迭代器区间):
- set 会自动过滤重复元素,仅插入不存在的元素。
- multiset 会插入所有元素,包括重复元素。
3.3.3 示例:插入操作
#include <iostream>
#include <set>
#include <vector>
using namespace std;int main() {// 一、set插入示例(去重)set<int> s1;// 1. 插入单个元素auto ret1 = s1.insert(5);cout << "插入5:" << (ret1.second ? "成功" : "失败") << ",元素位置:" << *ret1.first << endl; // 成功,5auto ret2 = s1.insert(5); // 插入重复元素cout << "插入5:" << (ret2.second ? "成功" : "失败") << ",元素位置:" << *ret2.first << endl; // 失败,5// 2. 插入初始化列表s1.insert({2, 7, 3, 2}); // 过滤重复的2,插入2、7、3cout << "插入列表后s1:";for (auto e : s1) cout << e << " "; // 输出:2 3 5 7cout << endl;// 3. 插入迭代器区间vector<int> vec = {4, 6, 3, 8};s1.insert(vec.begin(), vec.end()); // 过滤重复的3,插入4、6、8cout << "插入vector后s1:";for (auto e : s1) cout << e << " "; // 输出:2 3 4 5 6 7 8cout << endl;// 二、multiset插入示例(允许重复)multiset<int> ms1;// 1. 插入单个元素(重复插入)ms1.insert(5);ms1.insert(5);ms1.insert(5);cout << "multiset插入3个5后:";for (auto e : ms1) cout << e << " "; // 输出:5 5 5cout << endl;// 2. 插入初始化列表(含重复元素)ms1.insert({2, 5, 3, 2}); // 插入所有元素,包括重复的2和5cout << "插入列表后ms1:";for (auto e : ms1) cout << e << " "; // 输出:2 2 3 5 5 5 5cout << endl;return 0;
}
3.4 查找操作(find/count/lower_bound/upper_bound)
查找是关联式容器的核心优势,set 系列提供了多个高效的查找接口,满足不同场景的需求。
3.4.1 查找接口
// 1. 查找关键字val:返回指向val的迭代器,未找到返回end()
iterator find (const value_type& val);
const_iterator find (const value_type& val) const;// 2. 统计关键字val的个数:set返回0或1,multiset返回实际个数
size_type count (const value_type& val) const;// 3. 查找第一个>=val的元素:返回其迭代器
iterator lower_bound (const value_type& val) const;
const_iterator lower_bound (const value_type& val) const;// 4. 查找第一个>val的元素:返回其迭代器
iterator upper_bound (const value_type& val) const;
const_iterator upper_bound (const value_type& val) const;
3.4.2 接口差异说明(set vs multiset)
- find:
- set:若找到,返回唯一对应元素的迭代器;未找到返回 end ()。
- multiset:若存在多个相同元素,返回中序遍历的第一个元素的迭代器。
- count:
- set:仅用于判断元素是否存在(返回 0 或 1),效率与 find 一致(O (log N))。
- multiset:返回元素的实际个数,需遍历所有相同元素,效率 O (log N + k)(k 为元素个数)。
- lower_bound/upper_bound:
- 两者在 set 和 multiset 中行为一致,用于范围查找。
3.4.3 示例:查找操作
#include <iostream>
#include <set>
using namespace std;int main() {// 一、set查找示例set<int> s1 = {2, 3, 4, 5, 6, 7, 8};// 1. find查找auto pos1 = s1.find(5);if (pos1 != s1.end()) {cout << "找到元素5,位置:" << *pos1 << endl; // 输出:5} else {cout << "未找到元素5" << endl;}auto pos2 = s1.find(9);if (pos2 != s1.end()) {cout << "找到元素9" << endl;} else {cout << "未找到元素9" << endl; // 输出}// 2. count统计cout << "元素5的个数:" << s1.count(5) << endl; // 输出:1cout << "元素9的个数:" << s1.count(9) << endl; // 输出:0// 3. lower_bound/upper_bound范围查找auto itLow = s1.lower_bound(3); // 第一个>=3的元素:3auto itUp = s1.upper_bound(6); // 第一个>6的元素:7cout << "范围[3,6]的元素:";for (auto it = itLow; it != itUp; ++it) {cout << *it << " "; // 输出:3 4 5 6}cout << endl;// 二、multiset查找示例multiset<int> ms1 = {2, 2, 3, 5, 5, 5, 5};// 1. find查找(返回第一个匹配元素)auto pos3 = ms1.find(5);if (pos3 != ms1.end()) {cout << "multiset中第一个5的位置:" << *pos3 << endl; // 输出:5// 遍历所有5cout << "multiset中所有5:";while (pos3 != ms1.end() && *pos3 == 5) {cout << *pos3 << " "; // 输出:5 5 5 5++pos3;}cout << endl;}// 2. count统计cout << "multiset中元素5的个数:" << ms1.count(5) << endl; // 输出:4cout << "multiset中元素2的个数:" << ms1.count(2) << endl; // 输出:2return 0;
}
3.5 删除操作(erase)
删除操作用于移除容器中的元素,支持按元素值、迭代器位置或迭代器区间删除,set 和 multiset 的接口一致,但行为存在差异。
3.5.1 删除接口
// 1. 按迭代器位置删除:返回删除元素的下一个元素的迭代器
iterator erase (const_iterator position);// 2. 按元素值删除:返回删除的元素个数(set返回0或1,multiset返回实际删除个数)
size_type erase (const value_type& val);// 3. 按迭代器区间删除:返回删除区间的下一个元素的迭代器
iterator erase (const_iterator first, const_iterator last);
3.5.2 接口差异说明(set vs multiset)
- 按元素值删除:
- set:最多删除 1 个元素(若存在),返回 1;不存在返回 0。
- multiset:删除所有与 val 相等的元素,返回删除的元素个数。
- 按迭代器删除:
- 两者行为一致,仅删除迭代器指向的单个元素,返回下一个元素的迭代器。
- 按区间删除:
- 两者行为一致,删除 [first, last) 区间内的所有元素,返回 last 迭代器。
3.5.3 示例:删除操作
#include <iostream>
#include <set>
using namespace std;int main() {// 一、set删除示例set<int> s1 = {2, 3, 4, 5, 6, 7, 8};// 1. 按迭代器位置删除(删除第一个元素)auto pos1 = s1.begin();s1.erase(pos1);cout << "删除第一个元素后s1:";for (auto e : s1) cout << e << " "; // 输出:3 4 5 6 7 8cout << endl;// 2. 按元素值删除(删除5)size_t num1 = s1.erase(5);cout << "删除元素5:" << (num1 ? "成功" : "失败") << ",删除个数:" << num1 << endl; // 成功,1cout << "删除后s1:";for (auto e : s1) cout << e << " "; // 输出:3 4 6 7 8cout << endl;// 3. 按区间删除(删除[4,6])auto itLow = s1.lower_bound(4);auto itUp = s1.upper_bound(6);s1.erase(itLow, itUp);cout << "删除区间[4,6]后s1:";for (auto e : s1) cout << e << " "; // 输出:3 7 8cout << endl;// 二、multiset删除示例multiset<int> ms1 = {2, 2, 3, 5, 5, 5, 5};// 1. 按元素值删除(删除所有5)size_t num2 = ms1.erase(5);cout << "multiset删除所有5,删除个数:" << num2 << endl; // 输出:4cout << "删除后ms1:";for (auto e : ms1) cout << e << " "; // 输出:2 2 3cout << endl;// 2. 按迭代器位置删除(删除第一个2)auto pos2 = ms1.find(2);if (pos2 != ms1.end()) {ms1.erase(pos2);}cout << "删除第一个2后ms1:";for (auto e : ms1) cout << e << " "; // 输出:2 3cout << endl;return 0;
}
注意事项:
- 删除迭代器时,需确保迭代器有效(不为 end ()),否则会导致未定义行为。
- 按元素值删除时,set 仅删除一个元素,multiset 删除所有相同元素,大家需要根据实际需求进行选择。
四、set 与 multiset 的核心差异总结
set 和 multiset 的底层实现、大部分接口完全一致,但因是否支持关键字冗余,导致部分接口行为和使用场景存在差异。以下是核心差异的详细对比:
| 对比维度 | set | multiset |
|---|---|---|
| 关键字特性 | 关键字唯一,不允许重复 | 关键字可重复,支持冗余 |
| insert 返回值 | pair<iterator, bool>,标识插入成功与否 | iterator(C++11 后),插入必定成功 |
| find 行为 | 返回唯一匹配元素的迭代器,未找到返回 end () | 返回中序遍历的第一个匹配元素的迭代器 |
| count 行为 | 返回 0 或 1,仅用于判断元素是否存在 | 返回匹配元素的实际个数 |
| erase(按值) | 删除最多 1 个元素,返回 0 或 1 | 删除所有匹配元素,返回删除个数 |
| 适用场景 | 去重 + 有序存储、快速查找唯一元素 | 允许重复 + 有序存储、统计元素出现次数 |
- 若需存储唯一元素并快速查找,优先使用 set。
- 若需存储重复元素并维持有序,或需要统计元素出现次数,使用 multiset。
五、set 系列容器的实战场景与 LeetCode 例题解析
set 系列凭借其有序性、去重特性和高效的增删查操作,在实际开发和算法题中有着广泛的应用。本节将结合经典 LeetCode 例题,讲解 set 系列的实战用法。
5.1 场景一:数组交集(去重 + 有序特性)
题目链接:https://leetcode.cn/problems/intersection-of-two-arrays/description/
题目描述:给定两个数组 nums1 和 nums2,返回它们的交集。输出结果中的每个元素一定是唯一的。我们可以不考虑输出结果的顺序。
解题思路:
- 利用 set 的去重特性,将两个数组分别转换为 set,自动过滤重复元素。
- 利用 set 的有序特性,通过双指针遍历两个 set,高效查找共同元素(类似归并排序的合并过程)。
代码实现:
#include <iostream>
#include <vector>
#include <set>
using namespace std;class Solution {
public:vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {// 转换为set,去重并有序set<int> s1(nums1.begin(), nums1.end());set<int> s2(nums2.begin(), nums2.end());vector<int> result;auto it1 = s1.begin();auto it2 = s2.begin();// 双指针遍历,查找交集while (it1 != s1.end() && it2 != s2.end()) {if (*it1 < *it2) {++it1; // s1的元素更小,移动s1指针} else if (*it1 > *it2) {++it2; // s2的元素更小,移动s2指针} else {// 找到交集元素,加入结果集result.push_back(*it1);++it1;++it2;}}return result;}
};// 测试代码
int main() {Solution sol;vector<int> nums1 = {1, 2, 2, 1};vector<int> nums2 = {2, 2};vector<int> res = sol.intersection(nums1, nums2);cout << "交集结果:";for (auto e : res) cout << e << " "; // 输出:2cout << endl;return 0;
}
复杂度分析:
- 时间复杂度:O (m log m + n log n),其中 m 和 n 分别为两个数组的长度。转换为 set 的时间复杂度为 O (m log m + n log n),双指针遍历的时间复杂度为 O (m + n),整体由排序时间主导。
- 空间复杂度:O (m + n),用于存储两个 set。
5.2 场景二:环形链表检测(快速查找特性)
题目链接:https://leetcode.cn/problems/linked-list-cycle-ii/description/
题目描述:给定一个链表的头节点 head,返回链表开始入环的第一个节点。如果链表无环,则返回 null。
解题思路:
- 利用 set 的快速查找特性,遍历链表时将节点指针存入 set。
- 若当前节点已存在于 set 中,说明该节点是环的入口(首次重复出现的节点)。
- 若遍历至链表尾部(null)仍未发现重复节点,则链表无环。
代码实现:
#include <iostream>
#include <set>
using namespace std;// 链表节点定义
struct ListNode {int val;ListNode *next;ListNode(int x) : val(x), next(nullptr) {}
};class Solution {
public:ListNode *detectCycle(ListNode *head) {set<ListNode*> nodeSet;ListNode *cur = head;while (cur != nullptr) {// 尝试插入当前节点,若插入失败(已存在),则为环入口auto ret = nodeSet.insert(cur);if (!ret.second) {return cur;}cur = cur->next;}// 遍历结束未发现环return nullptr;}
};// 测试代码(创建带环链表并检测)
int main() {Solution sol;// 创建链表:1 -> 2 -> 3 -> 4 -> 2(环入口为2)ListNode *head = new ListNode(1);head->next = new ListNode(2);head->next->next = new ListNode(3);head->next->next->next = new ListNode(4);head->next->next->next->next = head->next; // 4指向2,形成环ListNode *cycleEntry = sol.detectCycle(head);if (cycleEntry != nullptr) {cout << "环的入口节点值:" << cycleEntry->val << endl; // 输出:2} else {cout << "链表无环" << endl;}// 释放内存(简化处理)delete head->next->next->next;delete head->next->next;delete head->next;delete head;return 0;
}
5.3 场景三:统计元素出现次数(multiset 的冗余特性)
题目描述:给定一个字符串数组,统计每个字符串出现的次数,并按出现次数降序排列;若次数相同,按字符串字典序升序排列。
解题思路:
- 利用 multiset 的冗余特性,插入所有字符串,自动维持字典序。
- 遍历 multiset,统计每个字符串的出现次数(利用 count 接口)。
- 按出现次数和字典序排序,输出结果。
代码实现:
#include <iostream>
#include <vector>
#include <set>
#include <algorithm>
#include <string>
using namespace std;// 自定义排序规则:先按次数降序,再按字典序升序
struct Compare {bool operator()(const pair<string, int>& a, const pair<string, int>& b) const {if (a.second != b.second) {return a.second > b.second; // 次数降序} else {return a.first < b.first; // 字典序升序}}
};vector<pair<string, int>> countAndSort(vector<string>& words) {// 用multiset存储所有单词,维持字典序multiset<string> wordSet(words.begin(), words.end());vector<pair<string, int>> result;// 遍历multiset,统计每个单词的出现次数auto it = wordSet.begin();while (it != wordSet.end()) {string word = *it;int count = wordSet.count(word); // 统计次数result.emplace_back(word, count);// 跳过当前单词的所有重复项it = wordSet.upper_bound(word);}// 按自定义规则排序sort(result.begin(), result.end(), Compare());return result;
}// 测试代码
int main() {vector<string> words = {"apple", "banana", "apple", "orange", "banana", "apple", "pear"};auto res = countAndSort(words);cout << "单词统计结果(按次数降序、字典序升序):" << endl;for (const auto& p : res) {cout << p.first << ": " << p.second << "次" << endl;}/* 输出:apple: 3次banana: 2次orange: 1次pear: 1次*/return 0;
}
复杂度分析:
- 时间复杂度:O (n log n + m log m),其中 n 为单词总数(multiset 插入时间),m 为不同单词的个数(排序时间)。
- 空间复杂度:O (n),用于存储 multiset 和结果集。
六、set 系列容器的使用注意事项与性能优化
6.1 使用注意事项
- 迭代器只读特性:set 和 multiset 的迭代器是只读的,不能通过迭代器修改元素的值,否则会破坏红黑树的有序性,导致容器行为异常。
- 关键字类型的要求:
- 关键字类型必须支持比较仿函数的操作(默认是 less<T>,即支持 < 运算符)。
- 若自定义关键字类型(如结构体),需重载 < 运算符或自定义比较仿函数,否则会编译报错。
- 插入重复元素的处理:
- set 插入重复元素会失败,需通过 insert 的返回值判断插入结果。
- multiset 插入重复元素会成功,若需去重需手动处理。
- erase 接口的差异:
- 按值删除时,set 仅删除一个元素,multiset 删除所有相同元素,需根据需求选择。
- 按迭代器删除时,需确保迭代器有效(不为 end ()),否则会导致未定义行为。
6.2 性能优化技巧
- 避免频繁插入删除:红黑树的插入删除会涉及旋转和颜色调整,频繁操作会影响性能。若需批量插入,优先使用迭代器区间插入(insert (first, last)),效率高于多次单个插入。
- 合理选择比较仿函数:默认的 less<T>已满足大部分场景,无需自定义。若需降序,直接使用 greater<T>(STL 内置),无需手动实现。
- 使用 const_iterator 和 const 引用:遍历容器时,使用 const_iterator 和 const auto & 可避免不必要的拷贝,提高效率。
- 提前预留空间(无直接接口):set 系列没有 reserve 接口(红黑树的空间分配由节点决定),若已知元素数量,可通过迭代器区间插入减少内存分配次数。
- 优先使用容器自带的 find 接口:STL 算法库的 find 函数(std::find)对 set 系列的查找效率为 O (N),而容器自带的 find 接口效率为 O (log N),需优先使用容器自带接口。
示例:避免使用 std::find,优先使用 set::find
#include <iostream>
#include <set>
#include <algorithm> // std::find
using namespace std;int main() {set<int> s = {2, 3, 4, 5, 6};// 不推荐:std::find,O(N)效率auto it1 = find(s.begin(), s.end(), 4);if (it1 != s.end()) {cout << "std::find找到4" << endl;}// 推荐:set::find,O(log N)效率auto it2 = s.find(4);if (it2 != s.end()) {cout << "set::find找到4" << endl;}return 0;
}
总结
STL 容器是 C++ 编程的核心工具,熟练掌握 set 系列的使用,能大幅提升代码的效率和可读性。在实际开发中,大家应根据数据的特性(是否有序、是否重复、访问频率)选择合适的容器,让工具为需求服务。感谢大家的支持!
