前言
各位读者朋友们大家好!上期我们讲完了二叉搜索树这一数据结构,这一期我们来讲STL中的map和set这两大容器。这两个容器的底层是红黑树,红黑树的底层是平衡二叉搜索树。
目录
- 前言
- [一. 序列式容器和关联式容器](#一. 序列式容器和关联式容器)
- [二. set系列的使用](#二. set系列的使用)
-
- [2.1 set类的介绍](#2.1 set类的介绍)
- [2.2 set的构造和迭代器](#2.2 set的构造和迭代器)
- [2.3 set的增删查](#2.3 set的增删查)
-
- [2.3.1 set的增删查](#2.3.1 set的增删查)
- [2.3.2 [lower_bound](https://legacy.cplusplus.com/reference/set/set/lower_bound/)和[upper_bound](https://legacy.cplusplus.com/reference/set/set/upper_bound/)](#2.3.2 lower_bound和upper_bound)
- [2.3.3 迭代器失效问题](#2.3.3 迭代器失效问题)
- [2.4 multiset和set的差异](#2.4 multiset和set的差异)
- [2.5 set的OJ题](#2.5 set的OJ题)
- [三. map系列的使用](#三. map系列的使用)
-
- [3.1 map类的介绍](#3.1 map类的介绍)
- [3.2 pair类型介绍](#3.2 pair类型介绍)
- [3.3 map的构造](#3.3 map的构造)
- [3.4 map的增删查](#3.4 map的增删查)
- [3.5 map数据的修改](#3.5 map数据的修改)
- [3.6 map和multimap的区别](#3.6 map和multimap的区别)
- [3.7 map的OJ题](#3.7 map的OJ题)
- 结语
一. 序列式容器和关联式容器
前面我们已经接触过STL中的部分容器如:string、vector、list、deque、array、forward_list等,这些容器统称为序列式容器,因为逻辑结构为线性序列 的数据结构,两个位置存储的值之间一般没有紧密的关联关系,比如交换一下,它依旧是序列式容器。顺序容器中的元素是按他们在容器中的存储位置来顺序保存和访问的。
关联式容器也是用来存储数据的,与序列式容器不同的是,关联式容器逻辑结构通常是非线性结构 ,两个位置有紧密的关联关系,交换一下,它的存储结构就被破坏了。顺序容器中的元素是按关键字来保存和访问的。关联式容器有map/set系列和unordered_map/unordered_set系列。
map和set底层是红黑树,红黑树是平衡二叉搜索树。set是key搜索场景的结构,map是key/value搜索场景的结构。
二. set系列的使用
2.1 set类的介绍
set的声明如下,T就是set的底层关键字的类型
- set默认要求T支持小于比较,如果不支持或者想按自己的需求实现可以自己实现仿函数传给第二个模板参数。
- set的底层存储数据是从空间配置器申请的,如果需要可以自己实现内存池,传给第三个参数。
- 一般情况下我们不需要传第二个和第三个模板参数。
- set的底层是用红黑树实现,增删查的时间复杂度是O(logN),迭代器遍历走的是二叉搜索树的中序,所以是有序的。
2.2 set的构造和迭代器
-
无参构造
-
迭代器区间构造
这里的迭代器区间是任意类型容器的迭代器
-
拷贝构造
-
initializer_list initializer 列表构造
- set的迭代器
set的迭代器是单向迭代器,因此只支持++操作
2.3 set的增删查
2.3.1 set的增删查
- pair<iterator,bool> insert (const value_type& val)
插入单个数据,可以看出set是不允许插入相同的数据的
-
void insert (initializer_list<value_type> il)
-
template void insert (InputIterator first, InputIterator last);
迭代器区间插入
这里的迭代器也是任意容器的迭代器
-
查找val,返回val所在的迭代器,没有找到返回end()
-
因为set中不允许有重复的值,所以count的返回值是1,就说明set中存有val数据,反之没有
set的find和算法库中的find,set中的时间复杂度是O (logN),因为走的是二叉搜索树的搜索逻辑;算法库中的时间复杂度是O(N),是遍历查找
返回值是删除位置下一元素的迭代器
- 删除⼀个迭代器位置的值
iterator erase (const_iterator position);
- 删除val,val不存在返回0,存在返回1
size_type erase (const value_type& val);
- 删除⼀段迭代器区间的值
iterator erase (const_iterator first, const_iterator last);
2.3.2 lower_bound和upper_bound
这里的it1返回的是5位置的迭代器,it2返回的是6位置的迭代器,运用这两个成员函数,我们也可以删除一段数据区间,因为erase删除的区间是左闭右开的区间,所以左迭代器我们使用lower_bound函数来找,右迭代器使用upper_bound函数中找。
cpp
void test_set02()
{
set<int> st({ 1,2,3,4,5,60,20,70,100,98});
// 删除20~98的数据
//auto it1 = st.lower_bound(20);
//auto it2 = st.upper_bound(98);// 因为这个函数返回的是大于98的迭代器,所以可以将98删除
// 删除15~65的数据
auto it1 = st.lower_bound(15);
auto it2 = st.upper_bound(65);
st.erase(it1, it2);
for (auto a : st)
{
cout << a << " ";
}
cout << endl;
}
不用担心数据在set中不存在的问题,删哪段数据就相应的传哪些数据即可
2.3.3 迭代器失效问题
使用库里的erase之后更新迭代器是可以使用的
迭代器失效分为两种情况:
所以删除后的迭代器不要使用了,如果要使用,更新后再使用
2.4 multiset和set的差异
multiset和set的使用基本完全相似,主要区别在于multiset支持存储相同的值,那么insert/find/count/erase都围绕着支持存储相同的值而有所差异
-
insert
和set相比,multiset支持存储相等的值,也会排序,不会去重
-
find
find返回的是中序遍历的第一个x的迭代器,因为这样可以通过迭代器自增找到后续的值为x节点。
-
count
count返回的是在set中值为x的实际节点个数
-
erase
会删除所有值为val的节点。
2.5 set的OJ题
cpp
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
vector<int> ret;
// 将数组中的数据放入set中
set<int> s1(nums1.begin(),nums1.end());
set<int> s2(nums2.begin(),nums2.end());
// 指向起始位置的迭代器
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;
}
};
cpp
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
set<ListNode*> s;
ListNode* cur = head;
while(cur)
{
if(s.count(cur) == 1)
return cur;
s.insert(cur);
cur = cur->next;
}
return nullptr;
}
};
三. map系列的使用
3.1 map类的介绍
map的声明如下,Key就是map底层关键字的类型,T是map底层value的类型,set默认要求Key支持小于比较,如果不支持或者需要的话可以自己实现仿函数传给第二个模板参数,map的底层存储数据的内存是在空间配置器申请的。一般情况下,我们不需要传后两个模板参数。map的底层是用红黑树实现,增删查改的时间复杂度是O (logN),迭代器遍历走的是中序,所以是按Key有序顺序遍历的。
3.2 pair类型介绍
map底层的红黑树节点中的数据,使用pair<Key,T>存储键值对数据,也就是说map存的是pair,pair中又存了key和value
pair的文档
pair的底层:
cpp
typedef pair<const Key, T> value_type;
template <class T1, class T2>
struct pair
{
typedef T1 first_type;
typedef T2 second_type;
T1 first;
T2 second;
pair() // pair的构造调用了类型的默认构造,自定义类型自己的构造函数,内置类型同样
:first(T1())
,second(T2())
{}
pair(const T1& a, const T2& b)
:first(a)
,second(b)
{}
template<class U, class V>
pair(const pair<U, V>& pr)
:first(pr.first)
,second(pr.second)
{}
};
template <class T1, class T2>
inline pair<T1, T2> make_pair(T1 x, T2 y)
{
return (pair<T1, T2>(x, y));
}
3.3 map的构造
map支持正向和反向迭代遍历,遍历默认按key的升序顺序,因为底层是二叉搜索树,迭代器遍历走的是中序,支持迭代器就意味着支持范围for,map支持修改value数据,不支持修改key数据,修改关键字数据就会破坏底层搜索树的结构。
- 无参构造
- 迭代器区间构造
- 拷贝构造
3.4 map的增删查
map的增加数据的接口,插入pair键值对数据,和set不同,但是查和删的接口只用关键字key和set是相同的,不过find返回iterator,不仅仅确认key在不在,还可以找到key映射的value,同时通过迭代器还可以修改value
- 增加数据
-
单个数据插⼊,如果已经key存在则插入失败,key存在相等value不相等也会插入失败
pair<iterator,bool> insert (const value_type& val);
-
列表插入,已经在容器中存在的值不会插入
void insert (initializer_list<value_type> il);
-
迭代器区间插入
- 查找
- 查找key,返回key所在的迭代器,没有就返回end()
- 查找key,返回k的个数
size_type count (const key_type& k) const;
- 删除
- 删除⼀个迭代器位置的值
iterator erase (const_iterator position);
删除第二个位置的值 - 删除key,key存在返回0,存在返回1
size_type erase (const key_type& k);
- 删除⼀段迭代器区间的值
iterator erase (const_iterator first, const_iterator last);
3.5 map数据的修改
前面提到map支持修改mapped_type数据,不支持修改key数据,修改关键字数据,破坏了底层搜索树的结构。map第一个支持修改的方式是通过迭代器,迭代器遍历时或者find返回key所在的iterator修改,map还有一个非常重要的修改接口operator[],但是operator[]不仅仅支持修改,还支持插入数据和查找数据,所以它是⼀个多功能复合接口需要注意从内部实现角度,map这里把我们传统说的value值,给的是T类型typedef为mapped_type。而value_type是红黑树结点中存储的pair键值对值。日常使用我们还是习惯将这里的T映射值叫做value。
operator[]
cpp
// operator的内部实现
mapped_type& operator[] (const key_type& k)
{
pair<iterator, bool> ret = insert({ k, mapped_type() });
iterator it = ret.first;
return it->second;
}
正常插入是插入+修改的功能
插入失败是查找+修改的功能
返回值是节点value的引用
3.6 map和multimap的区别
multimap和map的使用基本完全类似,主要区别点在于multimap支持关键值key冗余,那么insert/find/count/erase都围绕着支持关键值key冗余有所差异,这里跟set和multiset完全⼀样,比如find时,有多个key,返回中序第一个。其次就是multimap不支持[],因为支持key冗余,[]就只能支持插入了,不能支持修改。
equal_range
返回key相等值的迭代器区间
把所有字符对应的value打印出来。
3.7 map的OJ题
cpp
class Solution {
public:
Node* copyRandomList(Node* head) {
Node* copyhead = nullptr,*copytail = nullptr;
Node* cur = head;
map<Node*,Node*> mp;
// 拷贝节点,建立映射关系
while(cur)
{
if(copyhead == nullptr)
{
copyhead = copytail = new Node(cur->val);
}
else{
copytail->next = new Node(cur->val);
copytail = copytail->next;
}
mp[cur] = copytail;
cur = cur->next;
}
// 链接random节点
cur = head;
Node* copyNode = copyhead;
while(cur)
{
if(cur->random == nullptr)
{
copyNode->random = nullptr;
}
else{
copyNode->random = mp[cur->random];
// cur->random是一个节点,在map中找这个节点的映射
// 赋给拷贝节点的random指针
}
cur = cur->next;
copyNode = copyNode->next;
}
return copyhead;
}
};
结语
以上我们就讲完了map和set的使用,希望对大家有所帮助,感谢大家的阅读,欢迎大家批评指正!