目录
[1.1 序列式容器:按存储位置有序访问](#1.1 序列式容器:按存储位置有序访问)
[1.2 关联式容器:按关键字有序访问](#1.2 关联式容器:按关键字有序访问)
[二、set 系列容器底层实现与核心特性](#二、set 系列容器底层实现与核心特性)
[2.1 红黑树:set 系列的底层基石](#2.1 红黑树:set 系列的底层基石)
[2.2 set 容器的核心特性](#2.2 set 容器的核心特性)
[2.3 multiset 容器的核心特性](#2.3 multiset 容器的核心特性)
[三、set 系列容器核心接口详解与实战示例](#三、set 系列容器核心接口详解与实战示例)
[3.1 构造函数与初始化](#3.1 构造函数与初始化)
[3.1.1 构造接口](#3.1.1 构造接口)
[3.1.2 示例:多种初始化方式](#3.1.2 示例:多种初始化方式)
[3.2 迭代器与遍历](#3.2 迭代器与遍历)
[3.2.1 迭代器接口](#3.2.1 迭代器接口)
[3.2.2 示例:多种遍历方式](#3.2.2 示例:多种遍历方式)
[3.3 插入操作(insert)](#3.3 插入操作(insert))
[3.3.1 插入接口](#3.3.1 插入接口)
[3.3.2 接口细节说明](#3.3.2 接口细节说明)
[3.3.3 示例:插入操作](#3.3.3 示例:插入操作)
[3.4 查找操作(find/count/lower_bound/upper_bound)](#3.4 查找操作(find/count/lower_bound/upper_bound))
[3.4.1 查找接口](#3.4.1 查找接口)
[3.4.2 接口差异说明(set vs multiset)](#3.4.2 接口差异说明(set vs multiset))
[3.4.3 示例:查找操作](#3.4.3 示例:查找操作)
[3.5 删除操作(erase)](#3.5 删除操作(erase))
[3.5.1 删除接口](#3.5.1 删除接口)
[3.5.2 接口差异说明(set vs multiset)](#3.5.2 接口差异说明(set vs multiset))
[3.5.3 示例:删除操作](#3.5.3 示例:删除操作)
[四、set 与 multiset 的核心差异总结](#四、set 与 multiset 的核心差异总结)
[五、set 系列容器的实战场景与 LeetCode 例题解析](#五、set 系列容器的实战场景与 LeetCode 例题解析)
[5.1 场景一:数组交集(去重 + 有序特性)](#5.1 场景一:数组交集(去重 + 有序特性))
[5.2 场景二:环形链表检测(快速查找特性)](#5.2 场景二:环形链表检测(快速查找特性))
[5.3 场景三:统计元素出现次数(multiset 的冗余特性)](#5.3 场景三:统计元素出现次数(multiset 的冗余特性))
[六、set 系列容器的使用注意事项与性能优化](#六、set 系列容器的使用注意事项与性能优化)
[6.1 使用注意事项](#6.1 使用注意事项)
[6.2 性能优化技巧](#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 均为只读迭代器,不允许通过迭代器修改元素(修改会破坏红黑树的有序性)。
-
模板参数:
cpptemplate <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 构造接口
cpp
// 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 示例:多种初始化方式
cpp
#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,8
cout << "s2初始化结果:";
for (auto e : s2) cout << e << " "; // 输出:2 4 5 7 8
cout << endl;
// 3. 迭代器区间构造(用vector的元素初始化)
vector<int> vec = {9, 3, 6, 3, 1};
set<int> s3(vec.begin(), vec.end()); // 去重后:1,3,6,9
cout << "s3初始化结果:";
for (auto e : s3) cout << e << " "; // 输出:1 3 6 9
cout << endl;
// 4. 拷贝构造
set<int> s4(s3);
cout << "s4拷贝构造结果:";
for (auto e : s4) cout << e << " "; // 输出:1 3 6 9
cout << endl;
// 5. 自定义比较规则(降序)
set<int, greater<int>> s5 = {4, 2, 7, 2, 8, 5}; // 去重+降序:8,7,5,4,2
cout << "s5降序初始化结果:";
for (auto e : s5) cout << e << " "; // 输出:8 7 5 4 2
cout << endl;
return 0;
}
3.2 迭代器与遍历
set 系列支持双向迭代器,遍历方式包括迭代器遍历和范围 for 遍历(C++11 及以上),遍历顺序由比较仿函数决定(默认升序)。
3.2.1 迭代器接口
cpp
// 正向迭代器:指向第一个元素,遍历至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 示例:多种遍历方式
cpp
#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 插入接口
cpp
// 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 表示插入失败(元素已存在)。
- 返回值为pair<iterator, bool>,其中:
- 单个元素插入(multiset) :
- 无返回值(或返回 iterator,C++11 后统一为 iterator),因为支持关键字冗余,插入一定成功。
- 批量插入(初始化列表 / 迭代器区间) :
- set会自动过滤重复元素,仅插入不存在的元素。
- multiset会插入所有元素,包括重复元素。
3.3.3 示例:插入操作
cpp
#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; // 成功,5
auto ret2 = s1.insert(5); // 插入重复元素
cout << "插入5:" << (ret2.second ? "成功" : "失败") << ",元素位置:" << *ret2.first << endl; // 失败,5
// 2. 插入初始化列表
s1.insert({2, 7, 3, 2}); // 过滤重复的2,插入2、7、3
cout << "插入列表后s1:";
for (auto e : s1) cout << e << " "; // 输出:2 3 5 7
cout << endl;
// 3. 插入迭代器区间
vector<int> vec = {4, 6, 3, 8};
s1.insert(vec.begin(), vec.end()); // 过滤重复的3,插入4、6、8
cout << "插入vector后s1:";
for (auto e : s1) cout << e << " "; // 输出:2 3 4 5 6 7 8
cout << 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 5
cout << endl;
// 2. 插入初始化列表(含重复元素)
ms1.insert({2, 5, 3, 2}); // 插入所有元素,包括重复的2和5
cout << "插入列表后ms1:";
for (auto e : ms1) cout << e << " "; // 输出:2 2 3 5 5 5 5
cout << endl;
return 0;
}
3.4 查找操作(find/count/lower_bound/upper_bound)
查找是关联式容器的核心优势,set 系列提供了多个高效的查找接口,满足不同场景的需求。
3.4.1 查找接口
cpp
// 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 示例:查找操作
cpp
#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; // 输出:1
cout << "元素9的个数:" << s1.count(9) << endl; // 输出:0
// 3. lower_bound/upper_bound范围查找
auto itLow = s1.lower_bound(3); // 第一个>=3的元素:3
auto itUp = s1.upper_bound(6); // 第一个>6的元素:7
cout << "范围[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
// 遍历所有5
cout << "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; // 输出:4
cout << "multiset中元素2的个数:" << ms1.count(2) << endl; // 输出:2
return 0;
}
3.5 删除操作(erase)
删除操作用于移除容器中的元素,支持按元素值、迭代器位置或迭代器区间删除,set 和 multiset 的接口一致,但行为存在差异。
3.5.1 删除接口
cpp
// 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 示例:删除操作
cpp
#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 8
cout << endl;
// 2. 按元素值删除(删除5)
size_t num1 = s1.erase(5);
cout << "删除元素5:" << (num1 ? "成功" : "失败") << ",删除个数:" << num1 << endl; // 成功,1
cout << "删除后s1:";
for (auto e : s1) cout << e << " "; // 输出:3 4 6 7 8
cout << 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 8
cout << 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; // 输出:4
cout << "删除后ms1:";
for (auto e : ms1) cout << e << " "; // 输出:2 2 3
cout << 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 3
cout << 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,高效查找共同元素(类似归并排序的合并过程)。
代码实现:
cpp
#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 << " "; // 输出:2
cout << 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)仍未发现重复节点,则链表无环。
代码实现:
cpp
#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 接口)。
- 按出现次数和字典序排序,输出结果。
代码实现:
cpp
#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
cpp
#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 系列的使用,能大幅提升代码的效率和可读性。在实际开发中,大家应根据数据的特性(是否有序、是否重复、访问频率)选择合适的容器,让工具为需求服务。感谢大家的支持!