参考资料:
- 《C++ Primer》第5版
- 《C++ Primer 习题集》第5版
关联容器支持高效关键字查找和访问,两个主要的关联容器是 map
和 set
。map
中的元素是键-值( key value )对,set
中的元素只包含一个关键字。
标准库提供 8 个关联容器:
容易看出,上面的 8 个关联容器的不同点主要体现在 3 个维度上:
- 或者是
set
;或者是map
- 或者要求不重复关键字;或者允许重复关键字,以
multi
开头 - 或者按顺序保存元素;或者无序保存,以
unordered_
开头
map
和 multimap
定义在头文件 map
中;set
和 multiset
定义在头文件 set
中;无序容器则分别定义在头文件 unordered_map
和 unordered_set
中。
11.1 使用关联容器(P374)
map
通常被称为关联数组( associative array ),可以通过关键字来查找值;set
可以用来判断某个值是否存在。
使用map
一个经典的使用 map
的例子是单词计数程序:
map<string, size_t> word_count;
string word;
while (cin >> word) {
++word_count[word];
}
for (const auto &w : word_count) {
cout << w.first << ' ' << w.second << endl;
}
map
也是模板,在定义时必须指定关键字和值的类型。在 while
循环中,如果关键字 word
在 word_count
中还不存在,下标运算符会创建一个新元素,其关键字为 word
,值为 0 。当从 map
中提取一个元素时,会得到一个 pair
对象,其 first
成员保存关键字,second
成员保存值。
使用set
如果我们在上述程序的基础上,想要忽略一些常见单词,可以使用 set
:
// 将需要忽略的单词保存在set中
set<string> exclude = { "the","and","a","an" };
// 判断当前单词word是否需要忽略
if(exclude.find(word) == exclude.end())
...
11.2 关联容器概述(P376)
关联容器(有序和无序)支持大多数普通容器的操作,除了:
- 和位置相关的操作,如
push_back
、push_front
。 - 接受一个元素值和一个数量值的构造函数和插入操作。
关联容器的迭代器都是双向的。
11.2.1 定义关联容器(P376)
每个关联容器都定义了一个默认构造函数,它创建一个空容器;我们也可以将关联容器初始化为另一个同类型容器的拷贝;也可以对关联容器进行值初始化:
map<string, string> authors = {
{"Joyce", "James"},
{"Austen", "Jane"}
};
初始化multimap
或multiset
vector<int> vi;
// 定义一个含有20个元素的vector
for (int i = 0; i < 10; ++i) {
vi.push_back(i);
vi.push_back(i); // 重复一次
}
set<int> si(vi.cbegin(), vi.cend()); // si的size为10
multiset<int> msi(vi.cbegin(), vi.cend()); // msi的size为20
11.2.2 关键字类型的要求(P378)
对于有序容器,关键字类型必须定义元素比较的方法,默认使用 <
运算符。
有序容器的关键字类型
我们可以提供自己定义的操作来替换关键字类型的 <
运算符,所提供的操作必须在关键字类型上定义一个严格弱序(strict weak ordering)。严格弱序可以看作“小于”,具有如下基本性质:
- 两个关键字不能同时“小于”对方;
- 如果 a “小于” b 且 b “小于” c ,那么 a “小于” c ;
- 如果两个关键字都不“小于”对方,则这两个关键字等价。
如果两个关键字是等价的,那么容器将它们视作相等。
使用关键字类型的比较函数
为了指定使用自定义操作,我们必须在定义关联容器类型时提供操作的类型,操作类型在尖括号中紧跟着元素类型给出:
bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs){
return lhs.isbn() < rhs.isbn();
}
multiset<Sales_data, decltype(compareIsbn)*>
bookstore(compareIsbn));
此处,我们使用 decltype
来指出自定义操作的类型,用 compareIsbn
来初始化 bookstore
对象,表明当我们向 bookstore
中添加元素时,调用 compareIsbn
来为这些元素排序。
11.2.3 pair
类型(P379)
pair
类型定义在头文件 utility
中。一个 pair
保留两个数据成员:
pair<string, string> anon;
pair<string, size_t> word_count;
pair
的默认构造函数对数据成员进行值初始化。pair
的数据成员是 public
的,两个成员分别命名为 first
和 second
。pair
支持的操作如下:
创建pair
对象的函数
pair<string, size_t> process(vector<string> &v) {
if (!v.empty()) {
return { v.back(), v.back().size() };
}
else {
return {};
}
}
上面的代码中,我们使用了初始值列表来返回 pair
。此外,我们还可以显式构造返回值或者使用 make_pair
函数:
return pair<string, size_t>(v.back(), v.back().size());
return make_pair(v.back(), v.back().size())
11.3 关联容器操作(P381)
关联容器还定义了一些类型别名:
set<string>::value_type v1; // string
set<string>::key_type v2; // string
map<string, int>::value_type v3; // pair<const stirng, int>
map<string, int>::key_type v4; // string
map<string, int>::mapped_type v5; // int
11.3.1 关联容器迭代器(P382)
解引用关联容器迭代器时,会得到一个 value_type
的引用:
auto map_it = word_count.begin();
map_it->first = "new key"; // 错误,关键字是const
map_it->second = 0; // 正确
set
的迭代器是const
的
set
类型同时定义了 iterator
和 const_iterator
,但两种类型都只允许只读访问 set
中的元素:
set<int> iset = { 0,1,2 };
auto set_it = iset.begin();
*set_it = 1; // 错误
遍历关联容器
auto map_it = word_count.cbegin();
while (map_it != word_count.end()) {
cout << map_it->first << ' ' << map_it->second;
++map_it;
}
本程序的输出是按照字典序排列的。
关联容器和算法
我们通常不对关联容器使用泛型算法。在实际编程中,如果我们真要对一个关联容器使用算法,要么将它当成一个源序列,要么当作一个目的位置。例如,使用 copy
算法将关联容器中的内容拷贝到另一个序列。
练习
只有第二个调用是错误的,因为 back_inserter
是通过容器的 push_back
成员实现元素插入的,而关联容器没有 push_back
成员。
11.3.2 添加元素(P383)
向不允许重复关键字的容器(如 map
、set
)插入一个已经存在的关键字,对容器没有任何影响:
vector<int> vi = { 0,1,2,3 };
set<int> s;
s.insert(vi.cbegin(), vi.cend());
s.insert({ 0,1,2 });
s.insert(4); // 只添加一个元素也是可以的
向map
添加元素
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
的返回值
对于不允许重复关键字的关联容器,添加单一元素的 insert
和 emplace
返回一个 pair
,pair
的 first
成员是一个迭代器,指向具有给定关键字的元素,second
成员是一个 bool
值,指出元素是否插入成功(插入成功为 true
):
map<string, size_t> word_count;
string word;
while (cin >> word) {
auto it = word_count.insert({ word,1 });
if (!it.second) {
++it.first->second;
}
}
向multiset
和multimap
添加元素
对于允许关键字重复的关联容器,接受单个元素的 insert
返回一个指向新元素的迭代器。
11.3.3 删除元素(P386)
关联容器提供一个特殊版本的 erase
:它接受一个 key_type
参数,删除所有关键词匹配的元素,返回实际删除的元素数量。
11.3.4 map
的下标操作(P387)
map
和 unordered_map
都支持下标运算符和 at
函数,但 multimap
和 unordered_multimap
不支持。
map
的下标运算符接受一个关键字。如果该关键字不在 map
中,下标运算符会为它创建一个元素并插入到 map
中,关联值进行值初始化。
使用下标操作的返回值
对一个 map
进行下标操作时,会获得一个 mapped_type
对象;解引用一个 map
迭代器时,会得到一个 value_type
对象。
11.3.5 访问元素(P388)
需要纠正的是,
const
的map
和unordered_map
是可以使用at
操作的。
当我们需要判断一个特定元素是否在容器中时,find
是最佳选择。
对map
使用find
代替下标操作
前面提到,下标运算符会在关键字不存在时插入对应元素,如果我们不希望插入新元素的话,可以使用 find
。
在multimap
和multiset
中查找元素
如果一个 multimap
或 multiset
中有多个元素具有相同的关键字,则这些元素在容器中会相邻存储。
假设我们有一个作者到著作的 multimap
映射,我们想要打印一个特定作者的所有著作,可以通过三种不同的方法来解决这个问题:
-
使用
find
和count
:auto it = authors.find(name); auto cnt = authors.count(name); while(cnt--){ cout << it->second << endl; ++it; }
-
使用
lower_bound
和upper_bound
。如果关键字在容器中,lower_bound
指向第一个具有给定关键字的元素,upper_bound
指向最后一个匹配元素之后的位置;如果关键字不在容器中,lower_bound
和upper_bound
会返回相等的迭代器,指向一个不影响排序的关键字插入位置。for (auto beg = authors.lower_bound(name), end = authors.upper_bound(name); beg != end; ++beg) { cout << beg->second << endl; }
-
使用
equal_range
函数。此函数接受一个关键字,返回一个迭代器pair
,first
成员和lower_bound
返回值相同,second
成员和upper_bound
返回值相同:for (auto pos = authors.equal_range(search_item); pos.first != pos.second;++pos.first) { cout << pos.first->second << endl; }
11.4 无序容器(P394)
**无序关联容器(unordered associative container)**使用哈希函数和关键字类型的 ==
来组织元素。
虽然理论上哈希技术具有较好的平均性能,但实际中想要达到很好的效果需要进行一些测试和调优工作。通常来讲,使用有序容器更为简单,也更为高效(这里中文版翻译错了)。
使用无序容器
通常可以用无序容器替换对应的有序容器。
管理桶
无序容器在存储上组织为一组桶,每个通保存零个或多个元素。为了访问一个元素,容器首先计算元素的哈希值,根据哈希值顺序搜索对应的桶。无序容器的性能依赖于哈希函数的质量和桶的数量。
无序容器提供了一组管理桶的函数:
无序容器对关键字类型的要求
默认情况下,无序容器使用关键字类型的 ==
来比较元素,使用 hash<key_type>
类型的对象来生成元素的哈希值。标准库为内置类型、string
和智能指针提供了 hash
模板。
如果我们想要定义关键字类型为自定义类型的无序容器,必须提供 hash
模板版本。为了将 Sale_data
用作关键字,我们可以提供函数来替代 ==
和 哈希函数:
size_t hasher(const Sales_data &sd){
// 可以理解为:先利用默认构造函数构造一个匿名对象,然后调用对象重载的圆括号运算符
return hash<string>()(sd.isbn());
}
bool equal(const Sales_data &lhs, const Sales_data &rhs){
return lhs.isbn() == rhs.isbn();
}
然后,我们可以利用上面的函数定义无序容器:
using SD_multiset = unordered_multiset<Sales_data,
decltype(hasher) *, decltype(equal) *>;
// 参数是桶数量的下限、哈希函数指针、相等判断函数指针
SD_multiset bookstore(42, hasher, equal);