关联容器与顺序容器的本质差别在于:关联容器通过键值(key)存储和读取元素,而顺序容器则通过袁术在容器中的位置存储和访问元素。
1 关联容器类型概述
关联容器(Associative containers)支持通过键来高效地查找和读取元素。两个基本的关联容器类型是map
和 set
。标准库提供了8个关联容器:
map
类型通常被称为"关联数组"。map
的元素以键-值(key-value)对的形式组织:键用作元素在map
中的索引,而值则表示所存储和读取的数据。关联数组与普通数组类似,不同之处在于其下标不必是整数,需要通过关键值而非位置来查找对应值。set
类型是简单关键词的集合,高效存储和访问不同的关键词。set
仅包含一个键,并有效地支持关于某个键是否存在的查询。另外6个类型与
map
以及set
类型相关,multi
前缀类型支持重复的关键词,unorder
前缀类型是无序集合,用哈希函数组织。
2 关键字要求
关联容器对其关键字有一些限制:
2.1 有序容器关键字
默认情况下,标准库使用关键字类型的<运算符来比较两个关键字。用户可以提供一个自己定义的操作来替代关键字上的<,所提供的操作必须是严格弱序的。所谓严格弱序,必须遵序以下几点:
- 不能同时出现a<= b, b<=a的情况
- 如果a<=b且b<=c,那么a<=c
- 如果存在a不小于c,c不小于等于a的情况,则a,c等价。如果a,c等价,b,c等价,则a,c等价。
2.2 无序容器关键字
默认情况下,使用关键字类型==运算符来比较元素,它们还使用一个hash<key_type>
类型的对象为每个元素生成哈希值。标准库为内置类型,包括指针提供了哈希模板,还为string等标准库类型定义了哈希,因此上述类型可直接定义无需容器。
当使用自定义类型定义无序容器时,我们还需要通过模板特例化,提供自己的hash
模板版本。
3 pair类型
在介绍关联容器之前,我们需要了解名为pair
的标准库类型,它定义在头文件utility
中。
一个pair
保存两个数据成员。类似容器,pair
是一个用来生成特定类型的模板。创建一个pair
时,必须提供两个类型名,pair
的数据成员将具有对应的类型。pair上定义的操作如下:
除了构造函数,标准库还定义了一个make_pair
函数,由传递给它的两个实参生成一个新的pair
对象,实例如下:
pair<string, string> example;
string first, second;
example = make_pair(first, second);
提示:pair
类型定义较为繁琐,适当使用typedef
可以提高编程的效率。
4 关联容器的操作
4.1 类型操作
关联容器支持以下类型:
set<string>::value_type v1; //v1是string类型
set<string>::key_type v2; //v2是string类型
map<string, int>::value_type v3; //v3是pair<string,int>类型
map<string, int>::key_type v4; //v4是string类型
map<string, int>::mapped_type v5; //v5是int类型
4.2 迭代器
当解引用一个关联容器迭代器时,我们会得到一个类型为value_type
的值的引用。对map
而言,这是一个pair
,其first
是const
的关键字,second
成员保存值。对set而言,其迭代器是const
的,其关键字不能修改,示例如下:
map<string, int> mWordCnt;
auto m_iter = mWordCnt.begin();
//*m_iter是指向pair<tring, int>对象的引用
//iter->first是const,不能修改;只能修改其关联的值
m_iter->first = "new key"; //错误
++m_iter->second; //正确
cout << m_iter->first << " " << m_iter->second;
set<string> sNames;
auto s_iter = sNames.begin();
*s_iter = "new key"; //错误,关键字不能修改
map
和set
都支持用begin
和end
函数问出首尾迭代器,并以此进行关联容器的遍历。由于关键字是const
这一特性,我们不能将关联容器传递给修改或者重排容器元素的算法。在实际编程中,我们不通常不对关联容器使用泛型算法,即使使用,也只是将其作为一个源序列,或者当做一个目标位置来存放计算结果。
4.3 添加元素
关联容器的inset
成员可以向容器中添加一个元素或者一个元素的范围。由于map
和set
不包含重复的关键字,因此插入一个已经存在的元素没有任何影响。
向map添加元素
在C++11中,使用insert
函数添加单个元素通常有以下四种操作方式:
map<string, int> mWordCnt;
mWordCnt.insert({ "fiset", 1 });
mWordCnt.insert(make_pair("fiset", 1));
mWordCnt.insert(pair<string, int>("fiset", 1));
mWordCnt.insert(map<string, int>::value_type("fiset", 1));
除上述的四种方式,标准库还支持insert
某个范围的元素。需要注意的是:在插入单个元素时,insert函数会返回一个pair
,first
是当前成员的迭代器,second
是一个bool
变量,当插入成功是返回true
,如果该元素已经存在则返回false
;在插入某个范围的元素时,函数只返回void
。详细说明如下表:
向set添加元素
同样,可以使用insert
向set
容器添加元素:
set<string> sStr; // empty set
sStr.insert("the"); // sStr now has one element
sStr.insert("and"); // sStr now has two elements
另一种用法是,调用insert
函数时,提供一对迭代器实参,插入其标记范围内所有的元素。该版本的insert
函数类似于形参为一对迭代器的构造函数——对于一个键,仅插入一个元素:
vector<string> vStr = { "it`s", "a", "test" };
set<string> sStr2; // empty set
sStr2.insert(vStr.begin(), vStr.end()); // sStr2 has 3 elements
4.4 删除元素
关联容器定义了三个版本的erase
。与顺序容器一样,我们可以通过传递给erase
一个迭代器或者迭代器对,来删除一个元素或者一个元素的范围。这与添加元素十分相似,不再赘述。
关联容器提供了一个额外的erase
,它接受一个key_type
参数。此版本删除所有匹配给定关键词的元素,并返回实际删除的数量。示例如下:
vector<string> vStr = { "a", "a", "a" };
multiset<string> sStr3; // empty set
sStr3.insert(vStr.begin(), vStr.end()); // sStr2 has 3 elements
auto nCnt = sStr3.erase("a");// nCnt == 3
4.5 map下标
set
类型不支持下标,因为set
中没有关键字相关联的“值”,“值”本身就是关键字。我们可以对map
和unordermap
容器进行下标运算符以及at
函数的操作。但是,不能对multi
的版本进行相应操作,因为后者可能出现多个值与一个关键字对应的情况。
与其他下标运算符不同的是,如果关键字不再map
中,会为它创建一个元素并插入到map
中,关键值将进行初始化。
map<string, int> mWordCnt;
mWordCnt["basketball"] = 1;
上述代码将会执行如下操作:
- 在
mWordCnt
中查找basketball
关键字,未找到 - 将一个新的关键字-值对插入
mWordCnt
中,关键字是一个const string
,保存basketball
,并对second
进行默认初始化(int初始化为0) - 取出新插入的元素,并将1值赋予它
另外,需要注意的是,map
下标运算法返回的是mapped_type
对象,但在解引用一个map
迭代器时,会得到一个value_type
对象。
4.6 元素访问
find
函数可以返回指向元素的迭代器,如果元素不存在,则返回尾后迭代器。count
返回元素的个数,如果不需要计数,只需要知道是否存在,建议使用find
,`可以减少函数工作了,提供效率。
对于multi
前缀的类型,一个关键字可能对应多个值,其访问通常有三种方式:
- 先用
count
问出数量,后用find
问出第一个元素的位置,最后依次遍历
- 先用
auto nCnt = mWordCnt.count("basketball");
auto iter = mWordCnt.find("c");
while (nCnt){
cout << iter->second << endl;
++iter;
--nCnt;
}
- 使用
lower_bound
和upper_bound
问出关键字的上下界。注意:无需容器不支持这两个函数
- 使用
multimap<string, int> mWordCnt;
for (auto beg = mWordCnt.lower_bound("basketball"),
end = mWordCnt.upper_bound("basketball");
beg != end; ++beg){
cout << beg->second << endl;
}
- 使用
equal_range
函数,该函数返回一个迭代器pair
类型,first
是指向第一个元素的迭代器,second
指向该关键字尾后迭代器的值。
- 使用
multimap<string, int> mWordCnt;
for (auto range = mWordCnt.equal_range("basketball");
range.first != range.second; ++range.first){
cout << range.first->second << endl;
}