《C++ Primer》第11章 关联容器
11.3节关联容器操作 习题答案
练习11.16:使用一个map迭代器编写一个表达式,将一个值赋予一个元素。
【出题思路】
理解map的迭代器解引用的类型。
【解答】
解引用关联容器的迭代器,得到的是value_type的值的引用。因此对map而言,得到的是一个pair类型的引用,其first成员保存const的关键字,second成员保存值。因此,通过迭代器只能修改值,而不能改变关键字。
#include <iostream>
#include <map>
using namespace std;
int main()
{
map<int, int> m;
m[1] = 10;
auto it = m.begin();
for(auto temp: m)
cout << "temp.first=" << temp.first << " temp.second=" << temp.second << endl;
it->second = 20;
for(auto temp: m)
cout << "temp.first=" << temp.first << " temp.second=" << temp.second << endl;
cout << m.size() << endl;
return 0;
}
运行结果:
练习11.17:假定c是一个string的multiset,v是一个string的vector,解释下面的调用。指出每个调用是否合法:
copy(v.begin(), v.end(), inserter(c, c.end()));
copy(v.begin(), v.end(), back_inserter(c));
copy(c.begin(), c.end(), inserter(v, v.end()));
copy(c.begin(), c.end(), back_inserter(v));
【出题思路】
理解set的迭代器的特点。
【解答】
set的迭代器是const的,因此只允许访问set中的元素,而不能改变set。与map一样,set的关键字也是const,因此也不能通过迭代器来改变set中元素的值。
因此,前两个调用试图将vector中的元素复制到set中,
是非法的。而后两个调用将set中的元素复制到vector中,是合法的。
练习11.18:写出第382页循环中map_it的类型,不要使用auto或decltype。
【出题思路】
理解map的迭代器。
【解答】
*map_it是指向一个pair<const string, size_t>对象的引用。
所以map_it的类型是 pair<const string, size_t>::iterator
练习11.19:定义一个变量,通过对11.2.2节(第378页)中的名为bookstore的multiset调用begin()来初始化这个变量。写出变量的类型,不要使用auto或decltype。
【出题思路】
本题继续练习关联容器的迭代器。
【解答】
typedef bool (*pf)(const Sales_data &, const Sales_data &);
multiset<Sales_data, pf> bookstore(compareIsbn);
...
pair<const Sales_data, pf>::iterator it = bookstore.begin();
练习11.20:重写11.1节练习(第376页)的单词计数程序,使用insert代替下标操作。你认为哪个程序更容易编写和阅读?解释原因。
【出题思路】
熟悉关联容器不同的插入方式。
【解答】
使用insert操作的方式是:构造一个pair(单词,1),用insert将其插入容器,返回一个pair。若单词已存在,则返回pair的second成员为false,表示插入失败,程序员还需通过返回pair的first成员(迭代器)递增已有单词的计数器。判断单词是否已存在,并进行相应操作的工作完全是由程序员负责的。
使用下标操作的方式是:以单词作为下标获取元素值,若单词已存在,则提取出已有元素的值;否则,下标操作将pair(单词,0)插入容器,提取出新元素的值。单词是否已存在的相关处理完全是由下标操作处理的,程序员不必关心,直接访问提取出的值就行了。
显然,对于单词计数问题来说,下标操作更简洁易读。
#include <iostream>
#include <fstream>
#include <map>
#include <string>
#include <algorithm>
using namespace std;
int main(int argc, const char * argv[])
{
ifstream in(argv[1]);
if(!in)
{
cout << "打开输入文件失败!" << endl;
exit(1);
}
map<string, size_t> word_count; //string到count的映射
string word;
while(in >> word)
{
// auto ret = word_count.insert({word, 1});//插入单词,次数为1
// if(!ret.second) //插入失败,单词已存在
// ++ret.first->second; //已有单词的出现次数加1
++word_count[word]; //下标法
}
for(const auto &w: word_count) //对map中的每个元素
{
//打印结果
cout << w.first << "出现了 " << w.second << " 次" << endl;
}
return 0;
}
data10_20.txt文件内容为:
the quick red fox jumps over the the slow over red turtle
设置命令行参数,运行结果如下:
练习11.21:假定word_count是一个string到size_t的map,word是一个string,解释下面循环的作用:
while(cin >> word)
++word_count.insert({word, 0}).first->second;
【出题思路】
继续熟悉关联容器的insert操作。
【解答】
循环不断从标准输入读入单词(字符串),直至遇到文件结束或错误。
每读入一个单词,构造pair {word, 0},通过insert操作插入到word_count中。insert返回一个pair,其first成员是一个迭代器。若单词(关键字)已存在于容器中,它指向已有元素;否则,它指向新插入的元素。
因此,.first会得到这个迭代器,指向word对应的元素。继续使用->second,可获得元素的值的引用,即单词的计数。若单词是新的,则其值为0,若已存在,则值为之前出现的次数。对其进行递增操作,即完成将出现次数加1。
用这种方法,上一题可稍微简单些。
练习11.22:给定一个map<string,vector<int>>,对此容器的插入一个元素的insert版本,写出其参数类型和返回类型。
【出题思路】
继续熟悉关联容器的insert操作。
【解答】
参数类型是一个pair,first成员的类型是map的关键字类型string,second成员的类型是map的值类型vector<int>:
pair<string, vector<int>>
返回类型也是一个pair,first成员的类型是map的迭代器,second成员的类型是布尔型:
pair<map<string, vector<int>>::iterator, bool>
练习11.23:11.2.1节练习(第378页)中的map以孩子的姓为关键字,保存他们的名的vector,用multimap重写此map。
【出题思路】
本题练习允许重复关键字的关联容器的insert操作。
【解答】
由于允许重复关键字,已经不需要vector保存同一家的孩子的名字的列表,直接保存每个孩子的(姓,名)pair即可。容器类型变为multimap<string, string>。也不再需要add_family添加家庭,只保留add_child直接用insert操作添加孩子即可。
#include <iostream>
#include <map>
#include <string>
#include <algorithm>
using namespace std;
void add_child(multimap<string, string> &families, const string &family, const string &child)
{
families.insert({family, child});
}
int main()
{
multimap<string, string> families;
add_child(families, "张", "强");
add_child(families, "张", "刚");
add_child(families, "王", "五");
for(auto f: families)
{
cout << f.first << "家的孩子:" << f.second << endl;
}
return 0;
}
运行结果:
练习11.24:下面的程序完成什么功能?
map<int, int> m;
m[0] = 1;
【出题思路】
本题继续熟悉map的下标操作。
【解答】
若m中已有关键字0,下标操作提取出其值,赋值语句将值置为1。否则,下标操作会创建一个pair (0, 0),即关键字为0,值为0(值初始化),将其插入到m中,然后提取其值,赋值语句将值置为1。
练习11.25:对比下面程序与上一题程序
vector<int> v;
v[0] = 1;
【出题思路】
理解顺序容器和关联容器下标操作的不同。
【解答】
对于m,"0"表示“关键字0”。而对于v,"0"表示“位置0”。若v中已有不少于一个元素,即,存在“位置0”元素,则下标操作提取出此位置的元素(左值),赋值操作将其置为1。而map的元素是pair类型,下标提取的不是元素,而是元素的第二个成员,即元素的值。如v尚为空,则下标提取出的是一个非法左值(下标操作不做范围检查),向其赋值可能导致系统崩溃等严重后果。
练习11.26:可以用什么类型来对一个map进行下标操作?下标运算符返回的类型是什么?请给出一个具体例子——即,定义一个map,然后写出一个可以用来对map进行下标操作的类型以及下标运算符将会返回的类型。
【出题思路】
理解map的下标操作所涉及的各种类型。
【解答】
对map进行下标操作,应使用其key_type,即关键字的类型。
而下标操作返回的类型是mapped_type,即关键字关联的值的类型。
示例如下:map类型:map<string, int>
用来进行下标操作的类型:string
下标操作返回的类型:int
练习11.27:对于什么问题你会使用count来解决?什么时候你又会选择find呢?
【出题思路】
理解关联容器上不同算法的区别。
【解答】
find查找关键字在容器中出现的位置,而count则还会统计关键字出现的次数。因此,当我们希望知道(允许重复关键字的)容器中有多少元素的关键字与给定关键字相同时,使用count。
当我们只关心关键字是否在容器中时,使用find就足够了。特别是,对于不允许重复关键字的关联容器,find和count的效果没有什么区别,使用find就可以了。或者,当我们需要获取具有给定关键字的元素(而不只是统计个数)时,也需要使用find。
find和下标操作有一个重要区别,当给定关键字不在容器中时,下标操作会插入一个具有该关键字的元素。因此,当我们想检查给定关键字是否存在时,应该用find而不是下标操作。
练习11.28:对一个string到int的vector的map,定义并初始化一个变量来保存在其上调用find所返回的结果。
【出题思路】
理解map上的find。
【解答】
find返回一个迭代器,指向具有给定关键字的元素(若不存在则返回尾后迭代器),因此其返回类型是容器的迭代器。
//map类型
map<string, vector<int>> m;
//保存find返回结果的变量
map<string, vector<int>>::iterator iter;
练习11.29:如果给定的关键字不在容器中,upper_bound、lower_bound和equal_range分别会返回什么?
【出题思路】
熟悉适合multimap和multiset的基于迭代器的关键字查找方法。
【解答】
lower_bound返回第一个具有给定关键字的元素,upper_bound则返回最后一个具有给定关键字的元素之后的位置。即,这两个迭代器构成包含所有具有给定关键字的元素的范围。若给定关键字不在容器中,两个操作显然应构成一个空范围,它们返回相当的迭代器,指出关键字的正确插入位置——不影响关键字的排序。如果给定关键字比容器中所有关键字都大,则此位置是容器的尾后位置end。
equal_range返回一个pair,其first成员等价于lower_bound返回的迭代器,second成员等价于upper_bound返回的迭代器。因此,若给定关键字不在容器中,first和second都指向关键字的正确插入位置,两个迭代器构成一个空范围。
练习11.30:对于本节最后一个程序中的输出表达式,解释运算对象pos.first->second的含义。
【出题思路】
熟悉equal_range的使用。
【解答】
equal_range返回一个pair,其first成员与lower_bound的返回结果相同,即指向容器中第一个具有给定关键字的元素。因此,对其解引用会得到一个value_type对象,即一个pair,其first为元素的关键字,即给定关键字,而second为关键字关联的值。在本例中,关键字为作者,关联的值为著作的题目。因此pos.first->second即获得给定作者的第一部著作的题目。
练习11.31:编写程序,定义一个作者及其作品的multimap。使用find在multimap中查找一个元素并用erase删除它。确保你的程序在元素不在map中时也能正常运行。
【出题思路】
练习multimap的插入、查找和删除。
【解答】
将数据插入multimap,需使用insert操作。
在multimap中查找具有给定关键字的元素,有几种方法:使用find只能查找第一个具有给定关键字的元素,要找到所有具有给定关键字的元素,需编写循环;lower_bound和upper_bound配合使用,可找到具有给定关键字的元素的范围;equal_range最为简单,一次即可获得要查找的元素范围。
将找到的范围传递给erase,即可删除指定作者的所有著作。
为了解决元素不在multimap中的情况,首先检查equal_range返回的两个迭代器,若相等(空范围),则什么也不做。范围不为空时,才将迭代器传递给erase,删除所有元素。
#include <iostream>
#include <string>
#include <map>
#include <algorithm>
using namespace std;
void remove_author(multimap<string, string> &books, const string &author)
{
auto pos = books.equal_range(author); //查找给定作者范围
if(pos.first == pos.second) //空范围,没有该作者
cout << "没有" << author << "这个作者" << endl;
else
books.erase(pos.first, pos.second); //删除读作者所有著作
}
void print_books(multimap<string, string>& books)
{
cout << "当前书目包括:" << endl;
for(auto &book: books) //遍历所有书籍,打印之
cout << book.first << ", 《" << book.second << "》" << endl;
cout << endl;
}
int main()
{
multimap<string, string> books;
books.insert({"Barth, John", "Sot-Weed Factor"});
books.insert({"金庸", "射雕英雄传"});
books.insert({"Barth, John", "Lost in the Funhouse"});
books.insert({"金庸", "天龙八部"});
print_books(books);
remove_author(books, "张三");
remove_author(books, "Barth, John");
print_books(books);
return 0;
}
运行结果:
练习11.32:使用上一题定义的multimap编写一个程序,按字典序打印作者列表和他们的作品。
【出题思路】
本题要求理解multimap数据结构中关键字的顺序,以及利用它来实现关键字的有序输出。
【解答】multimap的数据结构是红黑树,它维护了元素的关键字的默认序。例如,对字符串关键字(作者),红黑树会维护它们的字典序。当我们遍历multimap(如遍历[begin(), end()),或更简单地使用范围for)时,就是按关键字的字典序来访问元素。
因此,上一题的print_books实际上已经实现了按字典序打印作者列表和他们的作品。
但是,当我们要求的不是关键字的默认序(运算符<定义的顺序)时,就要复杂一些。由于sort算法要求给定的两个迭代器是随机迭代器,关联容器的迭代器不符合这一要求,所以不能直接对其使用sort算法。其实这不难理解,关联容器的根本特征就是维护了关键字的默认序,从而实现了按关键字的插入、删除和查找。是不可能通过sort使其内部元素呈现出另外一种顺序的。只有本身不关心元素值的顺序容器,才可能随意安排元素顺序(位置)。我们可以在定义multimap时使用自己定义的比较操作所定义的关键字的序,而不是使用<定义的序,但这只是令multimap以另外一种序来维护关键字,仍然不可能在使用multimap的过程中来改变关键字顺序。为此,我们只能将multimap中的元素拷贝到一个顺序容器(如vector)中,对顺序容器执行sort算法,来获得关键字的其他序。
练习11.33:实现你自己版本的单词转换程序。
【出题思路】
关联容器的综合练习。
【解答】
{
if(value.size() > 1)//检查是否有转换规则
trans_map[key] = value.substr(1);//跳过前导空格
else
throw runtime_error("no rule for " + key);
}
return trans_map;
}
//生成转换文件
const string& transform(const string &s, const map<string, string> &m)
{
//实际的转换工作:此部分是程序的核心
//map<string, string>::const_iterator map_it = m.find(s);
auto map_it = m.find(s);
//如果单词在转换规map中
if(map_it != m.end())
return map_it->second;//使用替换短语
else
return s;//否则返回原string
}
//单词转换程序
void word_transform(ifstream &map_file, ifstream &input)
{
//map<string, string> trans_map = buildMap(map_file);
auto trans_map = buildMap(map_file);//保存转换规则
cout << "here is our transformation map:" <<endl;
for(map<string, string>::const_iterator entry = trans_map.begin(); entry != trans_map.end(); ++entry)
cout << "key: " << entry->first << "\tvalue: " << entry->second << endl;
cout << "\n\n";
string text;//保存输入中的每一行
while(getline(input, text))//读取一行输入
{
istringstream stream(text);//读取每个单词
string word;
bool firstword = true;
while(stream >> word)
{
if(firstword)
firstword = false;
else
cout << " ";//在单词间打印一个空格
//transform返回它的第一个参数或其转换之后的形式
cout << transform(word, trans_map);//打印输出
}
cout << endl;//完成一行的转换
}
}
int main(int argc, const char * argv[])
{
if(argc != 3)
throw runtime_error("wrong number of arguments");
ifstream map_file(argv[1]);
if(!map_file)
throw runtime_error("no transformation file");
ifstream input(argv[2]);
if(!input)
throw runtime_error("no input file");
word_transform(map_file, input);
return 0;
}
data11_33_map.txt的内容如下:
brb be right back
k okay?
y why
r are
u you
pic picture
the thanks!
l8r later
data11_33_input.txt的内容如下:
where r u
y don’t u send me a pic
k the l8r
设置命令行参数:
运行结果:
练习11.34:如果你将transform函数中的find替换为下标运算符,会发生什么情况?
【出题思路】继续理解find和下标操作的区别。
【解答】
如前所述,find仅查找给定关键字在容器中是否出现,若容器中不存在给定关键字,它返回尾后迭代器。当关键字存在时,下标运算符的行为与find类似,但当关键字不存在时,它会构造一个pair(进行值初始化),将其插入到容器中。对于单词转换程序,这会将不存在的内容插入到输出文本中,这显然不是我们所期望的。
练习11.35:在buildMap中,如果进行如下改写,会有什么效果?
trans_map[key] = value.substr(1);
改为trans_map.insert({key, value.substr(1)})
【出题思路】
继续理解insert操作和下标操作的区别。
【解答】
当map中没有给定关键字时,insert操作与下标操作+赋值操作的效果类似,都是将关键字和值的pair添加到map中。
但当map中已有给定关键字,也就是新的转换规则与一条已有规则要转换同一个单词时,两者的行为是不同的。下标操作会获得具有该关键字的元素(也就是已有规则)的值,并将新读入的值赋予它,也就是用新读入的规则覆盖了容器中的已有规则。但insert操作遇到关键字已存在的情况,则不会改变容器内容,而是返回一个值指出插入失败。因此,当规则文件中存在多条规则转换相同单词时,下标+赋值的版本最终会用最后一条规则进行文本转换,而insert版本则会用第一条规则进行文本转换。
练习11.36:我们的程序并没有检查输入文件的合法性。特别是,它假定转换规则文件中的规则都是有意义的。如果文件中的某一行包含一个关键字、一个空格,然后就结束了,会发生什么?预测程序的行为并进行验证,再与你的程序进行比较。
【出题思路】
本题练习字符串处理的技巧。
【解答】
此题有误,书中程序已经处理了这种情况。在buildMap函数中,当循环中读入要转换的单词和转换的内容后,会检查是否存在转换的内容(value.size()> 1),若不存在,则抛出一个异常。当然,程序只处理了这一种错误。你可以思考还有哪些错误,编写程序完成处理。