C++知识整理day12——set容器和map容器
文章目录
- 1.前言
- 2.set的使用
- 2.1 set底层的声明和定义
- 2.2 set的构造函数
- 2.3 set的迭代器
- 2.3 set的增删查
- 2.4 set的lower_bound、upper_bound、equal_range
- 2.6 set例题1
- 2.7 set例题2
- 3.map的使用
- 3.1 map底层的声明和定义
- 3.2 pair类型的介绍
- 3.3 map的构造函数
- 3.4 set的增删改
- 3.5 map的[]运算符重载(超级重要)
- 3.6 operator[]是怎么实现的?(面试题)
1.前言
我们先了解一下什么是序列式容器和关联式容器:
前⾯我们已经接触过STL中的部分容器如:string、vector、list、deque、array、forward_list等,这些容器统称为序列式容器,因为逻辑结构为线性序列的数据结构,两个位置存储的值之间⼀般没有紧密的关联关系,⽐如交换⼀下,他依旧是序列式容器。顺序容器中的元素是按他们在容器中的存储位置来顺序保存和访问的。
关联式容器也是⽤来存储数据的,与序列式容器不同的是,关联式容器逻辑结构通常是⾮线性结构,两个位置有紧密的关联关系,交换⼀下,他的存储结构就被破坏了。顺序容器中的元素是按关键字来保存和访问的。关联式容器有map/set系列和unordered_map/unordered_set系列。
本章节讲解的map和set像二叉搜索树一样的结构,底层是红⿊树,红⿊树是⼀颗平衡⼆叉搜索树。set是key搜索场景的结构,map是key/value搜索场景的结构。
2.set的使用
2.1 set底层的声明和定义
template < class T,
class Compare = less<T>,
class Alloc = allocator<T>
> class set;
- T就是set底层关键字的类型,是个模板类型(第一个参数)
- set默认要求T支持小于比较的,如果想实现大于比较的话,自己传入适当的仿函数可以进行修改(第二个参数)
- set底层存储数据的内存是从空间适配器申请的,如果需要可以自己实现内存池(第三个参数),但是一般是不需要传的。
- set底层是用红黑树实现的,之后的章节我们会介绍,他的增删查效率是O(logN),迭代器遍历默认是搜索树的中序,所以它是有序的。
2.2 set的构造函数
最常用的几种方式:
2.3 set的迭代器
set的迭代器是双向迭代器,知道这个我们就能知道他不可以使用算法库里的sort这种只能支持随机迭代器容器使用。
set的迭代器与其他容器一样还是有那六个:
2.3 set的增删查
-
增:insert,既可以插入一个值,也可以插入一个初始化列表的形式,还可以插入另一个set,也能插入一段迭代器区间。
-
删:erase,
-
查:find,查找val值,如果没有找到会返回迭代器的end
补充:对于set容器的查找,我们还可以通过count来查找,如果count大于0,说明能找到,为0说明找不到。对于set而言,只有0和1的取值,对于mutiset来说,它允许存在相同的值,所以说count的值可能会大于1.
2.4 set的lower_bound、upper_bound、equal_range
现在有一种情况,我们想要找到删除set容器中大于等于3的值,并且小于等于5的值。
lower_bound就是找到大于等于val值的第一个数。
upper_bound就是找到大于val值的第一个数。
特别注意,C++中的区间一般都是左闭右开的。
equal_range的返回值是一个pair元组类型,若元素存在,first指向该元素,second指向其下一个位置。若元素不存在,first和second相等,指向该值应插入的位置。
equal_range既可以实现查找的功能,也可以实现删除一个数的功能。在mutiset用的较多。
#include <set>
#include <iostream>
using namespace std;
int main() {
set<int> s = {1, 2, 4, 5, 6};
// 查找不存在的元素3
auto p1 = s.equal_range(3);
if (p1.first == p1.second) {
cout << "3 not found" << endl; // 输出结果
}
// 查找存在的元素4
auto p2 = s.equal_range(4);
if (p2.first != p2.second) {
cout << "Found: " << *p2.first << endl; // 输出4
}
// 插入元素3(利用equal_range确定位置以提高效率)
s.insert(p1.first, 3); // 插入到正确位置
cout << "After insertion: ";
for (int x : s) cout << x << " "; // 输出1 2 3 4 5 6
return 0;
}
像clear、swap、size、empty这样的接口就不介绍了。
2.6 set例题1
两个数组的交集
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
set<int> s1(nums1.begin(), nums1.end());
set<int> s2(nums2.begin(), nums2.end());
vector<int> ret;
auto it1 = s1.begin();
auto it2 = s2.begin();
while(it1 != s1.end() && it2 != s2.end())
{
if(*it1 == *it2)
{
ret.push_back(*it1);
it1++, it2++;
}
else
{
if(*it1 < *it2) it1++;
else it2++;
}
}
return ret;
}
};
【思想】类似于双指针的思想,我们先把两个数组各自送达set中,可以帮我们实现去重+排序的作用,然后定义两个变量分别指向这两个map,判断指向的两个数,要是相等,就加入结果中,不相等就让小的那个往后移一位,直到有一个数组遍历完。
【补充】对于两个数组求并集更简单,直接把两个数组丢到一个set容器中即可。对于两个数组求差集:和上面就交集的思想类似,还是双指针思想,要是指向的两个数相等,则不是差集,同时++,若不相等,小的放入结果中,然后小的++。
2.7 set例题2
环形链表II
数据结构初阶阶段,我们通过证明⼀个指针从头开始⾛⼀个指针从相遇点开始⾛,会在⼊⼝点相遇,理解证明都会很⿇烦。这⾥我们使⽤set查找记录解决⾮常简单⽅便,这⾥体现了set在解决⼀些问题时的价值,完全是降维打击。
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
set<ListNode*> s;
ListNode* cur = head;
while(cur != NULL)
{
if(s.count(cur)) return cur;
else s.insert(cur);
cur = cur->next;
}
return nullptr;
}
};
我们只需要把遍历过的指针地址放到set中去,若有重复了则存在环形。
3.map的使用
3.1 map底层的声明和定义
map的声明如下,Key就是map底层关键字的类型,T是map底层value的类型,set默认要求Key⽀持⼩于⽐较,如果不⽀持或者需要的话可以⾃⾏实现仿函数传给第⼆个模版参数,map底层存储数据的内存是从空间配置器申请的。⼀般情况下,我们都不需要传后两个模版参数。map底层是⽤红⿊树实现,增删查改效率是 O(logN) ,迭代器遍历是⾛的中序,所以是按key有序顺序遍历的。
template < class Key,
class T,
class Compare = less<Key>,
class Alloc = allocator<pair<const Key, Y>>
> class map
3.2 pair类型的介绍
std::pair 是一个模板类,用于将两个值组合成一个单一对象。它提供了一种便捷的方式存储和操作两个相关联的值(例如键值对、坐标点等),是标准模板库(STL)中广泛使用的基础组件。
3.3 map的构造函数
map⽀持正向和反向迭代遍历,遍历默认按key的升序顺序,因为底层是⼆叉搜索树,迭代器遍历⾛的中序;⽀持迭代器就意味着⽀持范围for,map⽀持修改value数据,不⽀持修改key数据,修改关键字数据,破坏了底层搜索树的结构。
为我们只需要掌握一个列表初始化的形式就可以,也支持迭代器区间的初始化,我将在下面接口中给出示例代码。
3.4 set的增删改
-
增:insert
也可以给一段迭代器的区间。 -
删:erase,与set类似,只不过是根据key值来进行删除的
-
查:find,还是与set很类似,根据key来进行查找
3.5 map的[]运算符重载(超级重要)
虽然map可以像数组那样通过下标访问,但是这里的下标指的是key,map的迭代器并不是随机迭代器,而是一个双向迭代器。
[]可以有这么多的用法,非常方便。
像map的迭代器、size、empty、clear、swap、lower_bound、upper_bound、equal_range这样的接口就不在介绍了。
3.6 operator[]是怎么实现的?(面试题)
先给结论,[]是通过insert函数实现的。
对于[],要是找的key值不存在,就会插入一个key的值。
我们先看看insert接口的返回值是什么:
我们再来对比上面的[]等价的形式:
调用insert,若是map里面不存在key则插入key,first指向的是新插入key位置的迭代器;若map中已经存在key,则insert返回值中的first指向的就是原本key的位置的迭代器,在解引用就可以访问了。