
🎬 个人主页 :MSTcheng · CSDN
🌱 代码仓库 :MSTcheng · Gitee
🔥 精选专栏 : 《C语言》
《数据结构》
《C++由浅入深》
💬座右铭: 路虽远行则将至,事虽难做则必成!
前言:在上一篇文章中,我们介绍了二叉搜索树这种树形结构,它与之前学过的序列式容器有所不同。本文将重点讲解基于二叉搜索树实现的两个容器之一:
set。它的底层实现也采用了平衡二叉搜索树。
文章目录
- 一、set认识
-
- [1.1 set的基本概念](#1.1 set的基本概念)
- 二、set的使用
-
- [2.1 set的构造和迭代器](#2.1 set的构造和迭代器)
- [2.2 set的增删查操作](#2.2 set的增删查操作)
- [2.2 multiset的使用](#2.2 multiset的使用)
- 三、总结
一、set认识
1.1 set的基本概念
set:
是一种只存储键(key)的容器,元素本身既是键也是值。set中的元素是唯一的,并且按升序自动排序。
set的特点:
- 有序性: 元素按升序自动排序。
- 唯一性: 每个元素在
set中只能出现一次。(不含重复数据) - 操作复杂度: 插入、删除和查找操作的平均时间复杂度为
O(log n)。
二、set的使用

关于set的模板参数有一些注意事项:
- 第一个模板参数:
T就是set底层关键字的类型,模板里用的是T,但其实使用key更直观。(在后面实现红黑树我们使用的就是key)。 - 第二个模板参数: 比较器
compare,set默认要求T(key)支持小于比较,如果不支持或者想按自己的需求走可以自行实现仿函数传给第二个模版参数。 - 第三个参数: 空间配置器
allocate,set底层存储数据的内存是从空间配置器申请的,如果需要可以自己实现内存池,传给第三个参数。(一般情况下不用传)
2.1 set的构造和迭代器
一、set的构造
对于set的构造我们需关注下面几个构造即可:

代码示例:
cpp
#include<iostream>
#include<set>
using namespace std;
void test_set1()
{
//使用initializer_list(初始化列表)构造
set<int> s = { 1,3,4,6,2,1,5,9,7,8 };//中序遍历 加上排序去重
//隐式类型转化
set<int> s2({1, 2, 3, 2, 5, 6, 7,9,10,8,12});
//拷贝构造
set<int> s3(s);
//迭代器区间构造
set<int> s4(s2.begin(),s2.end());
}
二、set的迭代器

set的迭代器是一个双向迭代器,所以支持正向和反向迭代遍历,遍历默认按升序顺序 ,因为底层是二叉搜索树,迭代器遍历走的中序遍历;支持迭代器就意味也支持范围for。
值得注意的是: set的iterator和const_iterator都不支持迭代器修改数据,因为 set存的是key(键),如果修改了key那么底层的二叉树结构就会被破坏 ,所以set是不支持修改的(不能修改数据)!!!
代码示例:
cpp
#include<iostream>
#include<set>
using namespace std;
void test_set1()
{
//使用initializer_list构造
set<int> s = { 1,3,4,6,2,1,5,9,7,8 };//中序遍历 加上排序去重
//隐式类型转化
set<int> s2({1, 2, 3, 2, 5, 6, 7,9,10,8,12});
//拷贝构造
set<int> s3(s);
set<int> s4(s2.begin(), s2.end());
//set<int>::iterator it = s.begin();
auto it = s4.begin();
while (it != s4.end())
{
cout << *it << " ";
it++;
}
cout << endl;
//反向迭代器
auto It = s4.rbegin();
while (It != s4.rend())
{
cout << *It << " ";
It++;
}
cout << endl;
}

2.2 set的增删查操作
1、插入数据insert

pair<iterator, bool> insert(const value_type& val)
插入元素val到set中。返回一个pair,其中first是指向插入元素的迭代器,second是一个布尔值,表示插入是否成功(若元素已存在则返回false)。
template <class InputIterator>
void insert(InputIterator first, InputIterator last);
插入区间[first, last)内的元素到 set 中。无返回值。
iterator insert(iterator hint, const value_type& val);
hint 是一个迭代器,用于提示插入位置(可能优化插入效率)。返回指向插入元素的迭代器(无论是否插入成功)。
void insert(initializer_list<value_type> il);
插入初始化列表 il 中的所有元素到 set 中。无返回值。
代码示例:
cpp
#include<iostream>
#include<set>
using namespace std;
void test_set2()
{
// 去重+升序排序
set<int> s;
// 去重+降序排序(给⼀个⼤于的仿函数)
//set<int, greater<int>> s;
s.insert(5);
s.insert(2);
s.insert(7);
s.insert(5);
auto it = s.begin();
while (it != s.end())
{
// *it = 1;//不能修改key
cout << *it << " ";
++it;
}
cout << endl;
}
2、查找find和删除erase

代码示例:
cpp
#include<iostream>
#include<set>
using namespace std;
void test_set3()
{
set<int> s = { 1,3,4,6,2,1,5,9,7,8 };//中序遍历 加上排序去重
//删除
auto pos=s.find(9);
if(pos!=s.end())
{
//删除一个迭代器位置的值
s.erase(pos);
}
for (auto& e : s)
{
cout << e << " ";
}
cout << endl;
//使用set查找在不在 关键比较的是key
//注意这里的find返回的是迭代器
auto pos1 = s.find(50);
auto pos2=find(s.begin(),s.end(),50);//算法库里的find不推荐
if (pos1 != s.end())
{
cout << "找到了!" << endl;
}
else
{
cout << "没找到!" << endl;
}
}
注意: 算法库里的find是一种泛型算法 ,它不在意容器的底层结构,而是遍历比较所有元素直到找到我们所指定的值,它的效率是极低的,时间复杂度为O(n) ,而 set中的find 底层是基于红黑树的结构来查找的,所以时间复杂度为O(logn)效率极高。
3、lower_bound和upper_bound的使用

lower_bound 返回指向第一个不小于给定值的元素的迭代器。如果所有元素都小于给定值,则返回 end()。
upper_bound 返回指向第一个大于给定值的元素的迭代器。如果所有元素都不大于给定值,则返回 end()。
代码示例:
cpp
void test_set4()
{
set<int> s2({1, 2, 3, 2, 5, 6, 7,9,10,8,12});
//==========================
//lower_bound和up_bound的使用
//例如我们需要2------9这个区间
//==========================
//s2.erase(2);
auto it1 = s2.lower_bound(2);//如果找不到2那就找比2大一点的数 闭区间
//s2.erase(9);
auto it2 = s2.upper_bound(9);//如果找不到9那就找比9小一点的数 开区间
for (auto it = it1;it != it2;it++)
{
cout << *it << " ";
}
cout << endl;
//如果我们想删除一段区间的值也可以使用lower_bound和up_bound
// 直接提供这一段迭代器区间删除
s2.erase(it1, it2);
for (auto& e : s2)
{
cout << e << " ";
}
cout << endl;
}
2.2 multiset的使用
multiset允许重复元素的存在,同一元素可多次插入。 其它的特性与set基本类似,主要区别就是multiset支持冗余,set不支持。则insert/find/count/erase都支持值冗余。
代码示例:
cpp
#include<iostream>
#include<set>
using namespace std;
void test_multiset()
{
//multi_set不需要另外包含头文件 已经被set包含
//multi_set与set最大的区别就是multi_set支持重复数据
multiset<int> s = { 1,2,3,3,5,3,7,9,10,40,3 };
auto it = s.begin();
while (it != s.end())
{
cout << *it << " ";
it++;
}
cout << endl;
s.insert(10);
//也可以使用范围for遍历
for (auto& e : s)
{
cout << e << " ";
}
cout << endl;
int x = 0;
cin >> x;
auto ret = s.find(3);
//取到所有与x相同的元素
while (ret != s.end() && (*ret == x))
{
cout << *ret << " ";
++ret;
}
cout << endl;
//删除重复的元素
s.erase(10);
for (auto& e : s)
{
cout << e << " ";
}
cout << endl;
}
三、总结
set与multiset的对比表格:
| 特性 | set | multiset |
|---|---|---|
| 元素唯一性 | 严格唯一,插入重复元素时自动忽略。 | 允许重复元素,相同值可多次插入。 |
| 插入行为 | 返回 pair<iterator, bool>,bool 为 false 表示元素已存在。 |
直接返回指向新元素的迭代器,始终成功。 |
| 底层实现 | 基于红黑树实现,元素自动按升序排列。 | 同 set,红黑树结构保持有序性。 |
| 时间复杂度 | 插入、删除、查找均为 O ( log n ) O(\log n) O(logn),依赖树高度。 | 与 set 相同, O ( log n ) O(\log n) O(logn) 操作。 |
| 成员函数示例 | count(key) 仅返回 0 或 1;erase(key) 删除所有匹配项(实际最多 1 个)。 |
count(key) 返回重复元素数量;erase(key) 删除所有匹配值的元素。 |
| 迭代器稳定性 | 插入或删除非当前元素时,迭代器不失效。 | 同 set,除非删除当前迭代器指向的元素。 |
| 典型应用场景 | 需要快速查找且数据唯一的场景(如黑名单)。 | 允许重复的排序数据(如成绩排名统计)。 |
MSTcheng 始终坚持用直观图解 + 实战代码,把复杂技术拆解得明明白白!
👁️ 【关注】 看普通程序员如何用实用派思路搞定复杂需求
👍【点赞】 给 "不搞虚的" 技术分享多份认可
🔖 【收藏】 把这些 "好用又好懂" 的干货技巧存进你的知识库
💬 【评论】 来唠唠 ------ 你踩过最 "离谱" 的技术坑是啥?
🔄 【转发】把实用技术干货分享给身边有需要的程序员伙伴
技术从无唯一解,让我们一起用最接地气的方式,写出最扎实的代码! 🚀💻