✨前言:本文中会对
map、set、multiset、multimap的常用接口进行介绍,对于map中operator[]运算符重载的原理进行了讲解,其中map和set介绍较为详细,对关联式容器,键值对的概念及使用也做了介绍.
🌿1. 关联式容器
根据"数据在容器中的排列"特性,STL容器可分为序列式(sequence)和关联式(associative)两种,比如vector、list、deque、forward_list(C++11)等,这些容器统称为序列式容器,因为其底层为线性的数据结构,里面存储的是元素本身.那什么是关联式容器,它与序列式容器有什么区别?
关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是<key,value>的键值对,在数据检索时比序列式容器效率更高.
🥦2. 键值对
键值对(pair)是用来表示具有一一对应关系的一种结构,该结构中一般只****包含两个成员变量key和value,key代表键值,value表示与key对应的信息.
比如:我们现在要建立一个英汉互译的字典,那该字典中就必须有英文单词与其对应的中文含义,而且,英文单词与其中文含义是一一对应的关系,即通过该单词,在字典中就能找到与其对应的中文含义.
这是SGI-STL中对于pair的定义

键值对的使用:
void Test()
{
pair<string, string> pr("moon", "月亮");
//first表示key值,second表示value
cout << pr.first << ":" << pr.second << endl;
}
🌳3. 树形结构的关联式容器
根据应用场景的不同,STL总共实现了两种不同结构的关联式容器:树型结构和哈希结构,树型结构的关联式容器主要有四种:map、set、multimap、multiset,其中multimap、multiset是根据map和set衍生出来的.
这四种容器共同的特点是:都使用红黑树作为其底层结构.
此外,SGI STL还提供了一个不在标准规格之列的哈希结构的关联式容器:hash_table(散列表),以及以此hash table为底层实现机制而完成的hash_set(散列集合)、hash_map(散列映射表)、hash_multiset(散列多键集合)、hash_multimap(散列多键映射表).
🍁4. set
📗4.1 set的介绍
首先来看一下set的一些介绍及特性:
set是按照一定次序存储元素的容器,所有元素都会根据元素的键值自动被排序set的元素不像map那样可以同时拥有实值(key)和键值(value),set元素的键值就是实值,实值就是键值,并且set不允许两个元素有相同的键值.set中的元素不能修改,但可以进行插入或删除.set的底层是用红黑树实现的set拥有与list相同的某些性质:当客户端对它进行元素新增操作(insert)或删除(erase)操作时,操作之前的所有迭代器,在操作完成后都依然有效.
那么为什么set中的元素不允许修改呢?
我们先来看一看SGI STL源码中的定义:
typedef typename rep_type::const_iterator iterator;
这里的rep_type是这样定义的:
typedef rb_tree<key_type, value_type, identity<value_type>, key_compare, Alloc> rep_type;
其实rep_type也就是红黑树,这样也就验证了底层是红黑树的说法,那么其实,set所开放的各种操作接口,RB-tree也都提供了,所以几乎所有的set操作行为,都只是在转调RB-tree的接口而已.
对于set中的元素能不能修改,我们来看看set的迭代器:
typedef typename rep_type::const_iterator iterator;
也就是说,set的迭代器其实是复用了RB-tree的const_iterator,所以set中不能修改元素值.
- 与
map/multimap不同,map/multimap中存储的是真正的键值对<key, value>,set中只放value,但在底层实际存放的是由<value, value>构成的键值对。set中插入元素时,只需要插入value即可,不需要构造键值对。set中的元素不可以重复(因此可以使用set进行去重)。- 使用
set的迭代器遍历set中的元素,可以得到有序序列set中的元素默认按照小于来比较set中查找某个元素,时间复杂度为:logNset中的元素不允许修改set中的底层使用二叉搜索树(红黑树)来实现
📗4.2 set的使用
📖4.2.1 set的模板参数列表

T: set中存放元素的类型,实际在底层存储<value, value>的键值对。
Compare:set中元素默认按照小于来比较
Alloc:set中元素空间的管理方式,使用STL提供的空间配置器管理
📖4.2.2 set的构造

set (const Compare& comp = Compare(), const Allocator& =Allocator() );
默认构造(空构造):构造一个空的set
注意:这里的key_compare和allocator_type是因为在STL源码中进行了typedef.

set (InputIterator first, InputIterator last, const Compare&comp = Compare(), const Allocator& = Allocator() );
用迭代器区间[first,last)进行构造.
set ( const set<Key,Compare,Allocator>& x);
是set的拷贝构造.
📖4.2.3 set的迭代器

set的迭代器与其他容器的迭代器种类类似,有正向迭代器,反向迭代器,const迭代器,这里不做太多介绍,下面会写代码演示.
📖4.2.4 set的容量

set::empty:判断set是否为空,是则返回true,反之返回false.

set::size:返回set中有效元素的个数

set::max_size:返回set中能容纳最大元素的个数
📖4.2.5 set的修改操作

这里主要介绍insert和erase,clear和swap操作比较简单,读者可自行了解.
set::insert:

pair<iterator,bool> insert (const value_type& x )
功能:在set中插入元素val,实际插入的是<val, val>构成的键值对,如果插入成功,返回<该元素在set中的位置,true>,如果插入失败,说明x在set中已经存在,返回<x在set中的位置,false>
iterator insert (iterator position, const value_type& val);
功能:在position位置插入val,返回新插入元素的位置.
void insert (InputIterator first, InputIterator last);
功能:插入迭代器区间[first,last)的值.
set::erase:

void erase (iterator position);
功能:删除set中position位置上的元素
size_type erase (const value_type& val);
功能:删除set中值为val的元素,返回删除的元素的个数
void erase (iterator first, iterator last);
功能:删除set中[first, last)区间中的元素
📖4.2.6 set的其他操作

这里主要介绍find、count、lower_bound、upper_bound这四个接口.
set::find:
iterator find (const value_type& val) const;
功能:在set中查找val,如果找到,返回val所在的位置,反之返回end()
set::count:
size_type count (const value_type& val) const;
功能:返回set中值为val的元素的个数,这里要特别强调的是,这个函数的返回值只有两个值:0或1,因为在set中元素不允许重复,所以要么val在set中,且只有一个,要么不在,为0个.
所以在set中,count方法也可以用来判断元素是否存在.
int main()
{
set<int> s;
//如果存在返回1,不存在返回0
if (s.count(1))
{
cout << "1存在" << endl;
}
{
cout << "1不在" << endl;
}
return 0;
}
set::lower_bound:
iterator lower_bound (const value_type& val) const;
功能:lower_bound会返回>= val的最小值.
set::upper_bound:
功能:upper_bound会返回set中> val的最小值
例如,对于这样一个0,1,2,3,4,5,6
如果调用s.lower_bound(3),就会返回3
如果调用s.upper_bound(3),就会返回4
使用这两个函数,我们可以实现一个删除[x,y],闭区间的值.
void Test()
{
set<int> s;
s.insert(5);
s.insert(0);
s.insert(3);
s.insert(1);
s.insert(4);
s.insert(6);
s.insert(2);
s.insert(7);
//删除[x,y]区域的值
int x, y;
cin >> x >> y;
auto left = s.lower_bound(x);
auto right = s.upper_bound(y);
//由于erase方法删除的是[first,end)前闭后开,而upper_bound方法正好返回比y大的那个值,
//保证了将[x,y]区间内的值删掉
s.erase(left, right);
}
然后,对于上述set的接口,我们来测试一下:
void Test01()
{
set<int> s;
s.insert(5);
s.insert(0);
s.insert(3);
s.insert(1);
s.insert(4);
s.insert(6);
s.insert(2);
s.insert(7);
s.insert(2); //在这里我们重复插入了一个2,但输出时,set中只有一个2,验证set具有去重功能.
cout << "删除前:" << endl;
//用范围for遍历set -> 遍历结果有序
for (const auto& e : s)
{
cout << e << " ";
}
cout << endl;
cout << "共插入"<<s.size()<<"个元素" << endl;
if (!s.empty())
{
cout << "set不为空" << endl;
}
else
{
cout << "set为空" << endl;
}
set<int>::iterator pos = s.find(3);
if (pos != s.end())
{
s.erase(pos);
}
cout<<"删除后:"<<endl;
//用迭代器遍历set
set<int>::iterator it = s.begin();
while (it != s.end())
{
//set不允许修改key
//*it = 10; -> 报错
cout << *it << " ";
++it;
}
cout << endl;
}
运行结果如下:

🍀5. map
📙5.1 map的介绍

这是文档中对于map的介绍,其中主要介绍这么几点:
map是关联式容器,它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元素。- 在
map中,键值key通常用于排序和惟一地标识元素,而值value中存储与此键值key关联的内容。键值key和值value的类型可能不同,并且在map的内部,key与value通过成员类型value_type绑定在一起,为其取别名称为pair:typedef pair value_type;- 在内部,
map中的元素总是按照键值key进行比较排序的。map中通过键值访问单个元素的速度通常比unordered_map容器慢,但map允许根据顺序对元素进行直接迭代(即对map中的元素进行迭代时,可以得到一个有序的序列)map支持下标访问符,即在[]中放入key,就可以找到与key对应的valuemap通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树))
注意:在
map中,键值key通常用于排序和惟一地标识元素,所以在进行insert、find、erase等这些操作时,操作对象一般都是key.
📙5.2 map的使用
📖5.2.1 map的模板参数

Key: 键值对中key的类型
T: 键值对中value的类型
Compare: 比较器的类型,map中的元素是按照key来比较的,缺省情况下按照小于来比较,一般情况
下(内置类型元素)该参数不需要传递,如果无法比较时(自定义类型),需要用户自己显式传递比较规则
(一般情况下按照函数指针或者仿函数来传递)
Alloc:通过空间配置器来申请底层空间,不需要用户传递,除非用户不想使用标准库提供的空间配置器
注意:在使用map时,需要包含头文件<map>
📖5.2.2 map的构造

默认构造(空构造):
explicit map (const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());
其中的key_compare和allocator_type为比较器和空间配置器,不需要我们自己传递.
功能:返回一个空的map
迭代器区间构造:
map (InputIterator first, InputIterator last,const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());
拷贝构造:
map (const map& x);
这两种构造与上面介绍的set类似,不再细说.
📖5.2.3 map的迭代器

📖5.2.4 map的元素与容量访问

这三个接口与上面set类似,可以看set中的叙述.
📖5.2.5 详解map的operator[]运算符重载
这里着重来看这样一个接口:

也就是map中重载了[]运算符,那么这个运算符如何使用呢?
假如,我们现在有一些水果,需要统计各个水果的个数,就可以使用这样一段代码:
#include<iostream>
#include<map>
using namespace std;
int main()
{
string str[] = { "香蕉", "苹果", "梨", "西瓜", "葡萄", "苹果", "西瓜", "香蕉", "香蕉" };
map<string, int> countMap;
for (const auto& e : str)
{
countMap[e]++;
}
for (const auto& m : countMap)
{
cout << m.first << ": " << m.second << "个" << " ";
}
cout << endl;
return 0;
}
运行结果:

我们会发现,我们只使用了countMap[e]++这样一个操作就完成了这个需求,那为什么它可以这样做呢?

这条语句便是operator[]的实现,但这句代码非常不直观,所以我们将它分解一下:

key_type表示的是map中键值对<key,value>中key的类型,mapped_type表示的是map中键值对<key,value>中value的类型.
所以这个函数的功能是,如果传入的k在map中还不存在,那就在map中插入以这个k值为key构造的键值对pair,然后返回这个键值对的value值,如果已经存在,返回的是已经存在的以k为键值的pair的位置.
所以对于countMap[e]++;:如果key存在,就++value,如果不存在,创建key然后++value.
如果只写成countMap[e],那就只插入,不修改.
📖5.2.6 map的修改操作

map的修改操作接口与set基本没有差别,唯一区别就是map插入的是pair键值对,所以这里对于其他接口不再细说,只说一下map的插入:

对于map的插入操作,首先,我们需要构建一个pair键值对,然后插入到map中,那么构造键值对除了我们在上面讲的方法之外,还有一种是使用pair中的一个方法:

由于函数模板可以进行类型自动推演,所以我们使用这个函数,就不用再显式的写出参数,只需要写我们要插入的值即可:
具体用法如下:
//创建一个map
map<string, string> dict;
//使用先构建pair对象,然后插入
dict.insert(pair<string, string>("moon","月亮"));
//使用make_pair函数
dict.insert(make_pair("moon", "月亮"));
map的一些其他接口:

这些接口也参考set中的讲解.
map的测试:
void Test02()
{
map<string, string> dict;
dict.insert(make_pair("moon", "月亮"));
dict.insert(make_pair("color", "颜色"));
dict.insert(make_pair("bottle", "瓶子"));
dict.insert(make_pair("paper", "纸"));
dict.insert(make_pair("seawater", "海水"));
dict.insert(make_pair("plank", "木板"));
string str;
while (cin >> str)
{
map<string, string>::iterator it = dict.find(str);
if (it != dict.end())
{
cout <<it->first<<":"<< it->second << endl;
}
else
{
cout << "未查询到此单词" << endl;
}
}
cout<<"删除前: "<<endl;
for (const auto& e : dict)
{
cout << e.first << ":" << e.second << endl;
}
//删除key为"moon"的元素
dict.erase("moon");
cout<<"删除后: "<<endl;
for (const auto& e : dict)
{
cout << e.first << ":" << e.second << endl;
}
}
运行结果:

同时可以观察到,map打印的结果是按照key排序的
🍃6. multiset
对于multiset的介绍,由于它与set太过类似,所以这里只是简单说下它们之间的区别.

对于multiset,它是对set可以插入重复元素后的衍生版本,所以它与set的唯一区别就是它允许插入重复值,也就是说它只能排序,不能去重.

还有对于count这个接口,在set中,它只会返回0或1,但在multiset中,允许插入重复元素,所以它的返回值就要根据具体的元素个数而定.

对于erase这个接口,在set中调用,会将set中存在的唯一的那个value删掉,但如果在multiset中调用,它会删除所有的val值.
对于其他的接口以及性质,multiset与set并无区别,大家可以参考set中的去使用.
multiset的底层也是通过红黑树实现的.
注意:使用
multiset时不需要重新包含头文件,包含头文件set即可
简单测试multiset:
void Test()
{
int arr[] = { 0, 0, 0, 1, 2, 3, 4, 5 };
multiset<int> s;
for (auto& e : arr)
{
s.insert(e);
}
for (auto& t : s)
{
cout << t << " ";
}
cout << endl;
}

🌴7. multimap
对于multimap的介绍,由于它与map太过类似,所以这里只是简单说下它们之间的区别.

multimap和map的唯一不同就是:map中的key是唯一的,而multimap中key是可以重复的.
在multimap中,也有各别接口稍有差别,参考multiset中的描述.
这里只有一点:在multimap中,没有重载operator[],为什么?
我们刚才在map说过,operator[]的功能是:如果key不存在,就用key创建pair并插入,如果存在,就返回当前已经存在的元素位置, 可是在multimap中,是允许插入重复key的,如果已经存在多个key,然后再次插入相同的key值,那应该返回哪一个呢?所以这个接口也不能再对单个元素实现计数功能,也就显得没有意义.
对于其他接口的使用,参考map中的讲解.
1809





