关联容器与顺序容器有着根本的不同:关联容器中的元素按关键字保存和访问,顺序容器中的元素按它们在容器中的位置来顺序保存和访问。
关联容器支持高效地关键字查找和访问。以下是两个主要的关联容器(associative-container)类型:
-
map
元素是一个键-值(key-value)对,关键字作为索引,值则表示与索引关联的数据。
-
set
元素只是一个关键字,支持高效地关键字查询。
标准库提供了 8 个关联容器,它们的区别体现在:
set
还是map
。- 是否允许关键字重复,容器名中的
multi
表示允许关键字重复。 - 顺序存储还是无序存储,以
unordered
开头的表示非顺序存储。
map
、multimap
定义在头文件map
中。set
、multiset
定义在头文件set
中。unordered_map
、unordered_multimap
定义在头文件unordered_map
中。unordered_set
、unordered_multiset
定义在头文件unordered_set
中。
存储键-值对的 map
类型通常也被称为关联数组(associative array),只不过其下标不必是整数,而是关键字。而 set
类型则只是关键字的简单集合。
关联容器概述
表 9.2 中的普通容器操作,关联容器都支持,但不支持顺序容器位置相关的操作,比如 push_front
、push_back
,也不支持通过 “元素个数 + 元素值” 的构造或插入操作。
关联容器的迭代器都是双向的。
定义关联容器
- 定义
map
时,必须同时指明关键字类型和值类型。 - 定义
set
时,只需指明关键字类型。
关联容器构造函数支持:
- 默认初始化。
- 同类型容器的拷贝初始化。
- 可转为容器所需类型的值范围初始化。
关联容器也是模板。
map<string, size_t> word_count;
set<string> exclude = { "the", "but" };
map<string, string> authors = {
{ "Karl", "Marx" },
{ "Tom", "Jerry" }
};
列表初始化 map
时,需要将键-值对包在一起,来表示 map
中的一个元素:
{ key, value }
容器 map
、set
的关键字必须唯一,容器 multimap
、multiset
允许关键字重复。
关键字类型的要求
传给排序算法的可调用对象,必须满足和关联容器中关键字一样的类型要求。
关联容器对其关键字类型有一定限制。对于有序容器 map
、multimap
、set
、multiset
,关键字类型必须定义元素比较的方法。标准库默认使用 <
运算符进行比较。也可以自定义操作来代替 <
。该操作必须在关键字类型上定义一个严格弱序 (strict weak ordering):
- 对于两个关键字 、,。
- 。
- 若 、,则称两者等价 ;而且 必须满足 。
容器将两个等价的关键字视为相等,用作 map
关键字时,只能有一个元素与这两个关键字关联,可以用两者中任一个访问相应的值。
如果一个类型定义了 “行为正常” 的
<
,则可以用作关键字类型。
用于组织容器中元素的操作类型也是该容器类型的一部分。为了自定义操作,必须在定义关联容器类型时提供操作类型。自定义操作类型必须紧跟着元素类型给出。创建容器时,还需以构造函数参数的形式提供真正的比较操作,其类型必须与指定的操作类型相符。
bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs)
{
return lhs.isbn() < rhs.isbn();
}
multiset<Sales_data, decltype(compareIsbn)*> bookstore(compareIsbn);
pair
类型
头文件 utility
中的标准库类型 pair
也是模板,创建 pair
时需要为它的两个数据成员分别提供类型名。pair
的默认构造函数对数据成员进行值初始化。pair
的两个数据成员 first
和 second
均为 public
。
pair<string, string> anon;
pair<string, string> author{"Tom", "Jerry"};
pair<string, vector<int>> anon1;
关联容器操作
除了普通容器上定义的类型之外,关联容器还定义了关键字和值的类型。
set<string>::value_type v1; // string
set<string>::key_type v2; // string
map<string, int>::value_type v3; // pair<const string, int>
map<string, int>::key_type v4; // string
map<string, int>::mapped_type v5; // int
map
关键字不能修改,因此value_type
中关键字为const key_type
类型。
关联容器迭代器
解引用关联容器迭代器得到的是一个对类型 value_type
的引用。
auto map_it = word_count.begin();
cout << map_it->first;
++map_it->second;
set
类型的关键字也不能修改,因此 iterator
和 const_iterator
对 set
中的元素都是只读的。
set<int> iset = { 0, 1, 2, 3 };
auto set_it = iset.begin();
*set_it = 1; // error
while (set_it != iset.end()) {
cout << *set_it++ << endl;
}
当使用迭代器遍历一个
map
、multimap
、set
、multiset
时,迭代器按关键字升序遍历元素。
通常不会对关联容器使用泛型算法。关键字是 const
意味着不能使用修改或重排元素的算法。关联容器虽然可用于只读元素的算法,但这些算法大多需要搜索序列,而不通过关键字快速查找。实践中,对关联容器使用算法时,要么将它作为源序列,要么作为目标位置。
添加元素
关联容器的 insert
成员可以向容器添加一个元素或一个元素范围。关键字不允许重复的容器,只有第一个带此关键字的元素才能被插入容器。
map
的元素类型是pair
。
vector<int> ivec = { 2, 4, 6, 2, 4, 6 };
set<int> set2;
set2.insert(ivec.cbegin(), ivec.cend());
set2.insert({ 1, 3, 5, 1, 3, 5 });
map<string, size_t> word_count;
word_count.insert({ word, 1 });
word_count.insert(make_pair(word, 1));
word_count.insert(pair<string, size_t>(word, 1));
word_count.insert(map<string, size_t>::value_type(word, 1));
insert
/emplace
返回值依赖于容器类型和参数。对于不允许重复关键字的容器,添加单个元素的 insert
/emplace
返回 pair
类型;它的 first
成员是一个指向具有给定关键字的元素的迭代器,second
成员是一个表示是否插入成功的 bool
值;如果关键字已存在,则 second
成员值为 false
,否则为 true
。
map<string, size_t> word_count;
// pair<map<string, size_t>::iterator, bool>
auto ret = word_count.insert({ word, 1 });
在 multi
容器上调用 insert
总会插入元素,因此插入单个元素的 insert
返回一个指向新元素的迭代器。
删除元素
除了接受一个迭代器或一个迭代器对来删除一个元素或一个元素范围之外,关联容器还提供一个额外的 erase
操作,接受一个 key_type
参数,删除所有匹配给定关键字的元素,返回实际删除的元素数量。
map
下标操作
map
和 unordered_map
容器提供了下标运算符和 at
函数。如果关键字不在 map
中,下标运算会创建一个元素并插入 map
中,关联值将会值初始化;介于此,只能对非 const
的 map
使用下标。下标运算的返回结果是一个 mapped_type
类型的左值对象。
set
类型没有与关键字相关联的值,因此不支持下标。
multimap
、unordered_multimap
中可能存在多个值与一个关键字关联,因此不支持下标。
map
的下标运算返回类型与解引用map
迭代器得到的类型不同。
map<string, size_t> word_count;
word_count["Anna"] = 1;
访问元素
如果不需要计数,只想知道一个特定元素是否在容器中,最好用 find
。
set<int> iset = { 0, 1, 2, 3, 4 };
iset.find(1);
iset.count(1);
对于 map
和 unordered_map
容器,下标运算符有自动插入元素的副作用,如果不想改变 map
,就应该使用 find
。
multimap
、multiset
中有多个元素具有相同关键字时,这些元素在容器中会相邻存储。所有具有给定关键字的元素可以通过 count
和 find
来获取,也可以使用 lower_bound
和 upper_bound
来获取,或者直接通过 equal_range
。
lower_bound
和 upper_bound
都接受一个关键字,返回一个迭代器。
- 如果关键字在容器中,
lower_bound
和upper_bound
返回值构成一个迭代器范围,它包含所有具有该关键字的元素。 - 如果关键字不在容器中,
lower_bound
和upper_bound
返回值相等,指向一个不影响容器中元素顺序的给定关键字的插入位置。
lower_bound
和upper_bound
都可能返回尾后迭代器。
multimap<string, string> authors;
string search_item = "marx";
for (auto beg = authors.lower_bound(search_item),
end = authors.upper_bound(search_item);
beg != end; ++beg) {
cout << beg->second << endl;
}
equal_range
接受一个关键字,返回一个迭代器 pair
,它的 first
、second
成员与 lower_bound
、upper_bound
返回值相同。
for (auto pos = authors.equal_range(search_item);
pos.first != pos.second; ++pos.first) {
cout << pos.first->second << endl;
}
无序容器
无序关联容器(unordered associative container)使用哈希函数(hash function)和关键字类型的 ==
运算符来组织元素,而不是比较运算符,常用于以下情况:
- 关键字类型没有明显的序关系;
- 维护元素的序代价很高。
如果关键字类型本身就是无序的,或者性能测试发现问题可以用哈希技术解决,就可以使用无序容器。
除了哈希管理操作之外,无序容器还提供了与有序容器相同的 find
、insert
等操作,而且也有允许关键字重复的版本。因此,通常无序容器和有序容器可以互相替换。但由于没有按顺序存储,无序容器的输出和有序容器不同。
管理桶
无序容器的存储用一组桶来组织,每个桶可以保存多个元素。元素通过一个哈希函数映射到一个桶,相同参数的哈希函数总是产生相同结果,哈希值相同的所有元素保存在一个桶中。要访问一个元素,首先计算它的哈希值,然后对桶中的元素顺序搜索。无序容器的性能依赖于哈希函数的质量、桶的数量和大小。
无序容器提供了一组管理桶的函数,这些成员函数可以查询容器的状态以及在必要时强制容器重组。
无序容器对关键字类型的要求
默认情况下,无序容器使用关键字类型的 ==
运算符来比较元素,使用 hash<key_type>
类型的对象来生成哈希值。标准库为包括指针在内的内置类型、string
类型、智能指针类型提供了 hash
模板,因此可以直接定义关键字为这些类型的无序容器。
关键字为自定义类类型的无序容器,必须提供自定义的 hash
模板,或者提供函数代替 ==
运算符和哈希函数。
size_t hasher(const Sales_data &sd) {
return hash<string>()(sd.isbn());
}
bool eqOp(const Sales_data &lhs, const Sales_data &rhs) {
return lhs.isbn() == rhs.isbn();
}
using SD_multiset = unordered_multiset<Sales_data, decltype(hasher)*, decltype(eqOp)*>;
SD_multiset bookStore(42, hasher, eqOp);
如果类已经定义了
==
运算符,则可以只提供哈希函数。