【C++】STL之set

1.序列式容器与关联式容器

在深入 set 之前,我们先搞清楚 STL 容器大家族里的两大派系。

序列式容器

我们已经学过的 string、vector、list、deque、array、forward_list 等,都属于序列式容器。你可以把它们想象成一排储物柜,每个元素按照插入的顺序占据一个位置。比如你依次存入 A、B、C,那它们在内存(或逻辑上)就按照 A->B->C 的顺序排列。即使你把 A 和 B 交换一下位置,容器本身并不会因此"坏掉",依然是一个合法的序列。所以,序列式容器中元素之间的关联很松散,主要靠存储位置来组织和访问。

关联式容器

而关联式容器则完全不一样。它的逻辑结构通常是非线性的(比如树形结构),元素之间存在着某种"紧密的关联关系"------这种关系由 关键字(key) 来维持。如果你胡乱交换两个元素的位置,这个容器的结构就会被破坏因为它不是靠位置来记忆元素的,而是靠关键字和它们之间的排序规则。

常见的关联式容器分为两大类:

有序关联容器:map / set 系列,底层基于红黑树(一种平衡二叉搜索树),元素按照键值自动排序。

无序关联容器:unordered_map / unordered_set 系列,底层基于哈希表,元素位置由哈希值决定,不保证顺序。

本章节讲解的map和set底层是红⿊树,红⿊树是⼀颗平衡⼆叉搜索树。set是key搜索场景的结构,map是key/value搜索场景的结构。

2.set系列的使用

2.1set和multiset参考文档

2.2set类 的介绍

cpp 复制代码
template<class T,//关键字类型
class Compare=std::less<T>,//比较函数对象
class Alloc=std::allocator<T>//空间配置器(内存管理)
>class set;

●set的声明如下,T就是set底层关键字的类型(key)

●set默认要求支持小于比较,若不支持或者想按自己的需求走可以自行实现仿函数传给第二模板参数

●set底层存储数据的内存 是从空间配置器申请的,若需要可以自己实现内存池,传给第三个参数。

●一般情况下,我们都不需要传后两个模板参数。

●set底层是用红黑树实现,增删查效率是O(logN),迭代器遍历是走的搜索树的中序,所以是有序的。

2.3构造与迭代器

set的构造我们关注以下⼏个接⼝即可。

set的⽀持正向和反向迭代遍历,遍历默认按升序顺序,因为底层是⼆叉搜索树,迭代器遍历⾛的中序;⽀持迭代器就意味着⽀持范围for,set的iterator和const_iterator都不⽀持迭代器修改数据,修改关键字数据,破坏了底层搜索树的结构。

cpp 复制代码
1// empty (1) ⽆参默认构造
2 explicit set (const key_compare& comp = key_compare(),
3 const allocator_type& alloc = allocator_type());
4 // range (2) 迭代器区间构造
5 template <class InputIterator>
6 set (InputIterator first, InputIterator last,
7 const key_compare& comp = key_compare(),
8 const allocator_type& = allocator_type());
9
10 // copy (3) 拷⻉构造
11 set (const set& x);
12
13 // initializer list (5) initializer 列表构造
14 set (initializer_list<value_type> il,
15 const key_compare& comp = key_compare(),
16 const allocator_type& alloc = allocator_type());
17//例如
	set<int> s1;
	set<int> s2={1,2,3,4,5};
	set<int> s3(s2.begin(),s2.end());
18 // 迭代器是⼀个双向迭代器
19 iterator -> a bidirectional iterator to const value_type
20
21 // 正向迭代器
22 iterator begin();
23 iterator end();
24 // 反向迭代器
25 reverse_iterator rbegin();
26 reverse_iterator rend();

2.4增删查核心操作

cpp 复制代码
1 Member types
2 key_type -> The first template parameter (T)
3 value_type -> The first template parameter (T)
4
5 // 单个数据插⼊,如果已经存在则插⼊失败
6 pair<iterator,bool> insert (const value_type& val);
7 // 列表插⼊,已经在容器中存在的值不会插⼊
8 void insert (initializer_list<value_type> il);
9 // 迭代器区间插⼊,已经在容器中存在的值不会插⼊
10 template <class InputIterator>
11 void insert (InputIterator first, InputIterator last);
12
13 // 查找val,返回val所在的迭代器,没有找到返回end()
14 iterator find (const value_type& val);
15 // 查找val,返回Val的个数
16 size_type count (const value_type& val) const;
17 // 删除⼀个迭代器位置的值,返回被删除元素的下一个迭代器
18 iterator erase (const_iterator position);
19 // 删除val,val不存在返回0,存在返回1(返回实际删除的元素个数)
20 size_type erase (const value_type& val);
21 // 删除⼀段迭代器区间的值[first,last),返回last
22 iterator erase (const_iterator first, const_iterator last);
23
24 // 返回⼤于等val位置的迭代器
25 iterator lower_bound (const value_type& val) const;
26 // 返回⼤于val位置的迭代器
27 iterator upper_bound (const value_type& val) const;

2.5insert插入的使用样例

cpp 复制代码
1 #include<iostream>
2 #include<set>
3 using namespace std;
4
5 int main()
6 {
7 // 去重+升序排序
8 set<int> s;
9 // 去重+降序排序(给⼀个⼤于的仿函数)
10 //set<int, greater<int>> s;
11 s.insert(5);
12 s.insert(2);
13 s.insert(7);
14 s.insert(5);
15
16 //set<int>::iterator it = s.begin();
17 auto it = s.begin();
18 while (it != s.end())
19 {
20 // error C3892: "it": 不能给常量赋值
21 // *it = 1;
22 cout << *it << " ";
23 ++it;
24 }
25 cout << endl;
26
27 // 插⼊⼀段initializer_list列表值,已经存在的值插⼊失败
28 s.insert({ 2,8,3,9 });
29 for (auto e : s)
30 {
31 cout << e << " ";
32 }
33 cout << endl;
34
35 set<string> strset = { "sort", "insert", "add" };
36 // 遍历string⽐较ascll码⼤⼩顺序遍历的
37 for (auto& e : strset)
38 {
39 cout << e << " ";
40 }
41 cout << endl;
42 }

插入:pair的第一个成员是迭代器指向刚插入的那个新元素,如果元素已经存在就指向已存在的元素。第二个成员是bool,表示是否插入成功。

2.6find和erase的使用样例

cpp 复制代码
1 #include<iostream>
2 #include<set>
3 using namespace std;
4
5 int main()
6 {
7 set<int> s = { 4,2,7,2,8,5,9 };
8 for (auto e : s)
9 {
10 cout << e << " ";
11 }
12 cout << endl;
13
14 // 删除最⼩值
15 s.erase(s.begin());
16 for (auto e : s)
17 {
18 cout << e << " ";
19 }
20 cout << endl;
21
22 // 直接删除x
23 int x;
24 cin >> x;
25 int num = s.erase(x);
26 if (num == 0)
27 {
28 cout << x << "不存在!" << endl;
29 }
30
31 for (auto e : s)
32 {
33 cout << e << " ";
34 }
35 cout << endl;
36
37 // 直接查找在利⽤迭代器删除x
38 cin >> x;
39 auto pos = s.find(x);
40 if (pos != s.end())
41 {
42 s.erase(pos);
43 }
44 else
45 {
46 cout << x << "不存在!" << endl;
47 }
48
49 for (auto e : s)
50 {
51 cout << e << " ";
52 }
53 cout << endl;
54
55 // 算法库的查找 O(N)
56 auto pos1 = find(s.begin(), s.end(), x);
57 // set⾃⾝实现的查找 O(logN)
58 auto pos2 = s.find(x);
59
60 // 利⽤count间接实现快速查找
61 cin >> x;
62 if (s.count(x))
63 {
64 cout << x << "在!" << endl;
65 }
66 else
67 {
68 cout << x << "不存在!" << endl;
69 }
70
71 return 0;
72 }

查找:此处的find的复杂度是O(logN),远比算法库里通用的std::find(复杂度O(N))高效。永远优先使用set自己的find

count对于set来说,通常用于检查某个元素是否存在。

删除:使用迭代器删除,要确保迭代器有效而且删除后该迭代器会失效,不能再使用。

2.7边界查找lower_bound与upper_bound

cpp 复制代码
1 #include<iostream>
2 #include<set>
3 using namespace std;
4
5 int main()
6 {
7 std::set<int> myset;
8 for (int i = 1; i < 10; i++)
9 myset.insert(i * 10); // 10 20 30 40 50 60 70 80 90
10
11 for (auto e : myset)
12 {
13 cout << e << " ";
14 }
15 cout << endl;
16
17 // 实现查找到的[itlow,itup)包含[30, 60]区间
18
19 // 返回 >= 30
20 auto itlow = myset.lower_bound(30);
21 // 返回 > 60
22 auto itup = myset.upper_bound(60);
23
24 // 删除这段区间的值
25 myset.erase(itlow, itup);
26
27 for (auto e : myset)
28 {
29 cout << e << " ";
30 }
31 cout << endl;
32
33 return 0;
34 }

lower_bound和upper_bound加起来可以精确划定一个范围:lower_bund(a),upper_bound(B))包含所有\[a,b的元素。可以利用这个区间进行批量删除或遍历。

3.multiset

multiset和set的使⽤基本完全类似,主要区别点在于multiset⽀持值冗余,那么insert/find/count/erase都围绕着⽀持值冗余有所差异,具体参看下⾯的样例代码理解。

cpp 复制代码
1 #include<iostream>
2 #include<set>
3 using namespace std;
4
5 int main()
6 {
7 // 相⽐set不同的是,multiset是排序,但是不去重
8 multiset<int> s = { 4,2,7,2,4,8,4,5,4,9 };
9 auto it = s.begin();
10 while (it != s.end())
11 {
12 cout << *it << " ";
13 ++it;
14 }
15 cout << endl;
16
17 // 相⽐set不同的是,x可能会存在多个,find查找中序的第⼀个
18 int x;
19 cin >> x;
20 auto pos = s.find(x);
21 while (pos != s.end() && *pos == x)
22 {
23 cout << *pos << " ";
24 ++pos;
25 }
26 cout << endl;
27
28 // 相⽐set不同的是,count会返回x的实际个数
29 cout << s.count(x) << endl;
30
31 // 相⽐set不同的是,erase给值时会删除所有的x,十分危险,如果只想删除一个,必须通过迭代器s.erase(pos)精确删除
32 s.erase(x);
33 for (auto e : s)
34 {
35 cout << e << " ";
36 }
37 cout << endl;
38
39 return 0;
40 }

常见误区

绝不能修改元素

  *it = new_value 在 set 中是行不通的。如果你需要修改一个元素,只能先 erase 旧值,再 insert 新值。

迭代器失效

  set 的插入操作不会使任何已有的迭代器失效;删除操作只会使指向被删除元素的迭代器失效,其它迭代器依然有效。这与 vector 有很大不同。代码中删除后继续使用 pos 是个常见的坑,务必重新赋值或提前备份。

优先使用成员函数

  set 的成员函数 find、lower_bound 等是专为树结构优化的,复杂度 O(log N)。不要图方便去用 里全局的 std::find,那会变成线性遍历。

为什么 set 没有下标操作?

  set 不是序列容器,元素的位置由排序规则决定,而不是线性索引。如果你需要"第 k 个元素",只能通过迭代器 + 循环,但那样做效率较低,通常说明 set 可能不是最适合你的结构。可以考虑在特定场景下使用 vector 并保持排序,或结合其它数据结构。

相关推荐
catchadmin2 小时前
免费可商用 PHP 管理后台 CatchAdmin V5.4.1 发布,新增 MCP 支持
开发语言·php
zhanghaha13142 小时前
C++构造函数的一些其他应用(逐词逐句保姆讲解)
c++
jimy13 小时前
c++的 Type Traits (二、更详细版本)
开发语言·c++
狗凯之家源码网3 小时前
纯 PHP+MySQL 应用市场系统源码实测!
开发语言·mysql·php
dishugj3 小时前
系统架构风险点、敏感点和权衡点的概念和区分
java·开发语言
geovindu4 小时前
rust:Memento Pattern(续)
开发语言·后端·设计模式·rust·行为模式·备忘录模
马优晨4 小时前
JAVA 中 VO 实体类是什么意思,为什么要做这个
java·开发语言·状态模式·java vo·java vo是什么
Aurorar0rua4 小时前
CS50 x 2024 Notes Python - 01
开发语言·python·学习方法
茉莉玫瑰花茶5 小时前
GO [ 标准库 ]
开发语言·后端·golang