【C++笔记】map和set的深度剖析
🔥个人主页:大白的编程日记
🔥专栏:C++笔记
文章目录
前言
哈喽,各位小伙伴大家好!上期我们讲了位图和布隆过滤器。今天我们来讲一下map和set的使用。话不多说,我们进入正题!向大厂冲锋
一.set
1.1 序列式容器和关联式容器
-
序列式容器
前面我们已经接触过STL中的部分容器如:string、vector、list、deque、array、forward_list等,这些容器统称为序列式容器,因为逻辑结构为线性序列的数据结构,两个位置存储的值之间⼀般没有紧密的关联关系,比如交换⼀下,他依旧是序列式容器。顺序容器中的元素是按他们在容器中的存储位置来顺序保存和访问的。 -
关联式容器
关联式容器也是用来存储数据的,与序列式容器不同的是,关联式容器逻辑结构通常是非线性结构,两个位置有紧密的关联关系,交换⼀下,他的存储结构就被破坏了。顺序容器中的元素是按关键字来保存和访问的。关联式容器有map/set系列和unordered_map/unordered_set系列。本章节讲解的map和set底层是红黑树,红黑树是⼀颗平衡二叉搜索树。set是key搜索场景的结构,map是key/value搜索场景的结构。
1.2 set系列的使用
set和multiset参考文档:
set和multiset参考⽂档
这里可以使用文档学习map和set.
1.3 set类的介绍
- 声明
set的声明如下,T就是set底层关键字的类型
template < class T, // set::key_type/value_type
class Compare = less<T>, // set::key_compare/value_compare
class Alloc = allocator<T> // set::allocator_type
> class set;
-
仿函数
set默认要求T支持小于比较,如果不支持或者想按自己的需求走可以自行实现仿函数传给第二个模版参数 -
空间配置器
set底层存储数据的内存是从空间配置器申请的,如果需要可以自己实现内存池,传给第三个参数。 -
模版参数
⼀般情况下,我们都不需要传后两个模版参数。 -
底层结构
set底层是用红黑树实现,增删查效率是 ,迭代器遍历是走的搜索树的中序,所以是有序的。
1.4 set的构造和迭代器
set的构造我们关注以下几个接口即可。
set的支持正向和反向迭代遍历,遍历默认按升序顺序,因为底层是二叉搜索树,迭代器遍历走的中序;支持迭代器就意味着支持范围for,set的iterator和const_iterator都不⽀持迭代器修改数据,修改关键字数据,破坏了底层搜索树的结构。
// 迭代器是⼀个双向迭代器
iterator -> a bidirectional iterator to const value_type
// 正向迭代器
iterator begin();
iterator end();
// 反向迭代器
reverse_iterator rbegin();
reverse_iterator rend();
1.5 set的增删查
-
insert
set的插入主要支持key关键字插入,迭代器位置插入,迭代器区间插入,initializer_list插入
这里有两个2会插入失败。因为set不允许介质冗余。同时插入后的数据中序遍历就是有序的。所以set有排序和去重的功能。
默认是升序。如果想控制升降序就用仿函数控制即可。
如果是降序那就是大的数在左边,小的数在右边。
中序遍历出来就是降序。
initializer_list插入。
-
修改
set不允许修改。因为set底层是红黑树,也就是二叉平衡搜索树的变形。所以修改key会破坏树的性质。
-
find
set的查找支持key关键字查找。同时底层也是利用二叉搜索树性质查找。
查找高度次。所以是O(logN).
如果找到返回该位置的迭代器。
如果没找到就返回end迭代器
这里的value是为了和map对称实际还是key。
也可以用count查找。返回key的个数。 -
erase
有三种方式:
迭代器删除,key关键字删除,迭代器区间删除。
因为默认是升序,同时迭代器走的是中序遍历。
所以直接删除begin就是最小值。
如果删除失败就返回end迭代器。
按照key关键字删除
返回删除个数也是为了和map对称。
注意因为删除后迭代器失效,所以erase返回迭代器的下一个位置。
- 迭代器失效
set删除后迭代器失效。
所以我们删除迭代器后就不要访问了。
迭代器删除
1.6 lower_bound和lower_bound
-
lower_bound
返回大于等于val位置的迭代器 -
upper_bound
返回大于val位置的迭代器。
这两个接口的作用在迭代器区间删除的时候,删除时迭代器区间默认都是左边右开
这是我们就可以用lower_bound查找左端点,x存在就是x迭代器。不在就会找到大于x的第一个迭代器位置。upper_bound查找右端点的下一个位置。就可以删除形成某段特定值的左闭右开的区间了。
底层查找也是利用二叉搜索树的性质查找。效率也很高。
1.7 multiset和set的差异
multiset和set的使用基本完全类似,主要区别点在于multiset支持值冗余,那么
insert/find/count/erase都围绕着支持值冗余有所差异,具体参看下面的样例代码理解。
#include<iostream>
#include<set>
using namespace std;
int main()
{
// 相⽐set不同的是,multiset是排序,但是不去重
multiset<int> s = {
4,2,7,2,4,8,4,5,4,9 };
auto it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl;
// 相⽐set不同的是,x可能会存在多个,find查找中序的第⼀个
int x;
cin >> x;
auto pos = s.find(x);
while (pos != s.end() && *pos == x)
{
cout << *pos << " ";
++pos;
}
cout << endl;
// 相⽐set不同的是,count会返回x的实际个数
cout << s.count(x) << endl;
// 相⽐set不同的是,erase给值时会删除所有的x
s.erase(x);
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
return 0;
}
multiset也排序但是不去重。
这里multiset的查找和删除和set重点区别一下。
-
查找
找到中序的第一个。
-
删除
如果按照key关键字删除就把所有相同值的节点都删除。
multiset和set的接口基本都一致。
1.8 set的应用
这里我们来做两个题体会一下set的使用场景。
-
题目一
两个数组的交集
-
思路分析
-
同步算法
-
代码实现
class Solution