C++ Primer习题集之第十章:泛型算法(Chapter 10)

本章深入探讨了C++标准库中的泛型算法,涵盖基础的只读和写操作,利用lambda和bind定制操作,以及插入器、IO流迭代器和反向迭代器的使用。通过习题10.1~10.42,旨在帮助读者理解泛型编程的核心思想,即算法与数据结构的分离,并通过迭代器连接两者。
摘要由CSDN通过智能技术生成

导读

本章介绍了标准库算法,包括:

         基础的只读算法、写容器元素的算法。

         利用 lambda、bind 等技术手段定制操作。

         更深入的迭代器知识,如插入器、IO 流迭代器、反向迭代器。

         泛型算法对迭代器的分类和算法命名规范,以及特定容器算法。

本章练习的最重要目的是让读者深入理解“泛型”思想,体会标准库是如何通过 算法和数据结构的分离来实现泛型的,以及如何通过迭代器在分离的算法和数据结构 间架起桥梁,达到算法“不知”数据结构,但又能操纵数据元素的效果。具体内容包 括基础算法使用的练习、lambda 和定制操作的练习、插入器/IO 流迭代器/反向迭代 器的练习等。

习题 10.1 ~ 10.42

练习题中所需的头文件:#include "Sales_data.h"

#ifndef SALES_DATA_H_
#define SALES_DATA_H_

#include <string>
#include <iostream>

class Sales_data
{
	friend std::istream &read(std::istream &is, Sales_data &item);
	friend std::ostream &print(std::ostream &os, const Sales_data &item);
	friend Sales_data add(const Sales_data &lhs, const Sales_data &rhs);

public:
	Sales_data() = default;
	Sales_data(const std::string &s) :bookNo(s) {}
	Sales_data(const std::string &s, unsigned n, double p) :bookNo(s), units_sold(n), revenue(n*p) {}
	Sales_data(std::istream &is) { read(is, *this); }

	std::string isbn() const { return bookNo; };
	Sales_data& combine(const Sales_data&);

private:
	inline double avg_price() const;

private:
	std::string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
};

inline
double Sales_data::avg_price() const
{
	return units_sold ? revenue / units_sold : 0;
}

std::istream &read(std::istream &is, Sales_data &item);
std::ostream &print(std::ostream &os, const Sales_data &item);
Sales_data add(const Sales_data &lhs, const Sales_data &rhs);

#endif  // SALES_DATA_H_
练习10.1:头文件algorithm中定义了一个名为count的函数,它类似find,
接受一对迭代器和一个值作为参数。count 返回给定值在序列中出现的次数。编写
程序,读取 int 序列存入 vector 中,打印有多少个元素的值等于给定值。

【出题思路】
本题练习泛型算法的简单使用。

【解答】
泛型算法的使用其实很简单,记住关键一点:泛型算法不会直接调用容器的操
作,而是通过迭代器来访问、修改、移动元素。对于本题,将 vector 的 begin 和
end 传递给 count,并将要查找的值作为第三个参数传递给它即可。

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

int main()
{
    int val = 0;// 输入要搜索的整数
    vector<int> vi = { 1, 2, 3, 4, 5, 6, 3, 3, 2, 1 };

    cout << "请输入要搜索的整数:";
    cin >> val;

    cout << "该 vi 序列中包含" << count(vi.begin(), vi.end(), val) << "个" << endl;
    cout << "该 vi 序列中包含" << count(vi.cbegin(), vi.cend(), val) << "个" << endl;

    return 0;
}

【其他解题思路】
读者可以尝试自己编写计数整数出现次数的代码,并尝试回答如下问题:
1.与使用 count 相比,编程效率如何?代码复杂度如何?若问题更为复杂呢?
2.若需要计数给定的是单/双精度浮点数、字符串……在 vector、deque、
list……中出现的次数,自己编写程序的方式会怎样,使用 count 的方式又如何?
通过回答这两个问题来理解泛型算法的特点。
练习 10.2:重做上一题,但读取 string 序列存入 list 中。

【出题思路】
理解“泛型”的优点。

【解答】
可以看到,与上一题对比,程序的变化只在不同类型变量的声明上,而算法的
使用部分几乎没有任何差异。

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

int main()
{
    string word;
    list<string> ls = { "aa", "aaa", "aa", "cc" };

    cout << "请输入要搜索的字符串:";
    cin >> word;

    cout << "该序列中包含" << count(ls.begin(), ls.end(), word) << "个" << word << endl;

    return 0;
}
练习 10.3:用 accumulate 求一个 vector<int>中的元素之和。

【出题思路】
练习泛型算法的使用。

【解答】
Accumulate 的前两个参数仍然是指定范围的一对迭代器,第三个参数是和的
初值。

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

int main()
{
    vector<int> vi = { 1, 2, 3, 4 };
    cout << "序列中整数之和为" << accumulate(vi.begin(), vi.end(), 0) << endl;
    cout << "序列中整数之和为" << accumulate(vi.cbegin(), vi.cend(), 0) << endl;

    return 0;
}
练习 10.4:假定 v 是一个 vector<double>,那么调用 
accumulate(v.cbegin(), v.cend(), 0)有何错误(如果存在的话)?

【出题思路】
理解 accumulate 的第三个参数。

【解答】
accumulate 的第三个参数是和的初值,它还决定了函数的返回类型,以及函
数中使用哪个加法运算符。

因此,本题中的调用是错误的,第三个参数 0 告知 accumulate,和是整型的,
使用整型加法运算符。读者可尝试输入带小数的值,函数返回的是一个整数。
正确的调用方法是将 0.0 作为第三个参数传递给 accumulate。
读者可以修改、运行程序,观察运行结果。

#include <iostream>
#include <vector>
#include <numeric>
using namespace std;

int main()
{
	vector<double> vd = { 1.1, 0.5, 3.3 };
    cout << "序列中浮点数之和为:" << accumulate(vd.begin(), vd.end(), 0) << endl;
	cout << "序列中浮点数之和为:" << accumulate(vd.cbegin(), vd.cend(), 0) << endl;

	return 0;
}
练习 10.5:在本节对名册(roster)调用 equal 的例子中,如果两个名册中保存
的都是 C 风格字符串而不是 string,会发生什么?

【出题思路】
理解 equal 如何比较元素。

【解答】
equal 使用==运算符比较两个序列中的元素。string 类重载了==,可比较两
个字符串是否长度相等且其中元素对位相等。而 C 风格字符串本质是 char*类型,
用==比较两个 char*对象,只是检查两个指针值是否相等,即地址是否相等,而不
会比较其中字符是否相同。所以,只有当两个序列中的指针都指向相同的地址时,
equal 才会返回 true。否则,即使字符串内容完全相同,也会返回 false。
如下面的程序,q 中的每个字符串是 p 中字符串的拷贝,虽然内容相同,但是不
同对象指向不同地址,因此 equal 判定它们不等。而 r 中每个指针都是 p 中指针的
拷贝,指向相同的地址,因此 equal 判定它们相等。

#include <iostream>
#include <algorithm>
#include <string.h>
using namespace std;

int main(int argc, char* argv[])
{
    const char* p[] = { "Hello", "World", "!" };
    char* q[] = { strdup(p[0]), strdup(p[1]), strdup(p[2]) };
    const char* r[] = { p[0], p[1], p[2] };
    cout << equal(begin(p), end(p), q) << endl;
    cout << equal(begin(p), end(p), r) << endl;

    return 0;
}
练习 10.6:编写程序,使用 fill_n 将一个序列中的 int 值都设置为 0。

【出题思路】
练习使用 fill_n。

【解答】
fill_n 接受一个迭代器,指出起始位置,还接受一个整数,指出设置的元素数
目,第三个参数则是要设置的值。

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

int main()
{
    vector<int> vi{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    fill_n(vi.begin(), vi.size(), 0);

    for (auto i : vi)
    {
        cout << i << " ";
    }
    cout << endl;

    return 0;
}
练习 10.7:下面程序是否有错误?如果有,请改正。
(a)vector<int> vec; list<int> lst; int i;
    while (cin >> i)
        lst.push_back(i);
    copy(lst.cbegin(), lst.cend(), vec.begin());
(b)vector<int> vec;
    vec.reserve(10); // reverse 将在 9.4 节(第 318 页)介绍
    fill_n(vec.begin(), 10, 0);

【出题思路】
进一步理解泛型算法的特点。

【解答】
(a)是错误的。因为泛型算法的一个基本特点是:算法总是通过迭代器操作容器,
因此不能直接向/从容器添加、删除元素,无法改变容器大小。因此,对于 copy 算
法,要求目标序列至少要包含与源序列一样多的元素。而此程序中,vec 进行缺省
初始化,它是空的,copy 无法进行。如需改变容器大小,需要使用一类特殊的称为
插入器的迭代器。我们可以将第三个参数改为 back_inserter(vec),通过它,
copy 算法即可将 lst 中元素的拷贝插入到 vec 的末尾。

// 答案参考代码
#include <iostream>
#include <fstream>
#include <vector>
#include <list>
#include <algorithm>
using namespace std;

int main(int argc, char *argv[])
{
    ifstream in(argv[1]);
    if (!in) 
    {
        cout << "打开输入文件失败!" << endl;
        exit(1);
    }
    list<int> lst;
    vector<int> vec;
    int val;
    while (in >> val)
       lst.push_back(val);
    copy(lst.begin(), lst.end(), back_inserter(vec));
    cout << equal(lst.begin(), lst.end(), vec.begin()) << endl;
    for (auto iter = vec.begin(); iter != vec.end(); iter++)
       cout << *iter << " ";
    cout << endl;
    
    return 0;
}


(b)这段程序仍然是错误的。粗看起来,reserve 为 vec 分配了至少能容纳
10 个 int 的内存空间,调用 fill_n 时,vec 已有足够空间。但泛型算法对于容器
的要求并不是有足够的空间,而是足够的元素。此时 vec 仍然为空,没有任何元素。
而算法又不具备向容器添加元素的能力,因此 fill_n 仍然失败。这里,我们还是
需要使用 back_inserter 来让 fill_n 有能力向 vec 添加元素。其实,只有 0 有
能力做到这一点,空间大小并不是问题,容器都能根据需要自动扩容。

// 答案参考代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main(int argc, char *argv[])
{
    vector<int> vec;
    vec.reserve(10);
    fill_n(back_inserter(vec), 10, 0);

    for (auto iter = vec.begin(); iter != vec.end(); iter++)
        cout << *iter << " ";
    cout << endl;

    return 0;
}
练习 10.8:本节提到过,标准库算法不会改变它们所操作的容器的大小。为什
么使用 back_inserter 不会使这一断言失效?

【出题思路】
深入理解泛型算法的这一特点。

【解答】
    严格来说,标准库算法根本不知道有“容器”这个东西。它们只接受迭代器参
数,运行于这些迭代器之上,通过这些迭代器来访问元素。

    因此,当你传递给算法普通迭代器时,这些迭代器只能顺序或随机访问容器中
的元素,造成的效果就是算法只能读取元素、改变元素值、移动元素,但无法添加
或删除元素。

    但当我们传递给算法插入器,例如 back_inserter 时,由于这类迭代器能调
用下层容器的操作来向容器插入元素,造成的算法执行的效果就是向容器中添加了
元素。

    因此,关键要理解&
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值