专栏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
}
}
如果想要更多的资源,欢迎关注 @我是管小亮,文字强迫症MAX~
回复【福利】即可获取我为你准备的大礼,包括C++,编程四大件,NLP,深度学习等等的资料。
想看更多文(段)章(子),欢迎关注微信公众号「程序员管小亮」~