11.3 关联容器操作
关联容器还定义了如下表所列出的类型,这些类型表示容器关键字和值的类型:
- key_type:此容器类型的关键字类型;
- mapped_type:每个关键字关联的类型,只适用于 map;
- value_type:对于 set,与 key_type 相同;对于 map,为 pair<const key_type, mapped_type>;
11.3.1 关联容器迭代器
当解引用一个关联容器迭代器时,我们会得到一个类型为容器的 value_type 的值的引用。对 map 而言,value_type 是一个 pair 类型,其 first 成员保存 const 关键字,second 成员保存值:
cpp
auto map_it = word_count.begin();
// *map_it 是指向一个 pair<const string, size_t> 对象的引用
cout << map_it -> first;
cout << " " << map_it -> second;
map_it -> first = "new key"; // 错误, 关键字是 const 的
++ map_it -> second; // 正确, 可以通过迭代器修改值的元素
需要记住的是,一个 map 的 value_type 是一个 pair,我们可以改变 pair 的值,但是不可以改变它的关键字成员。
set 的迭代器是 const 的
尽管 set 类型同时定义了 iterator 和 const_iterator 类型,但两种类型都只允许访问 set 当中的元素。
遍历关联容器
map 和 set 类型都支持 begin 和 end 操作。可以用这些函数获取迭代器,然后使用迭代器来遍历容器。
例如:
cpp
auto map_it = word_count.cbegin();
while(map_it != word_count.cend()) {
cout << map_it -> first << " occurs " << map_it -> second << " times" << endl;
++ map_it;
}
关联容器和算法
我们通常不对关联容器使用泛型算法。关键字(map 的 key 以及 set 当中存储的元素)为 const 这一特性意味着不能将关联容器传递给修改或重拍容器元素的算法,因为这类算法需要向元素写入值,而 set 类型中的元素是 const 的,map 中的元素是 pair,其第一个成员是 const 的。
关联容器可以用于只读取元素的算法。但很多这类算法都需要搜索序列。由于关联容器中的元素不能通过它们的关键字进行(快速)查找,因此对其使用泛型搜索算法几乎总是不好的。
在实际编程中,如果想要对一个关联容器使用算法,要么将它当作一个源序列,要么当作一个目的位置。例如可以用泛型 copy 算法将元素从关联容器拷贝到一个序列当中,也可以调用 inserter 将一个插入器绑定到一个关联容器。
11.3.2 添加元素
关联容器的 insert 成员向容器中添加一个元素 或一个元素范围。由于 map 和 set 包含不重复的关键字,因此插入一个已存在的元素对容器没有任何影响。
insert 有两个版本,分别接受一对迭代器,或是一个初始化列表。
向 map 添加元素
对一个 map 进行 insert 时,元素类型是 pair。
在 C++ 11 标准下,最简单的创建 pair 的方式是花括号初始化,也可以调用 make_pair 或 显式构造 pair:
cpp
word_count.insert({word, 1});
word_count.insert(make_pair(word, 1));
word_count.insert(map<string, size_t>::value_type(word, 1));
word_count.insert(pair<string, size_t>(word, 1));
检测 insert 的返回值
insert 返回的值依赖于容器类型和参数。对于不包含重复关键字的容器,添加单一元素的 insert 和 emplace 版本返回一个 pair,告诉我们插入操作是否成功。pair 的 first 是一个迭代器,指向给定关键字的元素;second 是一个 bool,指出元素是插入成功还是已经存在于容器当中。如果关键字已经存在于关联容器当中,那么 insert 什么事情也不做,second 的 bool 值为 false。如果关键字不存在,元素才会被插入到容器当中,且 bool 为 true。
以下是一个使用 insert 重写单词记数程序的例子:
cpp
map<string, size_t> word_count;
string word;
while(cin >> word) {
auto ret = word_count.insert({word, 1}); // 使用花括号初始化 pair
if(!ret.second) { // 如果 word 已经存在于 word_count 中
++ ret.first -> second; // 递增计数器
}
}
展开递增语句
上述的递增计数器语句较难理解,可以添加一些括号来反映优先级:
cpp
++ ((ret.first) -> second); // 等价的表达式
向 multiset 和 multimap 添加元素
有时我们希望能够添加具有相同关键字的多个元素。此时我们可以使用 multimap 而不是 map。
由于一个 multi 关联容器中的关键字不必是唯一的,在这些类型上调用 insert 总会插入一个元素:
cpp
multimap<string, string> authors;
authors.insert({"Barth, John", "Sot-Weed Factor"});
authors.insert({"Barth, John", "Lost in the Funhouse"});
对允许重复添加关键字的容器,接受单个元素的 insert 操作返回一个指向新元素的迭代器。这里允许返回一个 bool 值,因为 insert 总是向这类容器加入一个新元素。
11.3.3 删除元素
关联容器定义了三个版本的 erase。可以传递给 erase 一个迭代器或一个迭代器范围来删除一个元素或一个元素范围。这两个版本的 erase 较为相似,指定的元素会被删除,函数返回 void。
关联容器提供了一个额外的 erase 操作,它接受一个 key_type 参数。此版本删除所有匹配给定关键字的元素(如果存在的话),返回实际删除的元素的数量。可以用这一版本的 erase 在打印结果之前删除 word_count 中特定的单词:
cpp
if(word_count.erase(removal_word)) {
cout << "ok: " <, removal_word << " removed\n";
} else {
cout << "oops: " << removal_word << " not found!\n";
}
对于保存不重复关键字的容器,这一版本的 erase 总是返回 0 或 1,因为要么容器中不包含这一关键字,如果包含的话,也只包含一个,所以成功删除的数量是一个。而对于允许重复关键字的容器,删除元素的数量可能大于 1。
11.3.4 map 的下标操作
map 和 unordered_map 容器提供了下标运算符和一个对应的 at 函数。set 类型不支持下标,因为 set 中没有与关键字相关联的"值"。元素本身就是关键字,因此"获取与一个关键字相关联的值"的操作没有意义。
同样地,我们也不能对一个 multimap 或 unordered_multimap 进行下标操作,因为这些容器有多个值与一个关键字关联。
类似之前的下标运算符,map 下标运算符接受一个索引(即关键字),获取与此关键字相关联的值。但是与其它下标运算符不同的是,如果关键字不在 map 当中,那么 map 会创建一个元素并插入到 map 当中,关联值将进行值初始化。
如果使用 at 进行下标操作,则与直接使用下标运算符略有不同。当关键字不在 map 时,使用 at 操作会抛出一个 out_of_range 异常。
使用下标操作的返回值
当对一个 map 进行下标操作时,会得到一个 mapped_type 对象;而当解引用一个 map 迭代器时,会得到一个 value_type 对象。
11.3.5 访问元素
关联容器提供了多种查找一个指定元素的方法:
- lower_bound 和 upper_bound 不适用于无序容器;
- 下标和 at 操作只适用于非 const 的 map 和 unordered_map;
- c.find(k):返回一个迭代器,指向第一个关键字为 k 的元素。若 k 不在容器中则返回尾后迭代器;
- c.count(k):返回关键字等于 k 的元素的数量。对于不允许重复的关联容器,其值永远为 0 或 1;
- c.lower_bound(k):返回一个迭代器,指向第一个关键字不小于 k 的元素;
- c.upper_bound(k):返回一个迭代器,指向第一个关键字大于 k 的元素;
- c.equal_range(k):返回一个迭代器 pair,表示关键字等于 k 的元素的范围。若 k 不存在,pair 的两个成员均等于 c.end()。
对 map 使用 find 代替下标操作
对于 map 和 unordered_map,下标操作的一个严重副作用是:如果关键字不在 map 中,则会执行插入。
有时我们只想知道一个关键字是否在 map 中,而不想改变 map,此时使用 find 更合适:
cpp
if(word_count.find("foobar") == word_count.end())
cout << "foobar is not in the map" << endl;
在 multimap 或 multiset 中查找元素
如果一个 multimap 或 multiset 中有多个元素具有给定关键字,这些元素在容器中会相邻存储。
一个使用 find 和 count 进行查找的例子如下:
cpp
string search_item("Alain de Botton"); // 要查找的作者
auto entries = authors.count(search_item); // 查找元素的数量
auto iter = authors.find(search_item); // 此作者的第一本书
while(entries) {
cout << ite r -> second << endl; // 打印每个题目
++ iter; // 前进到下一本书
-- entries; // 记录已经打印了多少本书
}
一种不同的,面向迭代器的解决方式
还可以用 lower_bound 和 upper_bound 来解决此问题。这两个操作都接受一个关键字,返回一个迭代器。如果关键字在容器中,lower_bound 返回的迭代器将指向第一个具有给定关键字的元素,而 upper_bound 返回的迭代器指向最后一个匹配给定关键字的元素之后的位置。
equal_range 函数
equal_range 比使用 lower_bound 和 upper_bound 更为直接。此函数接受一个关键字,返回一个迭代器 pair:
cpp
for(auto pos = authors.equal_range(search_item); pos.first != pos.second; ++ pos.first) {
cout << pos.first -> second << endl;
}