STL之set以及set和map区别

一、set 的核心特性

set 是关联式容器 ,底层由红黑树(平衡二叉搜索树)实现,存储的是唯一且有序的元素

  • 自动排序 :元素按值的升序自动排列(默认使用 less<T> 比较函数)
  • 元素唯一性:不允许有重复元素,插入重复元素会失败
  • 高效查找 :插入、删除、查找操作的时间复杂度均为 O(log n)
  • 不支持随机访问 :不能使用下标运算符 [] 按位置访问
  • 迭代器是双向迭代器 :只支持 ++-- 操作,不支持随机访问运算
  • 元素不可修改 :set 中的元素是 const 类型,一旦插入不能修改,只能删除后重新插入

与 map 的关系:set 可以看作是一种特殊的 map,其中键和值是同一个元素。它们的底层实现完全相同,很多特性和操作也高度一致。

二、头文件与命名空间

cpp 复制代码
#include <set>  // 必须包含此头文件
using namespace std;  // 或使用 std::set 前缀

三、set 的定义与初始化

set 的模板参数为 set<T, Compare = less<T>, Allocator = allocator<T>>

cpp 复制代码
// 1. 空set
set<int> s1;

// 2. 拷贝构造
set<int> s2(s1);

// 3. 迭代器范围构造
int arr[] = {3, 1, 4, 1, 5, 9};
set<int> s3(arr, arr + 6);  // 自动去重,s3: {1, 3, 4, 5, 9}

// 4. 列表初始化(C++11及以上)
set<int> s4 = {5, 2, 8, 1, 9, 3};  // 自动排序去重,s4: {1, 2, 3, 5, 8, 9}
set<int> s5{5, 2, 8, 1, 9, 3};

// 5. 自定义比较函数的set
set<int, greater<int>> s6;  // 按值降序排列

四、元素访问

set 没有下标运算符 []at() 方法,只能通过迭代器查找函数访问元素:

cpp 复制代码
set<int> s = {1, 2, 3, 4, 5};

// 1. 访问首尾元素
cout << *s.begin() << endl;  // 输出1
cout << *s.rbegin() << endl; // 输出5

// 2. 通过find()查找元素
set<int>::iterator it = s.find(3);
if (it != s.end()) {
    cout << "找到元素:" << *it << endl;  // 输出3
}

// 3. 遍历访问所有元素(唯一方式)
for (int num : s) {
    cout << num << " ";
}

重要特性 :set 的迭代器是 const_iterator,不能通过迭代器修改元素的值:

cpp 复制代码
*it = 10;  // 编译错误!set元素不可修改

五、元素增删操作

(1)插入元素

cpp 复制代码
set<int> s;

// 1. insert()插入单个元素
s.insert(10);
s.insert(20);
s.insert(10);  // 插入重复元素,会失败

// 2. insert()插入迭代器范围的元素
int arr[] = {30, 40, 50};
s.insert(arr, arr + 3);

// 3. emplace()原地构造(C++11及以上,更高效)
s.emplace(60);  // 直接在set内部构造元素,避免拷贝

// 4. 检查插入是否成功
auto result = s.insert(20);
if (result.second) {
    cout << "插入成功" << endl;
} else {
    cout << "插入失败,元素20已存在" << endl;  // 会执行这行
}
// result.first 是指向插入位置(或已存在元素)的迭代器

上面的result.second解释:

函数insert返回的时pair<iterator,bool>result=s.insert(20)。

其中result.first代表的时插入的20,而s.second代表的时true或者是false;

(2)删除元素

cpp 复制代码
set<int> s = {1, 2, 3, 4, 5, 6, 7, 8, 9};

// 1. 通过值删除
int num_erased = s.erase(5);  // 删除值为5的元素,返回删除的元素个数(0或1)
cout << "删除了" << num_erased << "个元素" << endl;  // 输出1

// 2. 通过迭代器删除
set<int>::iterator it = s.find(3);
if (it != s.end()) {
    s.erase(it);  // 删除迭代器指向的元素,返回下一个迭代器(C++11及以上)
}

// 3. 删除迭代器范围的元素
it = s.find(7);
s.erase(it, s.end());  // 删除从it到end()的所有元素

// 4. 清空所有元素
s.clear();  // s: 空

六、容量管理

set 没有 reserve()capacity() 方法,因为它的内存是按需分配的:

cpp 复制代码
set<int> s = {1, 2, 3, 4, 5};

// 获取元素个数
cout << s.size() << endl;  // 输出5

// 判断是否为空
cout << s.empty() << endl;  // 输出0(false)

// 获取set能容纳的最大元素个数
cout << s.max_size() << endl;

七、迭代器与遍历

set 的迭代器是双向迭代器,指向的是 const T 类型的元素:

cpp 复制代码
set<int> s = {5, 2, 8, 1, 9, 3};

// 1. 迭代器遍历
for (set<int>::iterator it = s.begin(); it != s.end(); ++it) {
    cout << *it << " ";
}
cout << endl;  // 输出:1 2 3 5 8 9

// 2. 范围for循环(C++11及以上)
for (int num : s) {
    cout << num << " ";
}
cout << endl;

// 3. 反向迭代器
for (set<int>::reverse_iterator rit = s.rbegin(); rit != s.rend(); ++rit) {
    cout << *rit << " ";
}
cout << endl;  // 输出:9 8 5 3 2 1

八、与 STL 算法结合

set 的双向迭代器不能与需要随机访问迭代器的算法配合使用。同时,set 提供了自己的成员函数版本,比通用算法更高效:

cpp 复制代码
set<int> s = {1, 2, 3, 4, 5, 6, 7, 8, 9};

// 1. 查找元素(O(log n),比通用find快得多)
set<int>::iterator it = s.find(5);
if (it != s.end()) {
    cout << "找到元素:" << *it << endl;
}

// 2. 统计元素出现的次数(只能返回0或1,因为元素唯一)
int count = s.count(3);
cout << "元素3出现的次数:" << count << endl;  // 输出1

// 3. 查找第一个大于等于指定值的元素
it = s.lower_bound(4);
cout << "lower_bound(4): " << *it << endl;  // 输出4

// 4. 查找第一个大于指定值的元素
it = s.upper_bound(4);
cout << "upper_bound(4): " << *it << endl;  // 输出5

// 5. 查找等于指定值的元素范围(返回pair<iterator, iterator>)
auto range = s.equal_range(5);
for (auto it = range.first; it != range.second; ++it) {
    cout << *it << " ";
}
cout << endl;  // 输出:5

九、set 常见问题与注意事项

1. 迭代器失效问题

set 的迭代器失效情况非常少,与 map 完全相同:

  • 插入操作:不会使任何迭代器失效
  • 删除操作:只会使指向被删除元素的迭代器失效,其他迭代器仍然有效

这是 set 非常重要的特性,在频繁插入和删除元素的场景下,不需要担心迭代器失效问题。

2. 元素不可修改

set 中的元素是 const 类型,因为修改元素会破坏红黑树的排序结构。如果需要修改元素,必须先删除旧元素,再插入新元素:

cpp 复制代码
set<int> s = {1, 2, 3};
// *s.find(2) = 20;  // 编译错误!

// 正确做法:
s.erase(2);
s.insert(20);

3. 自定义比较函数

set 的元素按值自动排序,因此元素类型必须支持比较操作。对于自定义类型,需要提供比较函数:

cpp 复制代码
// 自定义类型
struct Person {
    string name;
    int age;
};

// 自定义比较函数
struct ComparePerson {
    bool operator()(const Person& p1, const Person& p2) const {
        if (p1.name != p2.name) {
            return p1.name < p2.name;  // 先按姓名排序
        }
        return p1.age < p2.age;  // 姓名相同按年龄排序
    }
};

// 使用自定义比较函数的set
set<Person, ComparePerson> s;

4. multiset 容器

multiset 是 set 的变体,允许存储重复元素:

  • 其他特性与 set 完全相同
  • count() 方法可以返回大于 1 的值
  • equal_range() 方法可以返回所有等于指定值的元素范围
  • erase(value) 方法会删除所有等于指定值的元素

5. 与 unordered_set 的区别

set 和 unordered_set 都是存储唯一元素的容器,但底层实现和特性有很大不同:

  • set:红黑树实现,有序,O (log n) 操作,空间开销小
  • unordered_set:哈希表实现,无序,平均 O (1) 操作,空间开销大

十、set和map的区别

因为底层都是自平衡二叉搜索树(红黑树),所以它们共享以下所有特性:

表格

特性 set map
底层实现 红黑树 红黑树
插入 / 删除 / 查找时间复杂度 O(log n) O(log n)
自动排序 元素按值升序排列(默认less<T> 元素按键升序排列(默认less<Key>
元素唯一性 不允许重复元素 不允许重复键
迭代器类型 双向迭代器(仅支持++/-- 双向迭代器(仅支持++/--
迭代器失效规则 插入不失效,删除仅失效被删元素迭代器 插入不失效,删除仅失效被删元素迭代器
不支持随机访问 不能用[]按位置访问 不能用[]按位置访问
自定义比较 支持自定义排序规则 支持自定义排序规则

二、核心区别(从根本到用法)

  1. 最根本区别:存储结构不同

这是所有其他区别的根源。

set:只存单个值

set 是值的集合,每个元素就是一个独立的值。

cpp 复制代码
set<int> s = {1, 2, 3, 4};
// 内存中存的是:1, 2, 3, 4

map:存键值对(key-value pair)

map 是键到值的映射 ,每个元素都是一个 pair<const Key, T> 类型的对象,包含一个不可修改的键和一个对应的值。

cpp 复制代码
map<int, string> m = {{1, "one"}, {2, "two"}};
// 内存中存的是:
// pair(1, "one"), pair(2, "two")
  1. 功能定位完全不同

set = 有序集合

  • 核心作用:去重 + 排序

  • 解决的问题:"这个元素是否存在?"、"所有元素按顺序排列是什么样?"

map = 有序字典 / 映射表

  • 核心作用:通过键快速查找对应的值

  • 解决的问题:"键为 x 的元素对应的值是什么?"、"建立 x 到 y 的对应关系"

  1. 元素访问方式不同

set:没有键,只能通过值或迭代器访问

  • 没有 operator[]at() 方法(因为没有键可以索引)

  • 只能通过 find() 查找元素,或者通过迭代器遍历

  • 不能通过位置访问元素

cpp 复制代码
set<int> s = {1, 2, 3};

// 正确:通过find查找
auto it = s.find(2);
if (it != s.end()) cout << *it << endl;

// 错误:没有[]运算符
// cout << s[2] << endl;

// 错误:没有at()方法
// cout << s.at(2) << endl;

map:必须通过键访问值

  • operator[]at() 方法(通过键索引)

  • 可以通过 find() 查找键值对

  • 不能通过位置访问元素

cpp 复制代码
map<int, string> m = {{1, "one"}, {2, "two"}};

// 正确:通过[]访问
cout << m[1] << endl;  // 输出"one"

// 正确:通过at()访问
cout << m.at(2) << endl;  // 输出"two"

// 正确:通过find查找
auto it = m.find(1);
if (it != m.end()) cout << it->second << endl;

重要陷阱 :map 的 operator[] 如果访问不存在的键,会自动插入一个默认值的键值对,而 set 根本没有这个问题。

  1. 元素可修改性不同

set:元素完全不可修改

set 中的元素是 const T 类型,一旦插入就不能修改。因为修改元素会破坏红黑树的排序结构。

cpp 复制代码
set<int> s = {1, 2, 3};
auto it = s.find(2);
// *it = 20;  // 编译错误!set元素不可修改

// 正确做法:删除旧元素,插入新元素
s.erase(it);
s.insert(20);

map:键不可修改,值可以修改

map 中的键是 const Key 类型,不能修改;但值是普通的 T 类型,可以任意修改。

cpp 复制代码
map<int, string> m = {{1, "one"}, {2, "two"}};
auto it = m.find(1);

// it->first = 10;  // 编译错误!键不可修改
it->second = "ONE";  // 正确!值可以修改
cout << m[1] << endl;  // 输出"ONE"
  1. 迭代器指向不同

set 迭代器:指向 const T

cpp 复制代码
set<int>::iterator it = s.begin();
// *it 是 const int& 类型

map 迭代器:指向 pair<const Key, T>

cpp 复制代码
map<int, string>::iterator it = m.begin();
// it->first 是 const int& 类型(键)
// it->second 是 string& 类型(值)
  1. 特殊成员函数不同

map 有两个 set 没有的核心成员函数:

  • operator[]:通过键访问值,不存在则插入

  • at():通过键访问值,不存在则抛出异常

  1. 变体容器对应关系
基础容器 允许重复的变体 区别
set multiset 允许存储重复元素
map multimap 允许存储重复键

三、代码写法直观对比

操作 set map
定义 set<int> s; map<int, string> m;
插入 s.insert(10); m.insert({10, "ten"});m[10] = "ten";
查找 auto it = s.find(10); auto it = m.find(10);
访问 cout << *it << endl; cout << it->first << ": " << it->second << endl;
修改 不能修改,只能删了重插 it->second = "TEN";
删除 s.erase(10);s.erase(it); m.erase(10);m.erase(it);
遍历 for (int num : s) cout << num << endl; for (auto& p : m) cout << p.first << ": " << p.second << endl;

四、适用场景对比

什么时候用 set?

  1. 需要对一组元素进行自动去重

  2. 需要元素保持有序

  3. 主要操作是判断元素是否存在

  4. 需要快速查找最大 / 最小值(*s.begin()*s.rbegin()

  5. 需要对元素进行集合运算(交集、并集、差集等)

典型场景

  • 统计文章中出现的不同单词

  • 存储用户 ID(保证唯一)

  • 实现一个有序的任务队列

什么时候用 map?

  1. 需要建立键到值的对应关系

  2. 需要通过键快速查找值

  3. 需要按键有序存储键值对

  4. 需要统计某个元素出现的次数

典型场景

  • 字典(单词到释义的映射)

  • 学生成绩管理(学号到成绩的映射)

  • 统计字符串中每个字符出现的次数

  • 配置文件解析(配置项到值的映射)


五、终极对比表

特性 set map
存储内容 单个值 键值对(pair<const Key, T>)
核心功能 去重、排序、存在性判断 键值映射、快速查找
元素唯一性 值唯一 键唯一
排序依据 元素值
operator\[\] 有(自动插入)
at() 有(抛异常)
元素可修改性 完全不可修改 键不可修改,值可修改
迭代器指向 const T pair<const Key, T>
典型用法 set<int> s = {1,2,3}; map<int, string> m = {``{1,"one"}};

六、一句话总结

  • set 是存单个元素的有序集合,用来去重和判断存在性

  • map 是存键值对的有序字典,用来通过键快速查值

相关推荐
Halo_tjn1 小时前
NIO 技术的使用
java·开发语言·nio
砍材农夫1 小时前
物联网 基于netty核心实战-安全tls
java·开发语言·前端·物联网·安全
SEO_juper1 小时前
JavaScript 渲染:AI 智能体无法读取,直接影响收录
开发语言·前端·javascript·aigc·seo·跨境电商·geo
Python+991 小时前
C++ 内存模型 & 底层原理
java·jvm·c++
jllllyuz1 小时前
通信信号调制识别系统(MATLAB实现)
开发语言·matlab
zincsweet1 小时前
Linux 命名管道(FIFO)详解:原理分析、源码封装与通信流程图解
linux·服务器·c++·流程图
Promise微笑1 小时前
算法突围:“双核四驱”理论下的“官网”AI引用概率提升指南
人工智能·算法·chatgpt
Kurisu5751 小时前
深度解析:Java 对象的内存布局与指针压缩原理
java·开发语言
KaMeidebaby1 小时前
卡梅德生物技术快报|免疫共沉淀 - Co-IP 实验在转录因子 ATF3/Smad4 蛋白互作研究中的应用实例解析
网络·人工智能·网络协议·tcp/ip·其他·算法·新浪微博