C++ Primer习题集之第十一章:关联容器(Chapter 11)

导读

本章介绍了标准库关联容器,包括:

         关联容器的概念和简单使用。

         关联容器涉及的类型和操作,特别是与顺序容器的差异。

         无序关联容器,特别是与有序容器的差异。

本章练习的最重要目的是让读者理解关联容器的思想及其与顺序容器的差异, 学会根据实际问题的特点选择适合的容器。具体内容除了关联容器基本类型和操作 的练习之外,还有一些较大的练习以及与实际问题相关的练习。

习题 11.1 ~ 11.38

练习 11.1:描述 map 和 vector 的不同。

【出题思路】
理解顺序容器和关联容器的不同。

【解答】
    学习关联容器,理解与顺序容器的不同,最关键的是理解其基础的数据结构,
随后它所表现出来的一些性质就很自然能够理解了。

    两类容器的根本差别在于,顺序容器中的元素是“顺序”存储的(链表容器中
的元素虽然不是在内存中“连续”存储的,但仍然是按“顺序”存储的)。理解“顺
序”的关键,是理解容器支持的操作形式以及效率。

    对于 vector 这样的顺序容器,元素在其中按顺序存储,每个元素有唯一对应
的位置编号,所有操作都是按编号(位置)进行的。例如,获取元素(头、尾、用
下标获取任意位置)、插入删除元素(头、尾、任意位置)、遍历元素(按元素位置
顺序逐一访问)。底层的数据结构是数组、链表,简单但已能保证上述操作的高效。
而对于依赖值的元素访问,例如查找(搜索)给定值(find),在这种数据结构上的
实现是要通过遍历完成的,效率不佳。

    而 map 这种关联容器,就是为了高效实现“按值访问元素”这类操作而设计的。
为了达到这一目的,容器中的元素是按关键字值存储的,关键字值与元素数据建立
起对应关系,这就是“关联”的含义。底层数据结构是红黑树、哈希表等,可高效
实现按关键字值查找、添加、删除元素等操作。
练习 11.2:分别给出最适合使用 list、vector、deque、map 以及 set 的例
子。

【出题思路】
理解顺序容器和关联容器的适用范围。

【解答】
    若元素很小(例如 int),大致数量预先可知,在程序运行过程中不会剧烈变化,
大部分情况下只在末尾添加或删除需要频繁访问任意位置的元素,则 vector 可带
来最高的效率。若需要频繁在头部和尾部添加或删除元素,则 deque 是最好的选择。

    如果元素较大(如大的类对象),数量预先不知道,或是程序运行过程中频繁变
化,对元素的访问更多是顺序访问全部或很多元素,则 list 很适合。

    map 很适合对一些对象按它们的某个特征进行访问的情形。典型的例如按学生
的名字来查询学生信息,即可将学生名字作为关键字,将学生信息作为元素值,保
存在 map 中。

    set,顾名思义,就是集合类型。当需要保存特定的值集合——通常是满足/不
满足某种要求的值集合,用 set 最为方便。
练习 11.3:编写你自己的单词计数程序。

【出题思路】
练习 map 的简单使用。

【解答】

#include <iostream>
#include <map>
#include <string>
#include <algorithm>
#include <cctype>
using namespace std;

int main()
{
    map<string, size_t> word_count;// string 到 count 的映射
    string word;

    while (std::cin >> word) 
    {
        ++word_count[word];// 这个单词的出现次数加 1
    }

    for (const auto& elem : word_count) 
    {   // 对 map 中的每个元素
        cout << elem.first << "出现了" << elem.second << "次" << endl;
    }

    return 0;
}
练习 11.4:扩展你的程序,忽略大小写和标点。例如,example.、example,
和 Example,应该递增相同的计数器。

【出题思路】
此题并非练习 set 的使用,而是字符串的处理。

【解答】
    编写函数 trans,将单词中的标点去掉,将大写都转换为小写。具体方法是:
遍历字符串,对每个字符首先检查是否是大写(ASCII 值在 A 和 Z 之间),若是,将
其转换为小写(减去 A,加上 a);否则,检查它是否带标点,若是,将其删除。最
终,将转换好的字符串返回。

#include <iostream>
#include <fstream>
#include <map>
#include <string>
#include <algorithm>
using namespace std;

string& trans(string& str)
{
    for (int p = 0; p < str.size(); p++)
    {
        if (str[p] >= 'A' && str[p] <= 'Z')
        {
            str[p] -= ('A' - 'a');
        }
        else if (str[p] == ',' || str[p] == '.')
        {
            str.erase(p, 1);
        }
    }

    return str;
}

int main(int argc, 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)
        ++word_count[trans(word)]; // 这个单词的出现次数加 1

    for (const auto& w : word_count) // 对 map 中的每个元素
        // 打印结果
        cout << w.first << "出现了" << w.second << "次" << endl;

    return 0;
}



【其它】
#include <iostream>
#include <map>
#include <string>
#include <algorithm>
#include <cctype>
using namespace std;

void word_count_pro(map<string, int>& m)
{
    std::string word;
    while (cin >> word)
    {
        for (auto& ch : word)
        {
            ch = tolower(ch);
        }

        word.erase(remove_if(word.begin(), word.end(), ispunct), word.end());

        ++m[word];
    }

    for (const auto& e : m)
    {
        cout << e.first << " : " << e.second << "\n";
    }
}

int main()
{
    map<string, int> m;
    word_count_pro(m);

    return 0;
}
练习 11.5:解释 map 和 set 的差别。你如何选择使用哪个?

【出题思路】
理解两种关联容器的差别。

【解答】
    当需要查找给定值所对应的数据时,应使用 map,其中保存的是<关键字, 值>
对,按关键字访问值。

    如果只需判定给定值是否存在时,应使用 set,它是简单的值的集合。
练习 11.6:解释 set 和 list 的差别。你如何选择使用哪个?

【出题思路】
理解关联容器和顺序容器的差别。

【解答】
两者都可以保存元素集合。
如果只需要顺序访问这些元素,或是按位置访问元素,那么应使用 list。
如果需要快速判定是否有元素等于给定值,则应使用 set。
练习 11.7:定义一个 map,关键字是家庭的姓,值是一个 vector,保存家中孩
子(们)的名。编写代码,实现添加新的家庭以及向已有家庭中添加新的孩子。

【出题思路】
理解 map 的稍复杂的使用。

【解答】
    此 map 的关键字类型是 string,值类型是 vector<string>。

    我们定义函数 add_family 添加一个家庭,注意,必须先检查是否已有这个家
庭,若不做这个检查,则可能将已有家庭的孩子名字清空(如 main 函数中的王姓家
庭的添加顺序)。若确实还没有这个家庭,则创建一个空的 vector<string>,表
示这个家庭的孩子名字列表。

    函数 add_child 向一个已有家庭添加孩子的名字:首先用[]运算符取出该家
庭的 vector,然后调用 push_back 将名字追加到 vector 末尾。

#include <iostream>
#include <map>
#include <vector>
#include <string>
#include <algorithm>
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(int argc, char* argv[])
{
    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;

    }

    return 0;
}

【其他解题思路】
add_family 的函数体其实可以有一个非常简单的实现:
families[family];

    当该家庭已存在时,此语句只是获取其 vector,不会导致 vector 有任何变化;
若该家庭不存在,标准库 map 的实现机制是在容器中为该关键字创建一个对象,进行
默认初始化,即构造一个空 vector。与 if 版本的效果完全一致。

    这也是 add_child 为什么不需要检查家庭是否存在的原因,当家庭存在时,将
孩子的名字追加到现有 vector 的末尾;若家庭不存在,标准库会先创建一个新的
空 vector,然后我们的程序将孩子名字添加进去。
练习 11.8:编写一个程序,在一个 vector 而不是一个 set 中保存不重复的单
词。使用 set 的优点是什么?

【出题思路】
通过实际编程理解 set 和 vector 的差别。

【解答】
    使用 vector 保存不重复单词,需要用 find 查找新读入的单词是否已在
vector 中,若不在(返回尾后迭代器),才将单词加入 vector。
而使用 set,检查是否重复的工作是由 set 模板负责的,程序员无须编写对应
代码,程序简洁很多。

    更深层次的差别,vector 是无序线性表,find 查找指定值只能采用顺序查找
方式,所花费的时间与 vector.size()呈线性关系。而 set 是用红黑树实现的,
花费的时间与 vector.size()呈对数关系。当单词数量已经非常多时,set 的性
能优势是巨大的。

    当然,vector 也不是毫无用处。它可以保持单词的输入顺序,而 set 则不能,
遍历 set,元素是按值的升序被遍历的。

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{
    vector<string> exclude = { "aa", "bb", "cc", "dd", "ee", "ff" };
    for (string word; cout << "Enter plz:\n", cin >> word;)
    {
        auto is_excluded = binary_search(exclude.cbegin(), exclude.cend(), word);
        auto reply = is_excluded ? "excluded" : "not excluded";
        cout << reply << std::endl;
    }

    return 0;
}
练习 11.9:定义一个 map,将单词与一个行号的 list 关联,list 中保存的是
单词所出现的行号。

【出题思路】
练习 map 的使用。

【解答】
map 的定义为:
    map<string, list<int>> word_lineno;

完整程序如下所示。其中用 getline 读取一行,统计行号。再用字符串流读取
这行中所有单词,记录单词行号。参见第 8 章内容。

#include <iostream>
#include <fstream>
#include <sstream>
#include <map>
#include <list>
#include <string>
#include <algorithm>
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(int argc, char* argv[])
{
    ifstream in(argv[1]);
    if (!in)
    {
        cout << "打开输入文件失败!" << endl;
        exit(1);
    }

    map<string, list<int>> word_lineno; // 单词到行号的映射
    string line;
    string word;
    int lineno = 0;
    while (getline(in, 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;
    }

    return 0;
}
练习 11.10:可以定义一个 vector<int>::iterator 到 int 的 map 吗?
list<int>::iterator 到 int 的 map 呢?对于两种情况,如果不能,解释为什
么。

【出题思路】
理解关联容器对关键字类型的要求。

【解答】
由于有序容器要求关键字类型必须支持比较操作<,
因此 map<vector<int>::iterator, int> m1;
是可以的,因为 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 中。

【出题思路】
本题练习 pair 的使用。

【解答】

#include <vector>
#include <utility>
#include <string>
#include <iostream>
using namespace std;

int main()
{
    vector<pair<string, int>> vec;
    string str;
    int i;

    while (cin >> str >> i)
    {
        vec.push_back(pair<string, int>(str, i));
    }


    for (const auto& p : vec)
    {
        cout << p.first << ":" << p.second << endl;
    }

    return 0;
}
练习 11.13:在上一题的程序中,至少有三种创建 pair 的方法。编写此程序的
三个版本,分别采用不同的方法创建 pair。解释你认为哪种形式最易于编写和理解,
为什么?

【出题思路】
熟悉 pair 的不同初始化方式。

【解答】
显然,列表初始化方式最为简洁易懂。

vec.push_back(std::make_pair(str, i));
vec.push_back({ str, i });
vec.push_back(std::pair<string, int>(str, i)); 
使用花括号的初始化器最易于理解和编写。
练习 11.14:扩展你在 11.2.1 节练习(第 378 页)中编写的孩子姓到名的 map,
添加一个 pair 的 vector,保存孩子的名字和生日。

【出题思路】
本题练习稍复杂的 pair 和关联容器的结合使用。

【解答】
在本题中,我们将家庭的姓映射到孩子信息的列表,而不是简单的孩子名字的
列表。因此,将在 vector 中的元素类型声明为 pair<string, string>,两个
string 分别表示孩子的名字和生日。在添加孩子信息时,用列表初始化创建名字和
生日的 pair,添加到 vector 中即可。

#include <iostream>
#include <map>
#include <string>
#include <vector>

using std::ostream;
using std::cout;
using std::cin;
using std::endl;
using std::string;
using std::make_pair;
using std::pair;
using std::vector;
using std::map;

class Families
{
public:
	using Child = pair<string, string>;
	using Children = vector<Child>;
	using Data = map<string, Children>;

	void add(string const& last_name, string const& first_name, string birthday)
	{
		auto child = make_pair(first_name, birthday);
		_data[last_name].push_back(child);
	}

	void print() const
	{
		for (auto const& pair : _data)
		{
			cout << pair.first << ":\n";
			for (auto const& child : pair.second)
				cout << child.first << " " << child.second << endl;
			cout << endl;
		}
	}

private:
	Data _data;
};

int main()
{
	Families families;
	auto msg = "Please enter last name, first name and birthday:\n";
	for (string l, f, b; cout << msg, cin >> l >> f >> b; families.add(l, f, b));
	families.print();

	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 迭代器编写一个表达式,将一个值赋予一个元素。

【出题思路】
理解 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 的迭代器的特点。

【解答】
    set 的迭代器是 const 的,因此只允许访问 set 中的元素,而不能改变 set。
与 map 一样,set 的关键字也是 const,因此也不能通过迭代器来改变 set 中元素
的值。

    因此,前两个调用试图将 vector 中的元素复制到 set 中,是非法的。
而后两个调用将 set 中的元素复制到 vector 中,是合法的。
练习 11.18:写出第 382 页循环中 map_it 的类型,不要使用 auto 或 decltype。

【出题思路】
理解 map 的迭代器。

【解答】
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 <map>
#include <string>

using std::string;
using std::map;
using std::cin;
using std::cout;

int main()
{
	map<string, size_t> counts;
	for (string word; cin >> word;)
	{
		auto result = counts.insert({ word, 1 });
		if (!result.second)
			++result.first->second;
	}
	for (auto const& count : counts)
		cout << count.first << " " << count.second << ((count.second > 1) ? " times\n" : " time\n");

	return 0;
}
练习 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(int argc, char *argv[])
{
    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;
}




【其它】
#include <map>
#include <string>
#include <iostream>

using std::string;
using std::multimap;
using std::cin;
using std::endl;

int main()
{
	multimap<string, string> families;
	for (string lname, cname; cin >> cname >> lname; families.emplace(lname, cname));
	for (auto const& family : families)
		std::cout << family.second << " " << family.first << 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 << 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(int argc, char *argv[])
{
    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);

    return 0;
}

【其他解题思路】
使用 find 或 lower_bound+upper_bound,也可实现本题目标,但复杂一
些。




【其它简答】
#include <map>
#include <string>
#include <iostream>

using std::string;

int main()
{
	std::multimap<string, string> authors{
		{ "alan", "DMA" },
		{ "pezy", "LeetCode" },
		{ "alan", "CLRS" },
		{ "wang", "FTP" },
		{ "pezy", "CP5" },
		{ "wang", "CPP-Concurrency" } };

	string author = "pezy";
	string work = "CP5";

	auto found = authors.find(author);
	auto count = authors.count(author);
	while (count)
	{
		if (found->second == work)
		{
			authors.erase(found);
			break;
		}
		++found;
		--count;
	}

	for (const auto &author : authors)
		std::cout << author.first << " " << author.second << std::endl;

	return 0;
}
练习 11.32:使用上一题定义的 multimap 编写一个程序,按字典序打印作者列
表和他们的作品。

【出题思路】
本题要求理解 multimap 数据结构中关键字的顺序,以及利用它来实现关键字
的有序输出。

【解答】
    multimap 的数据结构是红黑树,它维护了元素的关键字的默认序。例如,对
字符串关键字(作者),红黑树会维护它们的字典序。当我们遍历 multimap(如遍
历[begin(), end()),或更简单地使用范围 for)时,就是按关键字的字典序来
访问元素。

    因此,上一题的 print_books 实际上已经实现了按字典序打印作者列表和他
们的作品。

    但是,当我们要求的不是关键字的默认序(运算符<定义的顺序)时,就要复杂
一些。由于 sort 算法要求给定的两个迭代器是随机迭代器,关联容器的迭代器不符
合这一要求,所以不能直接对其使用 sort 算法。其实这不难理解,关联容器的根本
特征就是维护了关键字的默认序,从而实现了按关键字的插入、删除和查找。是不
可能通过 sort 使其内部元素呈现出另外一种顺序的。只有本身不关心元素值的顺序
容器,才可能随意安排元素顺序(位置)。我们可以在定义 multimap 时使用自己定
义的比较操作所定义的关键字的序,而不是使用<定义的序,但这只是令 multimap
以另外一种序来维护关键字,仍然不可能在使用 multimap 的过程中来改变关键字
顺序。为此,我们只能将 multimap 中的元素拷贝到一个顺序容器(如 vector)
中,对顺序容器执行 sort 算法,来获得关键字的其他序。

#include <map>
#include <set>
#include <string>
#include <iostream>
using std::string;

int main()
{
	std::multimap<string, string> authors{
		{ "alan", "DMA" },
		{ "pezy", "LeetCode" },
		{ "alan", "CLRS" },
		{ "wang", "FTP" },
		{ "pezy", "CP5" },
		{ "wang", "CPP-Concurrency" } };
	std::map<string, std::multiset<string>> order_authors;

	for (const auto &author : authors)
		order_authors[author.first].insert(author.second);

	for (const auto &author : order_authors)
	{
		std::cout << author.first << ": ";
		for (const auto &work : author.second)
			std::cout << work << " ";
		std::cout << std::endl;
	}

	return 0;
}
练习 11.33:实现你自己版本的单词转换程序。

【出题思路】
关联容器的综合练习。

【解答】
#include <iostream>
#include <map>
#include <fstream>
#include <sstream>

using namespace std;

void word_transform(ifstream&, ifstream&);
map<string, string> buildMap(ifstream&);
string transform(const string&, map<string, string>&);

int main()
{
	ifstream ifs_rules("transform_rules.txt");
	ifstream ifs_txt("for_transform.txt");
	
	word_transform(ifs_rules, ifs_txt);
	return 0;
}

void word_transform(ifstream& rule_file, ifstream& input)
{
	auto rule_map = buildMap(rule_file);
	string text;
	while (getline(input, text))
	{
		istringstream stream(text);
		string word;
		bool firstword = true;
		while (stream >> word)
		{
			if (firstword)
				firstword = false;
			else
				cout << " ";
			cout << transform(word, rule_map);
		}
		cout << endl;
	}
}

map<string, string> buildMap(ifstream& rule_file)
{
	map<string, string> m;
	string key;
	string value;
	while (rule_file >> key && getline(rule_file, value))
	{
		if (value.size() > 1)
			m[key] = value.substr(1);
		else
			throw runtime_error("no rule for " + key);
	}
	return m;
}

string transform(const string& s, map<string, string>& m)
{
	auto it = m.find(s);
	if (it != m.cend())
		return it->second;
	else
		return s;
}
练习 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),若不存在,则抛出一个异常。

    当然,程序只处理了这一种错误。你可以思考还有哪些错误,编写程序完成处理。
练习 11.37:一个无序容器与其有序版本相比有何优势?有序版本有何优势?

【出题思路】
理解无序关联容器与有序版本的差异。

【解答】
    无序版本通常性能更好,使用也更为简单。有序版本的优势是维护了关键字的
序。

    当元素的关键字类型没有明显的序关系,或是维护元素的序代价非常高时,无
序容器非常有用。

    但当应用要求必须维护元素的序时,有序版本就是唯一的选择。
练习 11.38:用 unordered_map 重写单词计数程序(参见 11.1 节,第 375 页)
和单词转换程序(参见 11.3.6 节,第 391 页)。

【出题思路】
本题练习使用无序关联容器。

【解答】
    对单词计数程序仅有的两处修改是将包含的头文件 map 改为 unordered_map,
以及将 word_count 的类型由 map 改为 unordered_map。

    尝试编译、运行此程序,你会发现,由于无序容器不维护元素的序,程序的输
出结果与第 3 题的输出结果的顺序是不同的。

#include <iostream>
#include <unordered_map>
#include <fstream>
#include <sstream>

using namespace std;

void word_transform(ifstream&, ifstream&);
unordered_map<string, string> buildMap(ifstream&);
string transform(const string&, unordered_map<string, string>&);

int main()
{
	ifstream ifs_rules("transform_rules.txt");
	ifstream ifs_txt("for_transform.txt");

	word_transform(ifs_rules, ifs_txt);
	return 0;
}

void word_transform(ifstream& rule_file, ifstream& input)
{
	auto rule_map = buildMap(rule_file);
	string text;
	while (getline(input, text))
	{
		istringstream stream(text);
		string word;
		bool firstword = true;
		while (stream >> word)
		{
			if (firstword)
				firstword = false;
			else
				cout << " ";
			cout << transform(word, rule_map);
		}
		cout << endl;
	}
}

unordered_map<string, string> buildMap(ifstream& rule_file)
{
	unordered_map<string, string> m;
	string key;
	string value;
	while (rule_file >> key && getline(rule_file, value))
	{
		if (value.size() > 1)
			m[key] = value.substr(1);
		else
			throw runtime_error("no rule for " + key);
	}
	return m;
}

string transform(const string& s, unordered_map<string, string>& m)
{
	auto it = m.find(s);
	if (it != m.cend())
		return it->second;
	else
		return s;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值