C++ Primer 5th 随堂练习
【C++ Primer】第三章 字符串、向量和数组 (练习)
第十一章 泛型算法
练习 11.1
描述 map 和 vector 的不同。
解答
两类容器的根本差别在于,顺序容器中的元素是 “顺序” 存储的(链表容器中的元素虽然不是在内存中 “连续” 存储的,但仍然是按 “顺序” 存储的)。理解 “顺序” 的关键,是理解容器支持的操作形式以及效率。
对 vector 这种 顺序容器,元素在其中顺序存储,每个元素都有唯一对应的位置编号,所有操作都按索引/下标编号(位置)进行。例如,访问/获取元素(头、尾、用下标获取任意位置)、插入删除元素(头、尾、任意位置)、遍历元素(按元素位置顺序逐一访问)。底层的数据结构是 数组、链表,简单但已能保证上述操作的高效。而对于依赖值的元素访问,例如查找(搜索)给定值(find),需要通过遍历完成,效率不佳 (时间复杂度O(n))。
而 map 这种 关联容器,就是为了高效实现 “按值访问元素” 这类操作而设计的,元素在其中按关键字存储,关键字与元素值建立起对应关系 ——“关联”。底层数据结构是 红黑树、哈希表 等,可高效实现按关键字查找、添加、删除元素值等操作 (时间复杂度O(1))。
练习 11.2
分别给出最适合使用 list、vector、deque、map 以及 set 的例子。
解答
若元素很小(如 int),大致数量预先可知,在程序运行过程中不会剧烈变化,大部分情况下只在末尾添加或删除,需要频繁访问任意位置的元素,则 vector 效率最高。若需要频繁在头部和尾部增删元素,则 deque 为最佳选择。
若元素较大(如大的类对象),数量预先未知,或是程序运行过程中频繁变化,对元素的访问更多是顺序访问全部或很多元素,则 list 很合适。
map 属于 哈希映射 类型,适合对一些对象按它们的某个特征进行访问的情形。例如:按学生的姓名来查询学生信息,即可将学生姓名作为关键字,将学生信息作为元素值,保存在 map 中。再比如统计单词出现的次数等。
set 属于 哈希集合 类型。当需要保存特定的值集合 —— 通常是满足/不满足某种要求的值集合,用 set 最方便,如黑名单,字母表等。
练习 11.3
编写你自己的单词计数程序。
解答
源程序:
#include <iostream>
#include <algorithm>
#include <map>
#include <string>
#include <cctype>
using namespace std;
bool is_word(const string word)
{
for (auto c : word) {
if (!isalpha(c)) // 若任一字符不为字母, 则不是单词
return false;
}
return true;
}
int main(int argc, char *argv[])
{
map<string, int> word_count; // 哈希字典
// 记录
string word;
while (cin >> word) {
if (is_word(word)) // 只记录字母
++word_count[word]; // 相当于 Python 中默认类型为 string 的默认字典 defaultdict
}
// 打印
for (const auto w : word_count) {
cout << w.first << " occurs " << w.second << " time(s)" << endl;
}
system("pause");
return 0;
}
控制台交互:
hello world my dear lady 666 you are my entire world
^Z
are occurs 1 time(s)
dear occurs 1 time(s)
entire occurs 1 time(s)
hello occurs 1 time(s)
lady occurs 1 time(s)
my occurs 2 time(s)
world occurs 2 time(s)
you occurs 1 time(s
练习 11.4
扩展你的程序,忽略大小写和标点。例如,"example."、"example," 和 "Example" 应该递增相同的计数器。
解答
源程序:(存在问题)
#include <iostream>
#include <algorithm>
#include <map>
#include <string>
#include <cctype>
using namespace std;
void word_count_plus(map<string, int> &m)
{
// 输入内容
string word;
while (cin >> word) {
// 字母小写化
for (auto &ch : word)
ch = tolower(ch);
// 删除标点
// 注意:必须写成 ::ispunct 而不能仅为 ispunct !!!
word.erase(remove_if(word.begin(), word.end(), ::ispunct), word.end());
// 计数加一
++m[word];
}
// 打印结果
for (const auto w : m)
cout << w.first << " : " << w.second << endl;
}
int main(int argc, char *argv[])
{
map<string, int> word_count;
word_count_plus(word_count);
system("pause");
return 0;
}
小贴士
remove_if():移除序列 [start, end) 中所有使谓词 p 为 true 的元素,返回一个指向被修剪的序列的最后一个元素迭代器。
注意, remove_if() 并 不会真正地移除 序列范围 [start, end) 中的元素,若在一个容器上应用 remove_if(),容器长度并不会改变(remove_if() 不可能仅通过迭代器改变容器的属性),所有的元素都还在容器中。实际做法是,remove_if() 将所有应该移除的元素都移动到了容器尾部并返回一个 分界迭代器。“移除” 的所有元素仍可通过返回的分界迭代器访问到。
为实际移除元素,必须再对容器调用 erase() 以 “擦除” 元素,这也是 erase-remove idiom 名称的由来:
container.erase(remove_if(container.begin(), container.end(), pred), container.end());
remove_if() 以线性时间运行。
remove_if() 不能用于关联容器,如 set<> 或 map<>。
remove_if() 类似于 partition(),但有所差异:
partition():接受一个谓词,对容器元素进行划分,使得谓词为 true 的值排在容器的前半部分,谓词为 false 的值排在后半部分,返回一个迭代器,指向最后一个使谓词为 true 的元素之后的位置。
控制台交互:
Hello world my dear lady. 666. you are my entire world.
^Z
666 : 1
are : 1
dear : 1
entire : 1
hello : 1
lady : 1
my : 2
world : 2
you : 1