愿那该死的荣耀尽归先锋战队!
Hello,大家好,今天我们来讲解一下set和map这两个容器,这两个容器也是STL库中的一部分,我们现在来对其进行一番讲解,我在这里为大家推荐一个英语的相关网站,希望对大家在学习STL时能够有所帮助。
C/C++相关函数查询官网:Reference - C++ Reference
目录
1 序列式容器和关联式容器
1.1 序列式容器
诸如string,vector,list,deque,array,forward_list着类容器等,它们因为逻辑结构为线性序列的数据结构,两个位置存储的值之间没有紧密的关联关系。比如说交换两个元素,交换之后他依旧式序列式容器。
1.2 关联式容器
关联式容器也是用来存储数据的,不过与序列式容器不同的是,关联式容器逻辑结构通常是非线性结构,两个位置之间往往有比较紧密的关联关系,交换一下,它的存储结构就被破坏了。关联式容器有map / set系列和unordered_map / unordered_set系列。
2 set系列的使用
(set是前面所讲的key搜索场景的结构)set是一个类模板,它的底层是红黑树,红黑树是一棵平衡二叉搜索树,在本章节我们只讲解它的使用,暂时不讲解它的底层是如何实现的,后面的章节会讲。
2.1 set的介绍
1>.set的声明如下,T就是set底层的关键字类型:
template<class T, class Compare = less<T>, class Alloc = allocator<T>>
class set;//T是关键字类型,用来构造二叉搜索树的时候会用到;Compare是一个仿函数,默认set类的底层的那棵二叉搜索树是左边的关键字永远比根小,右边的关键字永远比根大;Alloc是一个内存池,我们现在不需要看这个参数。
2>.set类默认要求T支持小于比较,如果不支持或者作者想按自己的需求走的话我们可以自行实现仿函数传给第二个模板参数。
3>.set底层存储数据的内存是从空间配置器中申请的,如果需要我们可以自己实现内存池,传给第三个参数。
4>.一般情况下,我们都不需要去传后两个模板参数。
5>.set的底层是用红黑树实现的,增删查的效率是O(log N),(红黑树的底层其实是一棵特殊的二叉搜索树,它不同于我们前面所学的二叉搜索树,我们前面所学的那棵二叉搜索树在某种特殊情况下它的所有节点会全部集中于一端,从而导致其时间复杂度为O(N),红黑树它在这里实际上是做了优化,使得它两端的高度趋于平衡)迭代器遍历走的是二叉搜索树的中序,所以是有序的。
前面的部分我们已经学会了vector / list等容器的使用,STL中容器接口的设计是高度相似的,所以这里我们就不再一个接口一个接口地为大家去介绍了,而是挑选文档中比较重要地接口为大家进行介绍。
2.2 set的构造和迭代器
2.2.1 无参默认构造
set(const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());//comp是一个仿函数,如果我们不传的话,就会默认用less<T>这个缺省值;alloc是一个空间配置器,这里我们暂时先不管这个参数。
2.2.2 迭代器区间构造
template <class InputIterator>
set(InputIterator first, InputIterator last,const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());
用任意一个容器的一段迭代器区间进行初始化操作。
2.2.3 拷贝构造
set (const set& x);
用一个set类型的对象中的数据进行初始化操作,是深拷贝。
2.2.4 列表构造
set(initializer_list<value_type> il, const key_compare& comp = key_compare(), const allocator_type& alloc = allocator_type());
这个列表构造和我们前面在list那一章节所讲解的差不了多少,它的内部实现就是将il对象中的元素一一拿出来,并且将它们全部都insert进set类型的对象中。
2.2.5 迭代器
set的迭代器是一个双向迭代器,只支持++和--操作符,并不会支持+和-操作符。
正向迭代器:iterator begin( ); 反向迭代器:reverse_iterator rbegin( );
iterator end( ); reverse_iterator rend( );
#include<iostream>
#include<set>
#include<list>
using namespace std;
int main()
{
set<int> s1;//无参默认构造,这里我们先了解它的使用,先不去看他的底层结构。
set<int> s2({ 3,2,5,7,6,8,4,9 });//列表构造。
set<int>::iterator it1 = s2.begin();
while (it1 != s2.end())
{
cout << *it1 << " ";
++it1;
}//2 3 4 5 6 7 8 9;迭代器走的是二叉树的中序遍历,再加上二叉搜索树的特殊结构,因此输出的顺序是升序。
cout << endl;
set<int>::iterator it3 = ++s2.begin();//set类的迭代器是不支持+和-这两个操作符的。
set<int>::iterator it4 = --s2.end();
set<int> s3(it3, it4);
set<int>::iterator it5 = s3.begin();
while (it5 != s3.end())
{
cout << *it5 << " ";
++it5;
}//3 4 5 6 7 8
cout << endl;
set<int> s4(s2);
set<int>::reverse_iterator it6 = s4.rbegin();
set<int>::reverse_iterator it7 = s4.rend();
while (it6 != it7)
{
cout << *it6 << " ";
++it6;
}//9 8 7 6 5 4 3 2
return 0;
}
2.3 set的增删查
在开始介绍set相关的增删查函数之前,我们这里先来看一下set的元素类型,如果我们在这里细看文档的话,会发现文档中有2个元素类型:key_type (关键字类型) 和value_type (元素数据类型),但是我们通过文档中表格的观察,发现这两种类型它们两个是一模一样的,都是元素类型:
key_type ——> The first template parameter (T)
value_type ——> The first template parameter (T)
通过我们前一章节的解释,我们能够知晓set是属于key的搜索场景的一种结构,也就是说,它里面实际只有一个元素,可是既然如此,我们这里为何要另外设计出一个key_value呢?其实,多设计一个key_value的原因是为了在这里兼容map,map是key / value搜索场景的结构,它需要用一个关键字来构造出二叉搜索树。
2.3.1 insert函数接口
2.3.1.1 单个数据插入
pair<iterator,bool> insert (const value_type& val);
插入单个元素,只不过和前面的插入元素有所不同的是,它这里还会进行一个去重操作,就是如果插入的这个元素已经存在于对象中,就插入失败,set这个类它不支持重复元素的插入。
2.3.2.2 列表插入
void insert (initializer_list<value_type> il);
将il这个列表对象中的所有的值全部都插入到set类类型的对象中,如果要插入的值在容器中已经存在的话,就不会插入。
2.3.2.3 迭代器区间插入
template <class InputIterator>
void insert (InputIterator first, InputIterator last);
将 [ first , last) 这个迭代器区间中的所有的值都一一插入到set类类型的对象中,如果要插入的值在容器中已经存在的话,就不会插入。
int main()
{
set<int> s1({ 2,5,4,7,6,9,8,3 });
set<int>::iterator it1 = s1.begin();
while (it1 != s1.end())
{
cout << *it1 << " ";
++it1;
}//2 3 4 5 6 7 8 9
cout << endl;
s1.insert(2);//插入失败;因为s1对象中已经有2这个元素了。
set<int>::iterator it2 = s1.begin();
while (it2 != s1.end())
{
cout << *it2 << " ";
++it2;
}//2 3 4 5 6 7 8 9
cout << endl;
s1.insert(10);//插入成功。
set<int>::iterator it3 = s1.begin();
while (it3 != s1.end())
{
cout << *it3 << " ";
++it3;
}//2 3 4 5 6 7 8 9 10
cout << endl;
s1.insert({ 2,3,11 });//2和3这两个元素插入失败,因为s1对象中已经有2和3这两个元素了,11插入成功。
set<int>::iterator it4 = s1.begin();
while (it4 != s1.end())
{
cout << *it4 << " ";
++it4;
}//2 3 4 5 6 7 8 9 10 11
cout << endl;
list<int> l({ 1,3,5,7,12 });
list<int>::iterator l1 = l.begin();
list<int>::iterator l2 = l.end();
s1.insert(l1, l2);//3、5、7这几个元素插入失败,因为s1对象中已经有3、5、7这几个元素了,1和12这两个元素插入成功。
set<int>::iterator it5 = s1.begin();
while (it5 != s1.end())
{
cout << *it5 << " ";
++it5;
}//1 2 3 4 5 6 7 8 9 10 11 12
return 0;
}
2.3.2 find函数接口
2.3.2.1 查找val
iterator find (const value_type& val);
在一个set类类型的对象中去找val这个值,若找到了,就返回val所在的迭代器,若在对象中没有找到val的话,返回end( )这个迭代器。
2.3.3 count函数接口
size_type count (const value_type& val) const;
count这个函数的作用也是去对象中去寻找val这个值,它是去遍历对象中的那棵二叉树,它统计的是val这个元素在对象中的个数,但是如果我们大家仔细去想的话,set这个类是不需要count这个函数的,因为set类中没有重复的元素,之所以写这个函数,是为了兼容multiset这个类,它和set是类似的,唯一不同的就是它支持重复的元素出现,set不支持。
int main()
{
set<int> s({ 3,5,7,9,6,8 });
set<int>::iterator it = s.find(7);//返回7所在的迭代器。
if (it != s.end())
{
cout << *it << endl;//7;
}
size_t j = s.count(3);//返回的是3所在的s对象中的个数。
cout << j << endl;//1;
return 0;
}
2.3.4 erase函数接口
2.3.4.1 删除一个迭代器位置的值
iterator erase (const_iterator position);
删除position这个迭代器所指向的那个位置的值,这个函数一般用不到。
2.3.4.2 删除val
size_type erase (const value_type& val);
val如果存在,则删除成功,返回1,若val不存在,删除失败,则返回0。
2.3.4.3 删除一段迭代器区间中的所有的值
iterator erase (const_iterator first, const_iterator last);
将 [ first , last ) 这个迭代器区间中所有的值全部删除。
int main()
{
set<int> s({ 2,5,7,4,8,6 });//2 4 5 6 7 8
s.erase(6);//2 4 5 7 8;删除成功,这里我们忽略了erase函数的返回值,原因后面会讲。
set<int>::const_iterator si = ++s.cbegin();
s.erase(si);//2 5 6 7 8
set<int>::iterator it1 = ++s.begin();
set<int>::iterator it2 = --s.end();
s.erase(it1, it2);//2 8
set<int>::iterator il1 = s.begin();
while (il1 != s.end())
{
cout << *il1 << " ";
++il1;
}//2 8
//写到这里,通过我们前面对STL中的几个容器中的erase函数的讲解,我们大部分情况下,都不允许去访问删除节点后的那个迭代器了,原因是因为有极大的失败风险,就像上述si迭代器一样,它指向val为4的这个节点,通过我们前一章的讲解,如果4这个节点为根节点并且它的左右节点子树均不为空的话,那么在删除之后的si这个迭代器还是有效的,但是如果4这个节点为叶子节点的话,将这个节点删除了之后,si就指向nullptr,就不能再被访问了,这两种情况中虽然有一种情况删除之后它的迭代器还是有效的,但是我们大家不妨想一想,我们又怎么知道我们想要删除的这个节点它是根节点还是叶子节点呢?因此,为了以防万一,我们这里规定erase之后统一不再访问原来的那个迭代器了。
return 0;
}
2.3.5 lower_bouund函数接口
2.3.5.1 返回大于等于val位置的迭代器
iterator lower_bound (const value_type& val);
在一个set类型的对象中去找关键字大于等于val的节点,若找到的话,就返回指向该节点的迭代器,反之,则返回end( )。
2.3.6 upper_bound函数接口
2.3.6.1 返回大于val位置的迭代器
iterator upper_bound (const value_type& val);
在一个set类型的对象中去找关键字大于val的节点,若找到的话,就返回指向该节点的迭代器,反之,则返回end( )。
int main()
{
set<int> s;
for (int i = 1; i <= 9; i++)
{
s.insert(i * 10);//10 20 30 40 50 60 70 80 90;
}
//我想要将s对象中的数据在[30,70]这个区间的值全部删除,我们先来理一下思路:我们这里既然要删除数据在[30,70]这个区间的所有值的话,那么我们就要用erase函数去删除,由于这个是迭代器删除,迭代器删除的区间范围必须是符合半闭半开的条件,就上述[30,70]来说,我们要先找到指向30这个节点的迭代器,并且找到指向大于70的那个节点的迭代器才可以。
auto itlow = s.lower_bound(30);//itlow是指向关键字为30这个节点的迭代器。
auto itup = s.upper_bound(70);//itup是指向关键字大于70的那个节点的迭代器。
s.erase(itlow, itup);
auto it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}//10 20 80 90;
//这里我们再提醒一下就是在使用这两个函数的时候,我们要删除那个区间,就将这个区间的值放到这两个函数中去就可以了,只有这样才会彻底清除干净,否则的话会清除不干净。
return 0;
}
2.4 multiset和set的差异
multiset和set的使用基本完全类似,主要的区别在于multiset支持值冗余(重复),那么 insert / find / count / erase 都围绕着支持冗余有所差异,我们这里挑几个不同的函数来说一说就可以了,其余都和set差不多。
int main()
{
multiset<int> s({ 4,2,7,2,4,8,4,5,4,9 });
auto it1 = s.begin();
while (it1 != s.end())
{
cout << *it1 << " ";
++it1;
}//2 2 4 4 4 4 5 7 8 9;multiset是支持数据冗余的,不会在这里进行去重操作。
cout << endl;
auto pos = s.find(4);//它在s对象中去寻找关键字为4的节点的时候,它找的是中序情况下的第一个关键字为4的节点。
cout << *(--pos) << endl;//2;根据编译器的这个输出结果我们可以由此观之,find函数在这里确实是找的中序遍历的情况下的第一个关键字为4的节点。
//说到这里,可能有的同学就会在这里问了,为什么非要去找第一个关键字为4的节点呢?去找最近的那个关键字为4的节点不是更快吗?因为找第一个关键字为4的节点的话,在做某些函数操作的话就会变得很方便,比如说,我可以一下将s对象中所有关键字为4的节点全部输出来,或全部删除掉,很方便,而且不会又余留。
cout << s.count(4) << endl;//4;count在multiset中就有了很大的作用。
return 0;
}
3 map系列的使用
(map就是我们前面所讲的key / value搜索场景的结构)
map和set一样,它也是一个类模板,map的底层也是用红黑树实现的,增删查改的效率是O(log N),迭代器遍历是走的中序,所以是按照key有序顺序遍历的。
3.1 map类的介绍
map类的声明如下:key就是map底层关键字的类型(主要依靠这个关键字去构造红黑树),T是map底层value的类型。
template < class Key,class T,class Compare = less<Key>,class Alloc = allocator<pair<const Key, T>>>
class map;//key是关键字类型;T是底层value的类型;Compare和Alloc这两个参数和set类中的解释类似。
3.2 pair类型介绍
我们通过上面的介绍可知,map类它是我们前面所讲的 key / value 搜索场景的结构,也就是说,map它的每一个节点里面其实都包含了两个变量,key和value,这里编译器因为某些原因,在这里选择将这两个变量给封装起来,将它们统一放到pair这个类模板中,接下来,我们来简单地看一下pair这个类模板的底层结构。
在看底层之前,我们先来看一下元素类型都代表的是什么:
通过上述那张图片,我们在这里可以知道mapped_type就是 key / value 结构中的那个除key关键字以外的那个value变量,在map中是将key和value这两个变量给封装起来了,封装好了以后的那个类型就是map中的value。
typedef pair<const key, T> value_type;
template<class T1,T2>
class pair
{
public:
typedef T1 first_type;
typedef T2 second_type;
T1 first;
T2 second;
pair(const T1& x,const T2& y)
:first(x)//first就是key;
,second(y)//second就是key/value上述场景中的value;
{ }
//...
};
这里既然说到了pair类模板,那么我们在这里就再说一下make_pair函数模板,这个函数它的主要作用就是去构建一个pair类型的对象,然后返回。
template<class T1,class T2>
pair<T1, T2> make_pair(T1 x, T2 y)
{
return (pair<T1, T2>(x, y));//调用pair类的构造函数构造一个pair<T1,T2>类型的匿名对象,值返回。
}
3.3 map的构造和迭代器
3.3.1 无参默认构造
map (const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());//comp是一个关于关键字类型的仿函数;alloc是空间适配器。
3.3.2 迭代器区间构造
map (InputIterator first, InputIterator last,const key_compare& comp = key_compare(),const allocator_type& = allocator_type());
用任意一个容器的一段迭代器区间进行初始化操作。
3.3.3 列表构造
map (initializer_list<value_type> il,const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());
map中的构造这里其实也和set中的一样,都是调用插入函数一一往对象里面插。
3.3.4 迭代器
map的迭代器和set的迭代器一样,都是双向迭代器(从性质划分)
正向迭代器: 反向迭代器:
iterator begin( ); reverse_iterator rbegin( );
iterator end( ); reverse_iterator rend( );
int main()
{
map<string, string> dict1;//默认构造;
pair<string, string> pr1("first", "第一个");//构造一个pair<string,string>类类型的对象。
pair<string, string> pr2("second", "第二个");
map<string, string> dict2({ pr1,pr2 });//列表构造,这样构造显然有点麻烦,我们可以直接写一个匿名对象构造。
map<string, string> dict3({ {"next","下一个"},{"prev","前一个"} });//这里编译器会进行一个叫隐式类型转换的过程,通过我们前面对列表的学习,知道了它的底层其实是遍历这个列表,将其一一插入到dict3这个map<string, string>类类型的对象中,在插入的时候,会将{"next","下一个"}隐式类型转换成pair<string,string>类类型的对象,进行插入操作。
map<string, string> dict4(dict2);//拷贝构造;
map<string, string>::iterator i1 = dict3.begin();
map<string, string>::iterator i2 = dict3.end();
map<string, string> dict5(i1, i2);//迭代器区间构造;
//map<string,string> dict6<pr2>;//这样写编译器会报错,语法不支持,原因是因为少传一个参数,pair<string,string>是可以被转化为map<string,string>类型的,这是C++规定的,我们这个map类在调用构造函数时只能调用我们上面所写的那三种调用方式外加上一个拷贝构造,也就是说,语法上并不支持除这四种构造方式以外的其他方式了。
map<string, string> dict6({ {"next","下一个"} });//列表构造;
return 0;
}
3.4 map的增删查
3.4.1 insert函数接口
3.4.1.1 单个数据插入
pair<iterator,bool> insert (const value_type& val);//value_type就是pair类型,val就是pair类类型的对象,它的返回值也是一个pair类类型的对象,返回值中的iterator是指向插入的那个节点的迭代器,如果插入失败的话,就返回指向和它的关键字相同的那个节点的迭代器,bool是是否插入成功,也就是说,insert函数的内部实际还进行了查找这个操作。
如果二叉搜索树中已经存在一个节点,且这个节点中的关键字和val中的key(关键字)相同,则插入失败,就是key存在而value不相同也不会插入,也就是说,决定是否要插入的因素不是value的相同,而是key(关键字)。
3.4.1.2 列表插入
void insert (initializer_list<value_type> il);
已经在容器中存在的key值不会插入。
3.4.1.3 迭代器区间插入
template <class InputIterator>
void insert (InputIterator first, InputIterator last);
将迭代器区间在 [ first , last ) 这个范围内的pair类类型的对象全部都一一插入到所对应的那个map类类型的对象中。
int main()
{
map<string, string> m;
m.insert({ {"first","第一个"},{"second","第二个"},{"next","下一个"} });//这样写也是可以的,这样写的话,它在这里会发生一个隐式类型转换的过程,会将列表中的每一个元素全部都转换为pair类类型的对象,再进行插入。
auto set = m.insert({ "first","一" });//多参数它也支持隐式类型转换,将{ "first","一" }转换为pair<string,string>类型的对象,但是这里插入失败了,原因是key(关键字)已经存在,first这个关键字已经存在,因此这里插入失败了。
cout << set.second << endl;//0;通过输出结果来看确实是插入失败了
return 0;
}
3.4.2 find函数接口
查找K(关键字),返回K所在的迭代器,如果没有找到K的话,则返回end( )。
3.4.3 count函数接口
查找K(关键字),返回map类类型的对象中关键字为K的节点的个数。
3.4.4 erase函数接口
3.4.4.1 删除K(关键字)
size_type erase (const key_type& k);
删除map类类型的对象中关键字为k的节点,若k存在,则删除成功,返回1,反之,删除失败,返回0。
3.4.4.2 删除一个迭代器所指向得到节点
iterator erase (const_iterator position);
删除position这个迭代器所指向的那个节点(这个重载函数不常用)。
3.5 map的数据修改
OK,通过我们对set的使用部分的学习,我们知道了set类,它只能进行增删查这3种操作,它是无法进行修改这一个操作的,因为一旦将set中的某个节点所存放的值给改变的话,就有极大的可能会破坏二叉搜索出的结构,因此不可以修改。而在map中,它是允许修改的,但是这里的这个修改并不能修改key_type(关键字),只能修改mapped_type(由于在map中value_type表示的是一个pair类类型的对象,而我们平时所说的value_type表示的是key / value结构中除key以外的那个元素变量,这样容易混淆,为了方便,我们把key / value结构中除key关键字以外的那个变量,要做mapped_type),这里的修改,我们需要着重的在这里讲一下了,修改操作主要是靠函数operate[ ]实现的。
在进行讲解之前,我们需要在这里了解一个insert中重载的一个函数:
pair<iterator,bool> insert (const value_type& val);
这里我们之所以要重点看一下这个函数,其实并不是去研究它的作用,而是它的返回值,如我们所见,这个函数的返回值是一个pair类类型的对象,这个对象的第一个成员变量是一个迭代器,第二个成员变量则是一个bool类型的变量,如果插入成功的话,迭代器(第一个成员)就会指向插入成功的那个节点,第二个成员变量为true,若插入失败的话,就证明对象(map类型)中已有key_type(关键字)存在,那么就会返回已经存在的且跟key_type相等的值的迭代器,第二个成员为false,简写成如下所示:
插入成功: pair<指向新插入的那个值所在的迭代器,true>
插入失败: pair<指向已经存在的跟key相等的值所在的节点的迭代器,false>
注意:如果插入失败的话,就相当于是充当了查找的功能。
有了上述的知识储备之后,我们接下来就来看map类类型的对象是如何进行修改的,也就是接下来我们来看一下operator[ ]函数。
通过我们在讲解string,vector等等容器时,就已经讲到过,operator[ ]是通过下标去访问的,可以通过下标快速地找到我们需要的那个下标所对应的元素,这里与前面通过下标来访问元素的传统方式有所不同,在map类中,它是通过关键字来访问mapped_type的,
mapped_type& operator[] (const key_type& k);
根据它的作用以及返回值我们可以初步判断出这个函数接口它就相当于一个查找和修改的综合操作,参数是key_type,查找出它所对应的那个mapped_map,返回mapped_type的引用,可以进行修改,但是,这个函数不仅综合查找和修改这两个操作,它还包括了插入操作,因为operator[ ]它的底层就是调用insert(我们在上面着重讲解的那个返回值是一个pair类类型的对象的insert函数)函数,它的内部实现如下所示:
mapped_type& operator[](const key_type& k)
{
//1.如果k不在map中,insert会插入k和mapped_type默认值(默认构造),同时[]返回节点中存储的那个mapped_type值的引用,那么我们就可以通过引用来修改返隐射值(引用)。所以[]具备了插入+修改的功能。
//2.如果k在map中,insert会插入失败,但是insert返回的pair类类型的对象中的first迭代器是指向关键字为k的节点的迭代器,同时[]返回节点中存储的mapped_type的值的引用,所以[]具备了查找+修改的功能。
pair<iterator, bool> ret = insert({ k,mapped_type()});//通过我们前面所学的关于insert的知识可知,insert它主要是通过key_type的值去到map中找,跟mapped_type的值无关,因此我们这里使用mapped_type的默认值就可以了,因为无论mapped_type为什么值,那也用不上。
iterator it = ret.first;//得到ret对象中的第一个成员变量(迭代器)。
return it->second;//返回关键字为k的节点中的那个mapped_type值。
}
OK,既然我们了解了operator[ ]这个函数后,我们接下来来测试一下这个函数。
int main()
{
map<string, string> dict;
dict.insert(make_pair("sort", "排序"));//make_pair是一个函数模板,这个函数的作用就相当于创建一个pair类类型的对象,然后返回。(前面有讲)
//1.key若不存在,[]相当于是插入操作。
dict["insert"];//这个就是查找的写法,它不像我们传统的查找方式那样[]里面放的是下标,而是放的是key_type类型的变量,因为map的查找逻辑就是通过key_value去map里面寻找相应的节点。这一句代码就相当于是插入{"insert","string()"}。
//2.key若不存在,[]相当于是插入操作+修改操作。
dict["left"] = "左边";//这里是去dict中找关键字为"left"的节点,根据我们上面中对operator[]函数的深度剖析可知,它如果找不到的话,会构造一个关键字为left节点,构造好后就会将这个节点插入到dict中,然后根据返回值将这个节点中的mapped_value给修改成"左边"就可以了。
//3.key存在,[]相当于是执行了修改操作。
dict["left"] = "左边,剩余";//他会先在dict中去找关键字为"left"的节点,它找到了,就不会再去开创新的节点去插入,而是返回刚刚找到的这个关键字为"left"的节点的mapped_type的引用,将其返回,将这个节点中的mapped_type修改变成"左边,剩余"。
return 0;
}
3.6 multimap和map的差异
multimap和map的使用基本完全类似,主要的区别就在于multimap支持关键字key的冗余,那么insert / count / erase都围绕着支持关键字key冗余有所差异,这里跟set和multiset完全一样。比如说find,当map中有多个key时,它会返回中序的第一个关键字为key的节点的迭代器。其次就是multimap不支持[ ],因为multimap支持冗余,如果这里支持[ ]操作的话,那么使用[ ]操作就相当于是使用insert了,只能插入了,不能支持修改。
OK,既然说到这里了,那么我们在这里来看一个函数接口:equal_range函数:
pair<iterator,iterator> equal_range (const key_type& k);
这个函数它的作用是返回一段半闭半开的迭代器区间,这个区间中的所有的节点中的关键字都是相同的,也就是说这个函数,它返回的是一段拥有着相同关键字的,且为半闭半开的空间,它返回的是一个pair类类型的对象,它其中的两个成员类型均为迭代器,第一个成员是中序中的第一个关键字为k的节点,第二个成员是中序中的最后一个关键字为k的节点的下一个节点的迭代器。
int main()
{
multimap<char, int> mymm;
mymm.insert({ {'a',10},{'b',20},{'b',30},{'b',40},{'b',50} });//这一步会发生隐式类型转换操作。
auto p = mymm.equal_range('b');//auto表示的类型是pair<multimap<char,int>::iterator,multimap<char,int>::iterator>。
while (p.first != p.second)
{
cout << p.first->second << " ";
++p.first;
}//20 30 40 50
//这里我们最后再建议,就是最好还是不要去访问p.scend(就上述代码而言,在声明中指的是pair类类型的对象中的第二个成员变量),因为有出错的风险,就上面代码而言,p.second这个迭代器指向的是mymm中最后一个节点的下一个位置,相当于是end(),访问会报错,equal_range这个函数的返回值之所以是一个半闭半开的区间,因为它是按照迭代器的访问去设计的。
return 0;
}//其余函数和map中的差不多,不会的可以参考map和文档。
set和map的使用部分我们就先讲到这里了,下一章我们来讲解AVL树(这棵树和set / map的底层的那棵红黑树有很大的相似度,先学AVL树对我们后面学习红黑树有很大的帮助)。
OK,今天我们就先讲到这里了,那么,我们下一篇再见,谢谢大家的支持!