《C++ Primer》习题参考答案:第11章 - 关联容器
欢迎关注WX公众号:【程序员管小亮】
专栏C++学习笔记
《C++ Primer》学习笔记/习题答案 总目录
——————————————————————————————————————————————————————
- 《C++ Primer》学习笔记(十一):关联容器
- 《C++ Primer》学习笔记(九):顺序容器
- 《C++ Primer》习题参考答案:第9章 - 顺序容器
- 《C++ Primer》学习笔记(三):字符串、向量和数组
- 《C++ Primer》习题参考答案:第3章 - 字符串、向量和数组
📚💻 Cpp-Prime5 + Cpp-Primer-Plus6 源代码和课后题
第11章 - 关联容器
练习11.1
描述
map和
vector的不同。
解:
学习关联容器,理解与顺序容器的不同,最关键的是理解其基础的数据结构,随后它所表现出来的一些性质就很自然能够理解了。
两类容器的根本差别在于,顺序容器中的元素是“顺序”存储的(链表容器中的元素虽然不是在内存中“连续”存储的,但仍然是按“顺序”存储的)。理解“顺序”的关键,是理解容器支持的操作形式以及效率。
对于
vector这样的顺序容器,元素在其中按顺序存储,每个元素有唯一对应的位置编号,所有操作都是按编号(位置)进行的。例如,获取元素(头、尾、用下标获取任意位置)、插入删除元素(头、尾、任意位置)、遍历元素(按元素位置顺序逐一访问)。底层的数据结构是数组、链表,简单但已能保证上述操作的高效。而对于依赖值的元素访问,例如查找(搜索)给定值(
find),在这种数据结构上的实现是要通过遍历完成的,效率不佳。
而
map这种关联容器,就是为了高效实现“按值访问元素”这类操作而设计的。为了达到这一目的,容器中的元素是按关键字值存储的,关键字值与元素数据建立起对应关系,这就是“关联”的含义。底层数据结构是红黑树、哈希表等,可高效实现按关键字值查找、添加、删除元素等操作。
练习11.2
分别给出最适合使用
list、
vector、
deque、
map以及
set的例子。
解:
- 若元素很小(例如
int
),大致数量预先可知,在程序运行过程中不会剧烈变化, 大部分情况下只在末尾添加或删除需要频繁访问任意位置的元素,则vector
可带来最高的效率。 - 若需要频繁在头部和尾部添加或删除元素,则
deque
是最好的选择。
list很适合。
map很适合对一些对象按它们的某个特征进行访问的情形。典型的例如按学生的名字来查询学生信息,即可将学生名字作为关键字,将学生信息作为元素值,保存在
map中。
set,顾名思义,就是集合类型。当需要保存特定的值集合——通常是满足/不满足某种要求的值集合,用
set最为方便。
练习11.3
编写你自己的单词计数程序。
解:
#include <iostream> #include <string> #include <algorithm> #include <map> #include <fstream> // file I/O support #include <cstdlib> // support for exit() using namespace std; int main() { char filename[20]; cout << "Enter name of data file: "; cin.getline(filename, 20); ifstream inFile; // object for handling file input inFile.open(filename); // associate inFile with a file if (!inFile.is_open()) // failed to open file { cout << "Could not open the file " << filename << endl; cout << "Program terminating.\n"; // cin.get(); // keep window open exit(EXIT_FAILURE); } map<string, size_t> word_count; string word; while (inFile >> word) { ++word_count[word]; } for (const auto &w : word_count){ cout << w.first << "出现了" << w.second << "次" << endl; } inFile.close(); // finished with the file system("pause"); return 0; }
练习11.4
扩展你的程序,忽略大小写和标点。例如,“example.”、“example,” 和 “Example” 应该递增相同的计数器。
解:
编写函数
trans,将单词中的标点去掉,将大写都转换为小写。
具体方法是:
遍历字符串,对每个字符首先检查是否是大写(
ASCII值在
A和
Z之间),若是,将其转换为小写(减去
A,加上
a);否则,检查它是否带标点,若是,将其删除。最终,将转换好的字符串返回。
#include <iostream> #include <string> #include <algorithm> #include <map> #include <fstream> // file I/O support #include <cstdlib> // support for exit() using namespace std; string &trans(string &s){ for (int p = 0; p < s.size(); p++){ if (s[p]>= 'A' && s[p] <= 'Z') s[p] -= ('A' - 'a'); else if (s[p] == ',' || s[p] == '.') s.erase(p, 1); } return s; } int main() { char filename[20]; cout << "Enter name of data file: "; cin.getline(filename, 20); ifstream inFile; // object for handling file input inFile.open(filename); // associate inFile with a file if (!inFile.is_open()) // failed to open file { cout << "Could not open the file " << filename << endl; cout << "Program terminating.\n"; // cin.get(); // keep window open exit(EXIT_FAILURE); } map<string, size_t> word_count; string word; while (inFile >> word) { ++word_count[trans(word)]; } for (const auto &w : word_count){ cout << w.first << "出现了" << w.second << "次" << endl; } inFile.close(); // finished with the file system("pause"); return 0; }
练习11.5
解释
map和
set的区别。你如何选择使用哪个?
解:
-
当需要查找给定值所对应的数据时,应使用
map
,其中保存的是<
关键字,值对,按关键字访问值。 -
如果只需判定给定值是否存在时,应使用
set
,它是简单的值的集合。
练习11.6
解释
set和
list的区别。你如何选择使用哪个?
解:
两者都可以保存元素集合。
- 如果只需要顺序访问这些元素,或是按位置访问元素,那么应使用
list
。 - 如果需要快速判定是否有元素等于给定值,则应使用
set
。
练习11.7
定义一个
map,关键字是家庭的姓,值是一个
vector,保存家中孩子(们)的名。编写代码,实现添加新的家庭以及向已有家庭中添加新的孩子。
解:
此
map的关键字类型是
string,值类型是
vector<string>。
我们定义函数
add family添加一个家庭,注意,必须先检查是否已有这个家庭,若不做这个检查,则可能将已有家庭的孩子名字清空(如
main函数中的王姓家庭的添加顺序)。若确实还没有这个家庭,则创建一个空的
vector<string>,表示这个家庭的孩子名字列表。
函数
add_child向一个已有家庭添加孩子的名字:首先用
[]运算符取出该家庭的
vector,然后调用
push back将名字追加到
vector末尾。
#include <iostream> #include <string> #include <algorithm> #include <map> #include <vector> using namespace std; void add_family(map<string, vector<string>> &families, const string &family){ if (families.find(family) == families.end()) families[family] = vector<string>(); } void add_child(map<string, vector<string>> &families, const string &family, const string &child){ families[family].push_back(child); } int main(){ map<string, vector<string>>families; add_family(families, "张"); add_child(families, "张", "强"); add_child(families, "张", "刚"); add_child(families, "王", "五"); add_family(families, "王"); for (auto f : families){ cout << f.first << "家的孩子:"; for (auto c : f.second) cout << c << " "; cout << endl; } system("pause"); return 0; }
练习11.8
编写一个程序,在一个
vector而不是一个
set中保存不重复的单词。使用
set的优点是什么?
解:
使用
vector保存不重复单词,需要用
find查找新读入的单词是否已在
vector中,若不在(返回尾后迭代器),才将单词加入
vector。而使用
set,检查是否重复的工作是由
set模板负责的,程序员无须编写对应代码,程序简洁很多。
更深层次的差别,
vector是无序线性表,
find查找指定值只能采用顺序查找方式,所花费的时间与
vector.size()呈线性关系。而
set是用红黑树实现的,花费的时间与
vector.size()呈对数关系。当单词数量已经非常多时,
set的性能优势是巨大的。当然,
vector也不是毫无用处。它可以保持单词的输入顺序,而
set则不能,遍历
set,元素是按值的升序被遍历的。
vector版本:
#include <iostream> #include <string> #include <algorithm> #include <vector> #include <fstream> // file I/O support #include <cstdlib> // support for exit() using namespace std; string &trans(string &s){ for (int p = 0; p < s.size(); p++){ if (s[p]>= 'A' && s[p] <= 'Z') s[p] -= ('A' - 'a'); else if (s[p] == ',' || s[p] == '.') s.erase(p, 1); } return s; } int main() { char filename[20]; cout << "Enter name of data file: "; cin.getline(filename, 20); ifstream inFile; // object for handling file input inFile.open(filename); // associate inFile with a file if (!inFile.is_open()) // failed to open file { cout << "Could not open the file " << filename << endl; cout << "Program terminating.\n"; // cin.get(); // keep window open exit(EXIT_FAILURE); } vector<string> unique_word; string word; while (inFile >> word) { trans(word); if (find(unique_word.begin(), unique_word.end(), word) == unique_word.end()) unique_word.push_back(word); } for (const auto &w : unique_word){ cout << w << endl; } cout << endl; inFile.close(); // finished with the file system("pause"); return 0; }
set版本:
#include <iostream> #include <string> #include <algorithm> #include <set> #include <fstream> // file I/O support #include <cstdlib> // support for exit() using namespace std; string &trans(string &s){ for (int p = 0; p < s.size(); p++){ if (s[p]>= 'A' && s[p] <= 'Z') s[p] -= ('A' - 'a'); else if (s[p] == ',' || s[p] == '.') s.erase(p, 1); } return s; } int main() { char filename[20]; cout << "Enter name of data file: "; cin.getline(filename, 20); ifstream inFile; // object for handling file input inFile.open(filename); // associate inFile with a file if (!inFile.is_open()) // failed to open file { cout << "Could not open the file " << filename << endl; cout << "Program terminating.\n"; // cin.get(); // keep window open exit(EXIT_FAILURE); } set<string> unique_word; string word; while (inFile >> word) { trans(word); if (find(unique_word.begin(), unique_word.end(), word) == unique_word.end()) unique_word.insert(word); } for (const auto &w : unique_word){ cout << w << endl; } cout << endl; inFile.close(); // finished with the file system("pause"); return 0; }
练习11.9
定义一个
map,将单词与一个行号的
list关联,
list中保存的是单词所出现的行号。
解:
#include <iostream> #include <string> #include <algorithm> #include <set> #include <map> #include <list> #include <sstream> #include <fstream> // file I/O support #include <cstdlib> // support for exit() using namespace std; string &trans(string &s){ for (int p = 0; p < s.size(); p++){ if (s[p]>= 'A' && s[p] <= 'Z') s[p] -= ('A' - 'a'); else if (s[p] == ',' || s[p] == '.') s.erase(p, 1); } return s; } int main() { char filename[20]; cout << "Enter name of data file: "; cin.getline(filename, 20); ifstream inFile; // object for handling file input inFile.open(filename); // associate inFile with a file if (!inFile.is_open()) // failed to open file { cout << "Could not open the file " << filename << endl; cout << "Program terminating.\n"; // cin.get(); // keep window open exit(EXIT_FAILURE); } map<string, list<int>> word_lineno; string line; string word; int lineno = 0; while (getline(inFile,line)) { lineno++; istringstream l_in(line); while (l_in >> word) { trans(word); word_lineno[word].push_back(lineno); } } for (const auto &w : word_lineno){ cout << w.first << "所在行"; for (const auto &i : w.second) cout << i << " "; cout << endl; } inFile.close(); // finished with the file system("pause"); return 0; }
练习11.10
可以定义一个
vector<int>::iterator到
int的
map吗?
list<int>::iterator到
int的
map呢?对于两种情况,如果不能,解释为什么。
解:
由于有序容器要求关键字类型必须支持比较操作
<,
- 因此
map<vector<int>::iterator, int>ml;
是可以的,因为vector
的迭代器支持比较操作。 - 而
map<list<int>::iterator, int>m2;
是不行的,因为list
的元素不是连续存储,其迭代器不支持比较操作。
练习11.11
不使用
decltype重新定义
bookstore。
解:
首先用
typedef定义与
compareIsbn相容的函数指针类型,然后用此类型声明
multiset即可。
typedef bool (*pf)(const Sales_data &, const Sales_data &); multiset<Sales_data, pf> bookstore(compareIsbn);
练习11.12
编写程序,读入
string和
int的序列,将每个
string和
int存入一个
pair中,
pair保存在一个
vector中。
解:
#include <iostream> #include <string> #include <algorithm> #include <utility> #include <vector> #include <fstream> // file I/O support #include <cstdlib> // support for exit() using namespace std; int main() { char filename[20]; cout << "Enter name of data file: "; cin.getline(filename, 20); ifstream inFile; // object for handling file input inFile.open(filename); // associate inFile with a file if (!inFile.is_open()) // failed to open file { cout << "Could not open the file " << filename << endl; cout << "Program terminating.\n"; // cin.get(); // keep window open exit(EXIT_FAILURE); } vector<pair<string, int>> data; string s; int v; while (inFile >> s && inFile >> v) { data.push_back(pair<string, int>(s, v)); } for (const auto &d : data){ cout << d.first << " " << d.second << endl; } inFile.close(); // finished with the file system("pause"); return 0; }
练习11.13
在上一题的程序中,至少有三种创建
pair的方法。编写此程序的三个版本,分别采用不同的方法创建
pair。解释你认为哪种形式最易于编写和理解,为什么?
解:
vec.push_back(make_pair(str, i)); vec.push_back({ str, i }); vec.push_back(pair<string, int>(str, i));
使用花括号的初始化器最易于理解和编写。
练习11.14
扩展你在11.2.1节练习中编写的孩子姓达到名的
map,添加一个
pair的
vector,保存孩子的名和生日。
解:
在本题中,我们将家庭的姓映射到孩子信息的列表,而不是简单的孩子名字的列表。因此,将在
vector中的元素类型声明为
pair<string, string>,两个
string分别表示孩子的名字和生日。在添加孩子信息时,用列表初始化创建名字和生日的
pair,添加到
vector中即可。
#include <iostream> #include <string> #include <algorithm> #include <map> #include <vector> using namespace std; void add_family(map<string, vector<pair<string, string>>> &families, const string &family){ families[family]; } void add_child(map<string, vector<pair<string, string>>> &families, const string &family, const string &child, const string &birthday){ families[family].push_back({ child, birthday }); } int main(){ map<string, vector<pair<string, string>>> families; add_family(families, "张"); add_child(families, "张", "强", "1970-1-1"); add_child(families, "张", "刚", "1980-1-1"); add_child(families, "王", "五", "1990-1-1"); add_family(families, "王"); for (auto f : families){ cout << f.first << "家的孩子:"; for (auto c : f.second) cout << c.first << "(生日" << c.second << "),"; cout << endl; } system("pause"); return 0; }
练习11.15
对一个
int到
vector<int>的
map,其
mapped_type、
key_type和
value_type分别是什么?
解:
mapped_type
是vector<int>
。key_type
是int
。value_type
是pair<const int, vector<int>>
。
练习11.16
使用一个
map迭代器编写一个表达式,将一个值赋予一个元素。
解:
解引用关联容器的迭代器,得到的是
value_type的值的引用。因此对
map而言,得到的是一个
pair类型的引用,其
first成员保存
const的关键字,
second成员保存值。因此,通过迭代器只能修改值,而不能改变关键字。
map<int, int>m; auto it=m.begin(); it->second = 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的迭代器是
const的,因此只允许访问
set中的元素,而不能改变
set。与
map一样,
set的关键字也是
const,因此也不能通过迭代器来改变
set中元素的值。
因此,前两个调用试图将
vector中的元素复制到
set中,是非法的。
而后两个调用将
set中的元素复制到
vector中,是合法的。
练习11.18
写出第382页循环中
map_it的类型,不要使用
auto或
decltype。
解:
pair<const string, size_t>::iterator
练习11.19
定义一个变量,通过对11.2.2节中的名为
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节练习的单词计数程序,使用
insert代替下标操作。你认为哪个程序更容易编写和阅读?解释原因。
解:
-
使用
insert
操作的方式是:构造一个pair
(单词,1),用insert
将其插入容器,返回一个pair
。若单词已存在,则返回pair
的second
成员为false
,表示插入失败,程序员还需通过返回pair
的first
成员(迭代器)递增已有单词的计数器。判断单词是否已存在,并进行相应操作的工作完全是由程序员负责的。 -
使用下标操作的方式是:以单词作为下标获取元素值,若单词已存在,则提取出已有元素的值;否则,下标操作将
pair
(单词,0)插入容器,提取出新元素的值。单词是否已存在的相关处理完全是由下标操作处理的,程序员不必关心,直接访问提取出的值就行了。
显然,对于单词计数问题来说,下标操作更简洁易读。
#include <iostream> #include <string> #include <algorithm> #include <map> #include <fstream> // file I/O support #include <cstdlib> // support for exit() using namespace std; int main() { char filename[20]; cout << "Enter name of data file: "; cin.getline(filename, 20); ifstream inFile; // object for handling file input inFile.open(filename); // associate inFile with a file if (!inFile.is_open()) // failed to open file { cout << "Could not open the file " << filename << endl; cout << "Program terminating.\n"; // cin.get(); // keep window open exit(EXIT_FAILURE); } map<string, size_t> word_count; string word; while (inFile >> word) { auto ret = word_count.insert({ word, 1 }); if (!ret.second) ++ret.first->second; } for (const auto &w : word_count){ cout << w.first << "出现了" << w.second << "次" << endl; } inFile.close(); // finished with the file system("pause"); return 0; }
练习11.21
假定
word_count是一个
string到
size_t的
map,
word是一个
string,解释下面循环的作用:
while (cin >> word) ++word_count.insert({word, 0}).first->second;
解:
循环不断从标准输入读入单词(字符串),直至遇到文件结束或错误。
每读入一个单词,构造
pair{word, 0},通过
insert操作插入到
word_count中。
insert返回一个
pair,其
first成员是一个迭代器。若单词(关键字)已存在于容器中,它指向已有元素;否则,它指向新插入的元素。
因此,
.first会得到这个迭代器,指向
word对应的元素。继续使用
->second,可获得元素的值的引用,即单词的计数。若单词是新的,则其值为0,若已存在,则值为之前出现的次数。对其进行递增操作,即完成将出现次数加1。
用这种方法,上一题可稍微简单些。
练习11.22
给定一个
map<string, vector<int>>,对此容器的插入一个元素的
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节练习中的
map以孩子的姓为关键字,保存他们的名的
vector,用
multimap重写此
map。
解:
#include <iostream> #include <string> #include <algorithm> #include <map> 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; } system("pause"); return 0; }
练习11.24
下面的程序完成什么功能?
map<int, int> m; m[0] = 1;
解:
若
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进行下标操作,应使用其
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所返回的结果。
解:
find返回一个迭代器,指向具有给定关键字的元素(若不存在则返回尾后迭代器),因此其返回类型是容器的迭代器。
// map类型 map<string, vector<int>> m; // 保存find返回结果的变量 map<string, vector<int>>::iterator iter;
练习11.29
如果给定的关键字不在容器中,
upper_bound、
lower_bound和
equal_range分别会返回什么?
解:
lower_bound返回第一个具有给定关键字的元素,
upper_bound则返回最后一个具有给定关键字的元素之后的位置。即,这两个迭代器构成包含所有具有给定关键字的元素的范围。若给定关键字不在容器中,两个操作显然应构成一个空范围,它们返回相当的迭代器,指出关键字的正确插入位置——不影响关键字的排序。如果给定关键字比容器中所有关键字都大,则此位置是容器的尾后位置
end。
equal_range返回一个
pair,其
first成员等价于
lower_bound返回的迭代器,
second成员等价于
upper_bound返回的迭代器。因此,若给定关键字不在容器中,
first和
second都指向关键字的正确插入位置,两个迭代器构成一个空范围。
练习11.30
对于本节最后一个程序中的输出表达式,解释运算对象
pos.first->second的含义。
解:
equal_range返回一个
pair,其
first成员与
lower bound的返回结果相同,即指向容器中第一个具有给定关键字的元素。因此,对其解引用会得到一个
value_type对象,即一个
pair,其
first为元素的关键字,即给定关键字,而
second为关键字关联的值。在本例中,关键字为作者,关联的值为著作的题目。因此
pos.first->second即获得给定作者的第一部著作的题目。
练习11.31
编写程序,定义一个作者及其作品的
multimap。使用
find在
multimap中查找一个元素并用
erase删除它。确保你的程序在元素不在
map中时也能正常运行。
解:
将数据插入
multimap,需使用
insert操作。
在
multimap中查找具有给定关键字的元素,有几种方法:
- 使用
find
只能查找第一个具有给定关键字的元素,要找到所有具有给定关键字的元素,需编写循环; lower_bound
和upper_bound
配合使用,可找到具有给定关键字的元素的范围;equal_range
最为简单,一次即可获得要查找的元素范围。将找到的范围传递给erase
,即可删除指定作者的所有著作。
为了解决元素不在
multimap中的情况,首先检查
equal_range返回的两个迭代器,若相等(空范围),则什么也不做。范围不为空时,才将迭代器传递给
erase,删除所有元素。
#include <iostream> #include <string> #include <algorithm> #include <map> 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 << 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({ "Barth, John", "Lost in the Funhouse" }); books.insert({ "金庸", "射雕英雄传" }); books.insert({ "金庸", "天龙八部" }); print_books(books); remove_author(books, "张三"); remove_author(books, "Barth, John"); print_books(books); system("pause"); return 0; }
练习11.32
使用上一题定义的
multimap编写一个程序,按字典序打印作者列表和他们的作品。
解:
multimap的数据结构是红黑树,它维护了元素的关键字的默认序。例如,对字符串关键字(作者),红黑树会维护它们的字典序。当我们遍历
multimap(如遍历
[begin(), end()),或更简单地使用范围
for)时,就是按关键字的字典序来访问元素。
因此,上一题的
print_books实际上已经实现了按字典序打印作者列表和他们的作品。
但是,当我们要求的不是关键字的默认序(运算符
<定义的顺序)时,就要复杂一些。由于
sort算法要求给定的两个迭代器是随机迭代器,关联容器的迭代器不符合这一要求,所以不能直接对其使用
sort算法。其实这不难理解,关联容器的根本特征就是维护了关键字的默认序,从而实现了按关键字的插入、删除和查找。是不可能通过
sort使其内部元素呈现出另外一种顺序的。只有本身不关心元素值的顺序容器,才可能随意安排元素顺序(位置)。我们可以在定义
multimap时使用自己定义的比较操作所定义的关键字的序,而不是使用
<定义的序,但这只是令
multimap以另外一种序来维护关键字,仍然不可能在使用
multimap的过程中来改变关键字顺序。为此,我们只能将
multimap中的元素拷贝到一个顺序容器(如
vector)中,对顺序容器执行
sort算法,来获得关键字的其他序。
练习11.33
实现你自己版本的单词转换程序。
解:
#include <map> #include <vector> #include <iostream> #include <fstream> #include <string> #include <stdexcept> #include <sstream> using namespace std; map<string, string> buildMap(ifstream &map_file) { map<string, string> trans_map; // holds the transformations string key; // a word to transform string value; // phrase to use instead // read the first word into key and the rest of the line into value while (map_file >> key && getline(map_file, value)) if (value.size() > 1) // check that there is a transformation trans_map[key] = value.substr(1); // skip leading space else throw runtime_error("no rule for " + key); return trans_map; } const string & transform(const string &s, const map<string, string> &m) { // the actual map work; this part is the heart of the program auto map_it = m.find(s); // if this word is in the transformation map if (map_it != m.cend()) return map_it->second; // use the replacement word else return s; // otherwise return the original unchanged } // first argument is the transformations file; // second is file to transform void word_transform(ifstream &map_file, ifstream &input) { auto trans_map = buildMap(map_file); // store the transformations // for debugging purposes print the map after its built cout << "Here is our transformation map: \n\n"; for (auto entry : trans_map) cout << "key: " << entry.first << "\tvalue: " << entry.second << endl; cout << "\n\n"; // do the transformation of the given text string text; // hold each line from the input while (getline(input, text)) { // read a line of input istringstream stream(text); // read each word string word; bool firstword = true; // controls whether a space is printed while (stream >> word) { if (firstword) firstword = false; else cout << " "; // print a space between words // transform returns its first argument or its transformation cout << transform(word, trans_map); // print the output } cout << endl; // done with this line of input } }
练习11.34
如果你将
transform函数中的
find替换为下标运算符,会发生什么情况?
解:
如前所述,
find仅查找给定关键字在容器中是否出现,若容器中不存在给定关键字,它返回尾后迭代器。当关键字存在时,下标运算符的行为与
find类似,但当关键字不存在时,它会构造一个
pair(进行值初始化),将其插入到容器中。对于单词转换程序,这会将不存在的内容插入到输出文本中,这显然不是我们所期望的。
练习11.35
在
buildMap中,如果进行如下改写,会有什么效果?
trans_map[key] = value.substr(1); //改为 trans_map.insert({key, value.substr(1)});
解:
当
map中没有给定关键字时,
insert操作与
下标操作+赋值操作的效果类似,都是将关键字和值的
pair添加到
map中。
但当
map中已有给定关键字,也就是新的转换规则与一条已有规则要转换同一个单词时,两者的行为是不同的。
- 下标操作会获得具有该关键字的元素(也就是已有规则)的值,并将新读入的值赋予它,也就是用新读入的规则覆盖了容器中的已有规则。
- 但
insert
操作遇到关键字已存在的情况,则不会改变容器内容,而是返回一个值指出插入失败。
因此,当规则文件中存在多条规则转换相同单词时,
下标+赋值的版本最终会用最后一条规则进行文本转换,而
insert版本则会用第一条规则进行文本转换。
练习11.36
我们的程序并没检查输入文件的合法性。特别是,它假定转换规则文件中的规则都是有意义的。如果文件中的某一行包含一个关键字、一个空格,然后就结束了,会发生什么?预测程序的行为并进行验证,再与你的程序进行比较。
解:
此题有误,书中程序已经处理了这种情况。
在
buildMap函数中,当循环中读入要转换的单词和转换的内容后,会检查是否存在转换的内容(
value.size()>1),若不存在,则抛出一个异常。
练习11.37
一个无序容器与其有序版本相比有何优势?有序版本有何优势?
解:
无序版本通常性能更好,使用也更为简单。有序版本的优势是维护了关键字的序。
当元素的关键字类型没有明显的序关系,或是维护元素的序代价非常高时,无序容器非常有用。但当应用要求必须维护元素的序时,有序版本就是唯一的选择。
练习11.38
用
unordered_map重写单词计数程序和单词转换程序。
解:
对单词计数程序仅有的两处修改是将包含的头文件
map改为
unordered_map,以及将
word_count的类型由
map改为
unordered map。
尝试编译、运行此程序,你会发现,由于无序容器不维护元素的序,程序的输出结果与第3题的输出结果的顺序是不同的。
#include <iostream> #include <string> #include <algorithm> #include <map> #include <fstream> // file I/O support #include <cstdlib> // support for exit() using namespace std; int main() { char filename[20]; cout << "Enter name of data file: "; cin.getline(filename, 20); ifstream inFile; // object for handling file input inFile.open(filename); // associate inFile with a file if (!inFile.is_open()) // failed to open file { cout << "Could not open the file " << filename << endl; cout << "Program terminating.\n"; // cin.get(); // keep window open exit(EXIT_FAILURE); } map<string, size_t> word_count; string word; while (inFile >> word) { ++word_count[word]; } for (const auto &w : word_count){ cout << w.first << "出现了" << w.second << "次" << endl; } inFile.close(); // finished with the file system("pause"); return 0; }
单词转换程序的修改类似。由于程序中不再有元素内容的顺序输出,因此输出结果与有序版本没有什么不同。
#include <unordered_map> #include <vector> #include <iostream> #include <fstream> #include <string> #include <stdexcept> #include <sstream> using namespace std; unordered_map<string, string> buildMap(ifstream &map_file) { unordered_map<string, string> trans_map; // holds the transformations string key; // a word to transform string value; // phrase to use instead // read the first word into key and the rest of the line into value while (map_file >> key && getline(map_file, value)) if (value.size() > 1) // check that there is a transformation trans_map[key] = value.substr(1); // skip leading space else throw runtime_error("no rule for " + key); return trans_map; } const string & transform(const string &s, const unordered_map<string, string> &m) { // the actual map work; this part is the heart of the program auto map_it = m.find(s); // if this word is in the transformation map if (map_it != m.cend()) return map_it->second; // use the replacement word else return s; // otherwise return the original unchanged } // first argument is the transformations file; // second is file to transform void word_transform(ifstream &map_file, ifstream &input) { auto trans_map = buildMap(map_file); // store the transformations // for debugging purposes print the map after its built cout << "Here is our transformation map: \n\n"; for (auto entry : trans_map) cout << "key: " << entry.first << "\tvalue: " << entry.second << endl; cout << "\n\n"; // do the transformation of the given text string text; // hold each line from the input while (getline(input, text)) { // read a line of input istringstream stream(text); // read each word string word; bool firstword = true; // controls whether a space is printed while (stream >> word) { if (firstword) firstword = false; else cout << " "; // print a space between words // transform returns its first argument or its transformation cout << transform(word, trans_map); // print the output } cout << endl; // done with this line of input } }
- 点赞 1
- 收藏
- 分享
- 文章举报
- c++ primer(第五版)学习笔记及习题答案代码版(第十一章)关联容器
- C++ Primer 5th 第11章 关联容器
- [C++ Primer] : 第11章: 关联容器
- c++ primer第五版(中文)习题答案 第十章第二节第二小节-写容器元素的算法
- c++primer(第五版) 第十一章 关联容器习题答案
- c++ primer第五版(中文)习题答案 第十章第六节-特定容器算法
- c++ primer 关联容器习题练习
- c++ primer 第11章 - 关联容器
- 《c++ primer》 第11章 关联容器 学习笔记
- C++ Primer【第五版】习题参考答案——第六章(函数)
- 抽象容器(参考c++ primer 3rd和习题解)
- C++Primer第五版 习题答案 第十一章 关联容器(Generic Algorithms)
- c++ primer第五版(中文)习题答案 第十章第二节第三小节-重排容器元素的算法
- c++ primer(第五版)学习笔记及习题答案代码版(第九章)顺序容器
- C++ Primer【第五版】习题参考答案——第五章(语句)
- C++ Primer 第 5 版 习题参考答案
- C++ Primer学习总结 第11章 关联容器
- 重新学习《C++Primer5》第11章-关联容器
- 【c++ primer读书笔记】【第11章】关联容器
- C++ Primer 第3章 标准库类型习题+答案