✨前言:本文中会对
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
中查找某个元素,时间复杂度为:logN
set
中的元素不允许修改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
对应的value
map
通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树))
注意:在
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中的讲解.