目录
二、set 核心特性:去重 + 自动升序,两大 “默认技能”
(4)边界查找:lower_bound + upper_bound
五、set 实战:LeetCode 349. 两个数组的交集
STL 容器之 set:从入门到实战,玩转去重排序神器
如果你在学习 C++ STL 时,对 “关联式容器” 感到困惑,或者想找一个能自动去重、有序存储数据的工具,那 set 绝对是你的不二之选。这篇文章会用最通俗的方式,从基础概念到实际代码,再到 LeetCode 真题实战,带你全方位掌握 set 容器,让你轻松应对数据去重与有序处理场景。
一、先理清:关联式容器与序列式容器的区别
在深入学习 set 之前,我们得先明确它的容器类别。之前接触过的 vector、list、deque 等属于序列式容器,就像排队买奶茶,数据按 “先来后到” 排列,访问时需按顺序查找,数据位置变动对整体逻辑结构影响小。
而 set 属于关联式容器,其逻辑结构更像超市里 “按规则分类的货架”—— 比如薯片、饼干、巧克力各占固定区域,找薯片无需从头逛,直接去对应区域即可。关联式容器的数据以 “关键字(key)” 为核心存储和访问,数据间关联紧密,随意变动位置会破坏分类规则。
这里要重点记住:set 底层基于红黑树(一种平衡二叉搜索树)实现,这带来两大优势:一是增删查效率极高,时间复杂度为 O (log N);二是遍历数据时,默认按关键字从小到大有序排列,这一特性在实际开发中经常用到。
二、set 核心特性:去重 + 自动升序,两大 “默认技能”
set 的核心价值就在于它的两个默认特性,掌握这两点,就抓住了 set 的精髓:
2.1 两大核心特性
- 自动去重:插入重复数据时会失败,容器内始终只保留一份相同数据。比如插入 5、2、7、5,最终容器内只有 2、5、7,重复的 5 会被自动过滤。
- 自动升序:遍历容器时,数据默认按关键字从小到大排列。这是因为红黑树的中序遍历本身就是有序的,set 直接利用了这一特性。
另外有个关键注意点:set 的迭代器是常量迭代器,无法通过迭代器修改容器内的数据。为什么?因为修改数据会破坏红黑树的有序结构 —— 比如把 5 改成 8,原本有序的存储结构会瞬间混乱,所以 STL 直接禁止了这种操作,从根源上保证容器有序性。
三、set 常用接口:构造、增删查,掌握这些就够了
不用死记所有接口,重点掌握以下常用接口即可,其用法和 vector、list 类似,上手很快。
3.1 构造方式:四种常用初始化方法
set 提供了多种构造方式,满足不同初始化需求,看代码就能轻松理解:
#include <set>
using namespace std;
int main() {
// 1. 无参构造:创建空 set
set<int> s1;
// 2. 迭代器区间构造:用其他容器的区间初始化(自动去重+升序)
vector<int> v = {2,5,7,5};
set<int> s2(v.begin(), v.end()); // s2 最终为 {2,5,7}
// 3. 拷贝构造:复制另一个 set 的所有数据
set<int> s3(s2); // s3 与 s2 完全相同,即 {2,5,7}
// 4. 初始化列表构造:直接传入数据列表(自动去重+升序)
set<int> s4 = {3,1,4,1,5}; // s4 最终为 {1,3,4,5}
return 0;
}
3.2 增删查接口:set 的核心操作
这部分是 set 的实用重点,每个接口都结合示例讲解,确保你能直接复用。
(1)插入操作:insert
insert 是 set 插入数据的核心接口,支持单个插入、列表插入和区间插入,且会自动过滤重复数据:
set<int> s;
// 1. 单个插入:返回 pair<iterator, bool>,bool 表示插入是否成功
auto ret1 = s.insert(5); // 插入 5 成功,ret1.second 为 true
auto ret2 = s.insert(5); // 重复插入 5 失败,ret2.second 为 false
// 2. 列表插入:一次性插入多个数据,重复数据自动过滤
s.insert({2,8,5}); // 此时 s 为 {2,5,8}
// 3. 区间插入:用其他容器的迭代器区间插入(与构造方式类似)
vector<int> v = {10, 3, 3};
s.insert(v.begin(), v.end()); // 插入后 s 为 {2,3,5,8,10}
(2)查找操作:find + count
查找数据主要用 find 和 count 两个接口,前者精准定位数据,后者统计数据个数(set 中只有 0 或 1):
set<int> s = {2,5,8,10};
// 1. find(val):查找 val,找到返回对应迭代器,没找到返回 end()
auto pos = s.find(5);
if (pos != s.end()) {
cout << "找到数据:" << *pos << endl; // 输出“找到数据:5”
}
auto pos2 = s.find(3);
if (pos2 == s.end()) {
cout << "未找到数据 3" << endl; // 输出“未找到数据 3”
}
// 2. count(val):返回 val 在 set 中的个数(0 或 1)
cout << "5 的个数:" << s.count(5) << endl; // 输出 1
cout << "3 的个数:" << s.count(3) << endl; // 输出 0
(3)删除操作:erase
erase 支持按迭代器、按值、按区间删除,操作灵活且高效:
set<int> s = {2,5,8,10,13};
// 1. 按迭代器删除:删除迭代器指向的元素
s.erase(s.begin()); // 删除第一个元素(2),此时 s 为 {5,8,10,13}
// 2. 按值删除:删除指定值,返回删除的个数(0 或 1)
int delNum = s.erase(8); // 删除 8 成功,delNum 为 1,s 变为 {5,10,13}
// 3. 按区间删除:删除 [first, last) 区间内的元素
s.erase(s.begin(), --s.end()); // 删除从 begin() 到倒数第二个元素,s 最终为 {13}
(4)边界查找:lower_bound + upper_bound
这两个接口特别适合 “查找区间数据”,比如找某个范围内的元素,效率比遍历高很多:
set<int> s = {10,20,30,40,50,60,70};
// lower_bound(val):返回第一个“大于等于 val”的元素迭代器
auto itlow = s.lower_bound(30); // 指向 30
// upper_bound(val):返回第一个“大于 val”的元素迭代器
auto itup = s.upper_bound(60); // 指向 70
// 结合 erase,删除 [30, 60] 区间的元素(因为 itup 指向 70,区间是 [itlow, itup))
s.erase(itlow, itup); // 此时 s 为 {10,20,70}
四、multiset:允许重复的 set
multiset 是 set 的 “兄弟容器”,两者用法几乎完全一致,唯一区别是允许重复数据。比如插入 5、2、5、7,multiset 内会保留 2、5、5、7,遍历依然有序。
由于允许重复,multiset 的部分接口行为与 set 有细微差异,重点看以下示例:
#include <set>
#include <iostream>
using namespace std;
int main() {
// 初始化:插入多个重复数据
multiset<int> ms = {4,2,7,2,4,8,4,5,4,9};
// 遍历:输出 2 2 4 4 4 4 5 7 8 9(有序且保留重复)
for (auto e : ms) {
cout << e << " ";
}
cout << endl;
// 1. find(val):返回中序遍历中第一个 val 的迭代器
auto pos = ms.find(2);
// 遍历所有 val=2 的元素
while (pos != ms.end() && *pos == 2) {
cout << *pos << " "; // 输出 2 2
pos++;
}
cout << endl;
// 2. count(val):返回 val 的实际个数(此处 4 出现 4 次)
cout << "4 的个数:" << ms.count(4) << endl; // 输出 4
// 3. erase(val):删除所有等于 val 的元素(此处删除所有 4)
ms.erase(4);
// 遍历:输出 2 2 5 7 8 9(所有 4 已删除)
for (auto e : ms) {
cout << e << " ";
}
return 0;
}
五、set 实战:LeetCode 349. 两个数组的交集
理论学得再好,不如实战练一练。这道题是 set 去重特性的经典应用,用 set 能轻松解决。
题目要求
给定两个数组,求它们的交集(元素唯一,顺序不限)。例如:
- 输入:nums1 = [1,2,2,1],nums2 = [2,2]
- 输出:[2]
解题思路
- 用两个 set 分别存储两个数组的元素,利用 set 的自动去重特性,过滤掉重复数据;
- 用双指针遍历两个 set,找到相同的元素,即为交集。
代码实现
#include <vector>
#include <set>
using namespace std;
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> result;
// 步骤2:双指针遍历,找相同元素
auto it1 = s1.begin();
auto it2 = s2.begin();
while (it1 != s1.end() && it2 != s2.end()) {
if (*it1 < *it2) {
// 小的元素指针后移,寻找更大的元素
it1++;
} else if (*it1 > *it2) {
it2++;
} else {
// 找到相同元素,加入结果集
result.push_back(*it1);
it1++;
it2++;
}
}
return result;
}
};
代码解析
- 去重排序:通过 set 的迭代器区间构造,一次性完成数组去重和排序,无需额外写去重逻辑;
- 双指针遍历:利用 set 有序的特性,双指针移动方向明确,避免了暴力遍历(时间复杂度从 O (N*M) 降至 O (log N + log M))。
六、set 常见问题与总结
6.1 常见问题解答
Q1:为什么 set 的迭代器是常量迭代器,不能修改元素?
A:因为 set 底层是红黑树,元素有序是核心特性。修改元素会破坏红黑树的有序结构,比如把 5 改成 8,原本的排序逻辑会完全混乱,所以 STL 直接禁止修改操作。
Q2:set 和 multiset 该怎么选?
A:如果需要存储 “无重复的有序数据”,用 set;如果需要存储 “允许重复的有序数据”(比如统计某个值的出现次数,但又需要有序),用 multiset。
Q3:查找 set 中的元素,用 std::find(算法库)还是 set 自带的 find?
A:必须用 set 自带的 find!std::find 是线性查找(时间复杂度 O (N)),而 set 自带的 find 基于红黑树特性,是对数查找(O (log N)),数据量大时效率差距极大。
6.2 set 核心用法总结
容器 | 核心特性 | 常用场景 | 关键接口 |
set | 去重、自动升序、key=value | 数组交集、元素去重、有序存储 | insert、find、erase、lower_bound |
multiset | 允许重复、自动升序 | 允许重复的有序数据存储、统计次数 | 同 set,但 count 返回实际个数 |
到这里,set 容器的核心内容就全部讲完了。其实 set 的用法并不复杂,关键是理解它 “去重 + 有序” 的核心特性,以及底层红黑树带来的高效性。下次遇到数据去重、有序存储或区间查找的场景,记得第一时间想到 set,它会帮你快速解决问题!