第十一章 关联容器

练习11.1

描述mapvector的不同。

解:

map 是关联容器, vector 是顺序容器。

练习11.2

分别给出最适合使用listvectordequemap以及set的例子。

解:

  • list:双向链表,适合频繁插入删除元素的场景。
  • vector:适合频繁访问元素的场景。
  • deque:双端队列,适合频繁在头尾插入删除元素的场景。
  • map:字典。
  • set:适合有序不重复的元素的场景。

练习11.3

编写你自己的单词计数程序。

解:

  1. #include <string>
  2. #include <map>
  3. #include <iostream>
  4. using namespace std;
  5. int main(){
  6. map<string, int> word_count;
  7. string tmp;
  8. while (cin >> tmp){
  9. word_count[tmp] += 1;
  10. }
  11. for (const auto& elem : word_count)
  12. std::cout << elem.first << " : " << elem.second << endl;
  13. return 0;
  14. }

练习11.4

扩展你的程序,忽略大小写和标点。例如,”example.”、”example,”和”Example”应该递增相同的计数器。

解:

  1. #include <iostream>
  2. #include <map>
  3. #include <string>
  4. #include <algorithm>
  5. #include <cctype>
  6. void word_count_pro(std::map<std::string, int>& m)
  7. {
  8. std::string word;
  9. while (std::cin >> word)
  10. {
  11. for (auto& ch : word)
  12. ch = tolower(ch);
  13. word.erase(std::remove_if(word.begin(), word.end(), ispunct),
  14. word.end());
  15. ++m[word];
  16. }
  17. for (const auto& e : m) std::cout << e.first << " : " << e.second << "\n";
  18. }
  19. int main()
  20. {
  21. std::map<std::string, int> m;
  22. word_count_pro(m);
  23. return 0;
  24. }

练习11.5

解释mapset的区别。你如何选择使用哪个?

解:

map 是键值对,而 set 只有键没有值。当我需要存储键值对的时候使用 map,而只需要键的时候使用 set

练习11.6

解释setlist的区别。你如何选择使用哪个?

set 是有序不重复集合,底层实现是红黑树,而 list 是无序可重复集合,底层实现是链表。

练习11.7

定义一个map,关键字是家庭的姓,值是一个vector,保存家中孩子(们)的名。编写代码,实现添加新的家庭以及向已有家庭中添加新的孩子。

解:

  1. map<string, vector<string>> m;
  2. for (string ln; cout << "Last name:\n", cin >> ln && ln != "@q";)
  3. for (string cn; cout << "|-Children's names:\n", cin >> cn && cn != "@q";)
  4. m[ln].push_back(cn);

练习11.8

编写一个程序,在一个vector而不是一个set中保存不重复的单词。使用set的优点是什么?

解:

  1. #include <iostream>
  2. #include <string>
  3. #include <vector>
  4. #include <algorithm>
  5. int main()
  6. {
  7. std::vector<std::string> exclude = { "aa", "bb", "cc", "dd", "ee", "ff" };
  8. for (std::string word; std::cout << "Enter plz:\n", std::cin >> word;)
  9. {
  10. auto is_excluded = std::binary_search(exclude.cbegin(), exclude.cend(), word);
  11. auto reply = is_excluded ? "excluded" : "not excluded";
  12. std::cout << reply << std::endl;
  13. }
  14. return 0;
  15. }

set的优点是集合本身的元素就是不重复。

练习11.9

定义一个map,将单词与一个行号的list关联,list中保存的是单词所出现的行号。

解:

  1. std::map<std::string, std::list<std::size_t>> m;

练习11.10

可以定义一个vector<int>::iteratorintmap吗?list<int>::iteratorintmap呢?对于两种情况,如果不能,解释为什么。

解:

可以定义 vector<int>::iteratorintmap,但是不能定义 list<int>::iteratorintmap。因为map的键必须实现 < 操作,list 的迭代器不支持比较运算。

练习11.11

不使用decltype 重新定义 bookstore

解:

  1. using Less = bool (*)(Sales_data const&, Sales_data const&);
  2. std::multiset<Sales_data, Less> bookstore(less);

练习11.12

编写程序,读入stringint的序列,将每个stringint存入一个pair 中,pair保存在一个vector中。

解:

  1. #include <vector>
  2. #include <utility>
  3. #include <string>
  4. #include <iostream>
  5. int main()
  6. {
  7. std::vector<std::pair<std::string, int>> vec;
  8. std::string str;
  9. int i;
  10. while (std::cin >> str >> i)
  11. vec.push_back(std::pair<std::string, int>(str, i));
  12. for (const auto &p : vec)
  13. std::cout << p.first << ":" << p.second << std::endl;
  14. }

练习11.13

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

解:

  1. vec.push_back(std::make_pair(str, i));
  2. vec.push_back({ str, i });
  3. vec.push_back(std::pair<string, int>(str, i));

使用花括号的初始化器最易于理解和编写。

练习11.14

扩展你在11.2.1节练习中编写的孩子姓达到名的map,添加一个pairvector,保存孩子的名和生日。

解:

  1. #include <iostream>
  2. #include <map>
  3. #include <string>
  4. #include <vector>
  5. using std::ostream;
  6. using std::cout;
  7. using std::cin;
  8. using std::endl;
  9. using std::string;
  10. using std::make_pair;
  11. using std::pair;
  12. using std::vector;
  13. using std::map;
  14. class Families
  15. {
  16. public:
  17. using Child = pair<string, string>;
  18. using Children = vector<Child>;
  19. using Data = map<string, Children>;
  20. void add(string const& last_name, string const& first_name, string birthday)
  21. {
  22. auto child = make_pair(first_name, birthday);
  23. _data[last_name].push_back(child);
  24. }
  25. void print() const
  26. {
  27. for (auto const& pair : _data)
  28. {
  29. cout << pair.first << ":\n";
  30. for (auto const& child : pair.second)
  31. cout << child.first << " " << child.second << endl;
  32. cout << endl;
  33. }
  34. }
  35. private:
  36. Data _data;
  37. };
  38. int main()
  39. {
  40. Families families;
  41. auto msg = "Please enter last name, first name and birthday:\n";
  42. for (string l, f, b; cout << msg, cin >> l >> f >> b; families.add(l, f, b));
  43. families.print();
  44. return 0;
  45. }

练习11.15

对一个intvector<int>的map,其mapped_typekey_typevalue_type分别是什么?

解:

  • mapped_type : vector<int>
  • key_type : int
  • value_type : std::pair<const int,vector >

练习11.16

使用一个map迭代器编写一个表达式,将一个值赋予一个元素。

解:

  1. std::map<int, string>::iterator it = m.begin();
  2. it->second = "hello";

练习11.17

假定c是一个stringmultisetv 是一个stringvector,解释下面的调用。指出每个调用是否合法:

  1. copy(v.begin(), v.end(), inserter(c, c.end()));
  2. copy(v.begin(), v.end(), back_inserter(c));
  3. copy(c.begin(), c.end(), inserter(v, v.end()));
  4. copy(c.begin(), c.end(), back_inserter(v));

解:

第二个调用不合法,因为 multiset 没有 push_back 方法。其他调用都合法。

练习11.18

写出第382页循环中map_it 的类型,不要使用autodecltype

解:

  1. map<string, size_t>::const_iterator map_it = word_count.cbegin();

练习11.19

定义一个变量,通过对11.2.2节中的名为 bookstoremultiset 调用begin()来初始化这个变量。写出变量的类型,不要使用autodecltype

解:

  1. using compareType = bool (*)(const Sales_data &lhs, const Sales_data &rhs);
  2. std::multiset<Sales_data, compareType> bookstore(compareIsbn);
  3. std::multiset<Sales_data, compareType>::iterator c_it = bookstore.begin();

练习11.20

重写11.1节练习的单词计数程序,使用insert代替下标操作。你认为哪个程序更容易编写和阅读?解释原因。

解:

  1. #include <iostream>
  2. #include <map>
  3. #include <string>
  4. using std::string;
  5. using std::map;
  6. using std::cin;
  7. using std::cout;
  8. int main()
  9. {
  10. map<string, size_t> counts;
  11. for (string word; cin >> word;)
  12. {
  13. auto result = counts.insert({ word, 1 });
  14. if (!result.second)
  15. ++result.first->second;
  16. }
  17. for (auto const& count : counts)
  18. cout << count.first << " " << count.second << ((count.second > 1) ? " times\n" : " time\n");
  19. return 0;
  20. }

使用insert更容易阅读和编写。insert有返回值,可以明确的体现出插入操作的结果。

练习11.21

假定word_count是一个stringsize_tmapword是一个string,解释下面循环的作用:

  1. while (cin >> word)
  2. ++word_count.insert({word, 0}).first->second;

解:

这条语句等价于:

  1. while (cin >> word)
  2. {
  3. auto result = word_count.insert({word, 0});
  4. ++(result.first->second);
  5. }

insert成功:先添加一个元素,然后返回一个 pairpairfirst元素是一个迭代器。这个迭代器指向刚刚添加的元素,这个元素是pair,然后递增pairsecond成员。
insert失败:递增已有指定关键字的元素的 second成员。

练习11.22

给定一个map<string, vector<int>>,对此容器的插入一个元素的insert版本,写出其参数类型和返回类型。

  1. std::pair<std::string, std::vector<int>> // 参数类型
  2. std::pair<std::map<std::string, std::vector<int>>::iterator, bool> // 返回类型

练习11.23

11.2.1节练习中的map 以孩子的姓为关键字,保存他们的名的vector,用multimap 重写此map

解:

  1. #include <map>
  2. #include <string>
  3. #include <iostream>
  4. using std::string;
  5. using std::multimap;
  6. using std::cin;
  7. using std::endl;
  8. int main()
  9. {
  10. multimap<string, string> families;
  11. for (string lname, cname; cin >> cname >> lname; families.emplace(lname, cname));
  12. for (auto const& family : families)
  13. std::cout << family.second << " " << family.first << endl;
  14. }

练习11.24

下面的程序完成什么功能?

  1. map<int, int> m;
  2. m[0] = 1;

解:

添加一个元素到map中,如果该键存在,则重新赋值。

练习11.25

对比下面的程序与上一题程序

  1. vector<int> v;
  2. v[0] = 1;

解:

未定义行为,vector 的下标越界访问。

练习11.26

可以用什么类型来对一个map进行下标操作?下标运算符返回的类型是什么?请给出一个具体例子——即,定义一个map,然后写出一个可以用来对map进行下标操作的类型以及下标运算符将会返会的类型。

解:

  1. std::map<int, std::string> m = { { 1,"ss" },{ 2,"sz" } };
  2. using KeyType = std::map<int, std::string>::key_type;
  3. using ReturnType = std::map<int, std::string>::mapped_type;

练习11.27

对于什么问题你会使用count来解决?什么时候你又会选择find呢?

解:

对于允许重复关键字的容器,应该用 count ; 对于不允许重复关键字的容器,应该用 find

练习11.28

对一个stringintvectormap,定义并初始化一个变量来保存在其上调用find所返回的结果。

解:

  1. map<string, vector<int>> m;
  2. map<string, vector<int>>::iterator it = m.find("key");

练习11.29

如果给定的关键字不在容器中,upper_boundlower_boundequal_range 分别会返回什么?

解:

如果给定的关键字不在容器中,则 lower_boundupper_bound 会返回相等的迭代器,指向一个不影响排序的关键字插入位置。而equal_range 会返回一个 pairpair 中的两个迭代器都指向关键字可以插入的位置。

练习11.30

对于本节最后一个程序中的输出表达式,解释运算对象pos.first->second的含义。

解:

pos 是一个pairpos.first 是一个迭代器,指向匹配关键字的元素,该元素是一个 pair,访问该元素的第二个成员。

练习11.31

编写程序,定义一个作者及其作品的multimap。使用findmultimap中查找一个元素并用erase删除它。确保你的程序在元素不在map 中时也能正常运行。

解:

  1. #include <map>
  2. #include <string>
  3. #include <iostream>
  4. using std::string;
  5. int main()
  6. {
  7. std::multimap<string, string> authors{
  8. { "alan", "DMA" },
  9. { "pezy", "LeetCode" },
  10. { "alan", "CLRS" },
  11. { "wang", "FTP" },
  12. { "pezy", "CP5" },
  13. { "wang", "CPP-Concurrency" } };
  14. string author = "pezy";
  15. string work = "CP5";
  16. auto found = authors.find(author);
  17. auto count = authors.count(author);
  18. while (count)
  19. {
  20. if (found->second == work)
  21. {
  22. authors.erase(found);
  23. break;
  24. }
  25. ++found;
  26. --count;
  27. }
  28. for (const auto &author : authors)
  29. std::cout << author.first << " " << author.second << std::endl;
  30. return 0;
  31. }

练习11.32

使用上一题定义的multimap编写一个程序,按字典序打印作者列表和他们的作品。

解:

  1. #include <map>
  2. #include <set>
  3. #include <string>
  4. #include <iostream>
  5. using std::string;
  6. int main()
  7. {
  8. std::multimap<string, string> authors{
  9. { "alan", "DMA" },
  10. { "pezy", "LeetCode" },
  11. { "alan", "CLRS" },
  12. { "wang", "FTP" },
  13. { "pezy", "CP5" },
  14. { "wang", "CPP-Concurrency" } };
  15. std::map<string, std::multiset<string>> order_authors;
  16. for (const auto &author : authors)
  17. order_authors[author.first].insert(author.second);
  18. for (const auto &author : order_authors)
  19. {
  20. std::cout << author.first << ": ";
  21. for (const auto &work : author.second)
  22. std::cout << work << " ";
  23. std::cout << std::endl;
  24. }
  25. return 0;
  26. }

练习11.33

实现你自己版本的单词转换程序。

解:

  1. #include <map>
  2. #include <string>
  3. #include <fstream>
  4. #include <iostream>
  5. #include <sstream>
  6. using std::string; using std::ifstream;
  7. std::map<string, string> buildMap(ifstream &map_file)
  8. {
  9. std::map<string, string> trans_map;
  10. for (string key, value; map_file >> key && getline(map_file, value); )
  11. if (value.size() > 1) trans_map[key] = value.substr(1).substr(0, value.find_last_not_of(' '));
  12. return trans_map;
  13. }
  14. const string & transform(const string &s, const std::map<string, string> &m)
  15. {
  16. auto map_it = m.find(s);
  17. return map_it == m.cend() ? s : map_it->second;
  18. }
  19. void word_transform(ifstream &map, ifstream &input)
  20. {
  21. auto trans_map = buildMap(map);
  22. for (string text; getline(input, text); ) {
  23. std::istringstream iss(text);
  24. for (string word; iss >> word; )
  25. std::cout << transform(word, trans_map) << " ";
  26. std::cout << std::endl;
  27. }
  28. }
  29. int main()
  30. {
  31. ifstream ifs_map("../data/word_transformation_bad.txt"), ifs_content("../data/given_to_transform.txt");
  32. if (ifs_map && ifs_content) word_transform(ifs_map, ifs_content);
  33. else std::cerr << "can't find the documents." << std::endl;
  34. }

练习11.34

如果你将transform 函数中的find替换为下标运算符,会发生什么情况?

解:

如果使用下标运算符,当关键字未在容器中时,会往容器中添加一个新元素。

练习11.35

buildMap中,如果进行如下改写,会有什么效果?

  1. trans_map[key] = value.substr(1);
  2. //改为
  3. trans_map.insert({key, value.substr(1)});

解:

当一个转换规则的关键字多次出现的时候,使用下标运算符会保留最后一次添加的规则,而用insert则保留第一次添加的规则。

练习11.36

我们的程序并没检查输入文件的合法性。特别是,它假定转换规则文件中的规则都是有意义的。如果文件中的某一行包含一个关键字、一个空格,然后就结束了,会发生什么?预测程序的行为并进行验证,再与你的程序进行比较。

解:

如果关键字没有对应的规则,那么程序会抛出一个 runtime_error

练习11.37

一个无序容器与其有序版本相比有何优势?有序版本有何优势?

无序容器拥有更好的性能,有序容器使得元素始终有序。

练习11.38

unordered_map 重写单词计数程序和单词转换程序。

解:

  1. #include <unordered_map>
  2. #include <set>
  3. #include <string>
  4. #include <iostream>
  5. #include <fstream>
  6. #include <sstream>
  7. using std::string;
  8. void wordCounting()
  9. {
  10. std::unordered_map<string, size_t> word_count;
  11. for (string word; std::cin >> word; ++word_count[word]);
  12. for (const auto &w : word_count)
  13. std::cout << w.first << " occurs " << w.second << (w.second > 1 ? "times" : "time") << std::endl;
  14. }
  15. void wordTransformation()
  16. {
  17. std::ifstream ifs_map("../data/word_transformation.txt"), ifs_content("../data/given_to_transform.txt");
  18. if (!ifs_map || !ifs_content) {
  19. std::cerr << "can't find the documents." << std::endl;
  20. return;
  21. }
  22. std::unordered_map<string, string> trans_map;
  23. for (string key, value; ifs_map >> key && getline(ifs_map, value); )
  24. if (value.size() > 1) trans_map[key] = value.substr(1).substr(0, value.find_last_not_of(' '));
  25. for (string text, word; getline(ifs_content, text); std::cout << std::endl)
  26. for (std::istringstream iss(text); iss >> word; ) {
  27. auto map_it = trans_map.find(word);
  28. std::cout << (map_it == trans_map.cend() ? word : map_it->second) << " ";
  29. }
  30. }
  31. int main()
  32. {
  33. //wordCounting();
  34. wordTransformation();
  35. }