《C++ Primer》第11章 11.3节习题答案

《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),若不存在,则抛出一个异常。当然,程序只处理了这一种错误。你可以思考还有哪些错误,编写程序完成处理。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值