STL 容器之 set:从入门到实战,玩转去重排序神器

目录

STL 容器之 set:从入门到实战,玩转去重排序神器

一、先理清:关联式容器与序列式容器的区别

二、set 核心特性:去重 + 自动升序,两大 “默认技能”

2.1 两大核心特性

三、set 常用接口:构造、增删查,掌握这些就够了

3.1 构造方式:四种常用初始化方法

3.2 增删查接口:set 的核心操作

(1)插入操作:insert

(2)查找操作:find + count

(3)删除操作:erase

(4)边界查找:lower_bound + upper_bound

四、multiset:允许重复的 set

五、set 实战:LeetCode 349. 两个数组的交集

题目要求

解题思路

代码实现

代码解析

六、set 常见问题与总结

6.1 常见问题解答

6.2 set 核心用法总结


STL 容器之 set:从入门到实战,玩转去重排序神器

如果你在学习 C++ STL 时,对 “关联式容器” 感到困惑,或者想找一个能自动去重、有序存储数据的工具,那 set 绝对是你的不二之选。这篇文章会用最通俗的方式,从基础概念到实际代码,再到 LeetCode 真题实战,带你全方位掌握 set 容器,让你轻松应对数据去重与有序处理场景。

一、先理清:关联式容器与序列式容器的区别

在深入学习 set 之前,我们得先明确它的容器类别。之前接触过的 vector、list、deque 等属于序列式容器,就像排队买奶茶,数据按 “先来后到” 排列,访问时需按顺序查找,数据位置变动对整体逻辑结构影响小。

而 set 属于关联式容器,其逻辑结构更像超市里 “按规则分类的货架”—— 比如薯片、饼干、巧克力各占固定区域,找薯片无需从头逛,直接去对应区域即可。关联式容器的数据以 “关键字(key)” 为核心存储和访问,数据间关联紧密,随意变动位置会破坏分类规则。

这里要重点记住:set 底层基于红黑树(一种平衡二叉搜索树)实现,这带来两大优势:一是增删查效率极高,时间复杂度为 O (log N);二是遍历数据时,默认按关键字从小到大有序排列,这一特性在实际开发中经常用到。

二、set 核心特性:去重 + 自动升序,两大 “默认技能”

set 的核心价值就在于它的两个默认特性,掌握这两点,就抓住了 set 的精髓:

2.1 两大核心特性

  1. 自动去重:插入重复数据时会失败,容器内始终只保留一份相同数据。比如插入 5、2、7、5,最终容器内只有 2、5、7,重复的 5 会被自动过滤。
  1. 自动升序:遍历容器时,数据默认按关键字从小到大排列。这是因为红黑树的中序遍历本身就是有序的,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]

解题思路

  1. 用两个 set 分别存储两个数组的元素,利用 set 的自动去重特性,过滤掉重复数据;
  1. 用双指针遍历两个 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,它会帮你快速解决问题!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值