《C++ Primer》第10章 泛型算法
10.4节再探迭代器 习题答案
练习10.26:解释三种插入迭代器的不同之处。
【出题思路】理解插入迭代器的概念,以及几种插入迭代器的不同。
【解答】
在书中前文,我们已经学习了一种插入迭代器back_inserter。插入迭代器又称插入器,本质上是一种迭代器适配器。如前所述,标准库算法为了保证通用性,并不直接操作容器,而是通过迭代器来访问容器元素。因此,算法不具备直接向容器插入元素的能力。而插入器正是帮助算法实现向容器插入元素的机制。
除了back_inserter,标准库还提供了另外两种插入器:front_inserter和inserter。三者的差异在于如何向容器插入元素:back_inserter调用push_back,front_inserter调用push_front,inserter则调用insert。显然,这也决定了它们插入元素位置的不同。back_inserter总是插入到容器尾元素之后,front_inserter总是插入到容器首元素之前,而inserter则是插入到给定位置(作为inserter的第二个参数传递给它)之前。因此,需要注意这些特点带来的元素插入效果的差异。例如,使用front_inserter向容器插入一些元素,元素最终在容器中的顺序与插入顺序相反,但back_inserter和inserter则不会有这个问题。
练习10.27:除了unique(参见10.2.3节,第343页)之外,标准库还定义了名为unique_copy的函数,它接受第三个迭代器,表示拷贝不重复元素的目的位置。编写一个程序,使用unique_copy将一个vector中不重复的元素拷贝到一个初始为空的list中。
【出题思路】
本题练习unique_copy的使用,以及使用插入迭代器帮助算法(unique_copy)实现向容器插入新元素。
【解答】
本题要求将vector中不重复元素按原顺序拷贝到空list中,因此使用back_inserter即可。需要注意的是,与unique一样,unique_copy也要求在源容器中重复元素是相邻存放的。因此,若vector未排序且重复元素未相邻存放,unique_copy就会失败。稳妥的方法是先对vector排序。
#include <iostream>
#include <vector>
#include <list>
#include <algorithm>
using namespace std;
int main(int argc, const char *argv[])
{
vector<int> vi = {1, 2, 2, 3, 4, 5, 5, 5, 6};
list<int> li;
unique_copy(vi.begin(), vi.end(), back_inserter(li));
for(auto v: li)
cout << v << " ";
cout << endl;
return 0;
}
运行结果:
练习10.28:一个vector中保存1到9共9个值,将其拷贝到三个其他容器中。分别使用inserter、back_inserter和front_inserter将元素添加到三个容器中。对每种inserter,估计输出序列是怎样的,运行程序验证你的估计是否正确。
【出题思路】
进一步理解三种插入迭代器的差异,并练习使用它们。
【解答】
若三个目的容器均为空,则显然inserter和back_inserter的输出结果是"1 2 3 4 56 7 8 9",而front_inserter的结果是"9 8 7 6 5 4 3 2 1"。但如果目的容器不空,则inserter的结果取决于传递给它的第二个参数(一个迭代器)指向什么位置。
#include <iostream>
#include <vector>
#include <list>
#include <algorithm>
#include <iterator>
using namespace std;
int main(int argc, const char *argv[])
{
vector<int> vi = {1, 2, 3, 4, 5, 6, 7, 8, 9};
list<int> li1, li2, li3;
unique_copy(vi.begin(), vi.end(), inserter(li1, li1.begin()));
for(auto v:li1)
cout << v << " ";
cout << endl;
unique_copy(vi.begin(), vi.end(), back_inserter(li2));
for(auto v:li2)
cout << v << " ";
cout << endl;
unique_copy(vi.begin(), vi.end(), front_inserter(li3));
for(auto v:li3)
cout << v << " ";
cout << endl;
return 0;
}
运行结果:
练习10.29:编写程序,使用流迭代器读取一个文本文件,存入一个vector中的string里。
【出题思路】本题练习流迭代器的简单使用。
【解答】
虽然流不是容器,但标准库提供了通过迭代器访问流的方法。声明一个流迭代器时,需指出所绑定的流。对于本题,首先打开一个文本文件,将此文件的流作为参数提供给流迭代器的构造函数即可。当默认构造流迭代器时,得到一个尾后迭代器,对应文件结束。
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <iterator>
#include <algorithm>
using namespace std;
int main(int argc, const char *argv[])
{
ifstream in(argv[1]);
if(!in)
{
cout << "打开输入文件失败!" << endl;
exit(1);
}
//创建流迭代器从文件读入字符串
istream_iterator<string> in_iter(in);
//尾后迭代器
istream_iterator<string> eof;
vector<string> words;
while(in_iter != eof)
words.push_back(*in_iter++); //存入vector并递增迭代器
for(auto word: words)
cout << word << " ";
cout << endl;
return 0;
}
运行结果:
练习10.30:使用流迭代器、sort和copy从标准输入读取一个整数序列,将其排序,并将结果写到标准输出。
【出题思路】
本题练习输出流迭代器的使用。
【解答】
使用流迭代器从标准输入读取整数序列的程序与上一题类似,创建流迭代器时指出是int,并用cin代替文件流对象即可。用copy将整数写到标准输出,需要声明一个输出流迭代器,作为第三个参数传递给copy。将cin传递给输出流迭代器的构造函数,copy即可将整数写到标准输出。将" "作为第二个参数传递给输出流迭代器的构造函数,表示在每个输出之后写一个空格,从而将整数分隔开来输出。
#include <iostream>
#include <fstream>
#include <vector>
#include <iterator>
#include <algorithm>
using namespace std;
int main(int argc, const char *argv[])
{
cout << "输入整数" << endl;
//创建流迭代器从标准输入整数
istream_iterator<int> in_iter(cin);
//尾后迭代器
istream_iterator<int> eof;
vector<int> vi;
while(in_iter != eof)
vi.push_back(*in_iter++); //存入vector并递增迭代器
sort(vi.begin(), vi.end());
ostream_iterator<int> out_iter(cout, " ");
copy(vi.begin(), vi.end(), out_iter);
cout << endl;
// for(auto v: vi)
// cout << v << " ";
// cout << endl;
return 0;
}
运行结果:
练习10.31:修改前一题的程序,使其只打印不重复的元素。你的程序应使用unique_copy(参见10.4.1节,第359页)。
【出题思路】
继续练习输出流迭代器的使用,并复习unique_copy的使用。
【解答】用unique_copy替代上题的copy即可。
#include <iostream>
#include <fstream>
#include <vector>
#include <iterator>
#include <algorithm>
using namespace std;
int main(int argc, const char *argv[])
{
cout << "输入整数" << endl;
//创建流迭代器从标准输入整数
istream_iterator<int> in_iter(cin);
//尾后迭代器
istream_iterator<int> eof;
vector<int> vi;
while(in_iter != eof)
vi.push_back(*in_iter++); //存入vector并递增迭代器
sort(vi.begin(), vi.end());
ostream_iterator<int> out_iter(cout, " ");
unique_copy(vi.begin(), vi.end(), out_iter);
cout << endl;
// for(auto v: vi)
// cout << v << " ";
// cout << endl;
return 0;
}
运行结果:
练习10.32:重写1.6节(第21页)中的书店程序,使用一个vector保存交易记录,使用不同算法完成处理。使用sort和10.3.1节(第345页)中的compareIsbn函数来排序交易记录,然后使用find和accumulate求和。
【出题思路】
继续练习流迭代器的使用,并复习算法的使用。
【解答】
读取交易记录存入vector的程序与前两题类似。
将compareIsbn作为第三个参数传递给sort,即可实现将交易记录按ISBN排序。本题要求将ISBN相同的交易记录累加,而find算法查找与给定值相同的容器元素,需要逐个查找需要累加的元素,显然性能太差。我们使用find_if,并构造如下lambda:
lambda:[item](const Sales_item &item1) {return item1.isbn() != item.isbn();}
作为第三个参数传递给find_if,从而查找到第一个ISBN与当前交易l记录不同的记录r(ISBN相同的范围的尾后位置)。接下来调用accumulate即可实现范围[l, r)间交易记录的累加。最后将l移动到r,继续循环,计算下一段交易记录的查找和累加。
#ifndef SALES_ITEM_H
#define SALES_ITEM_H
#include <iostream>
#include <string>
class Sales_item {
// these declarations are explained section 7.2.1, p. 270
// and in chapter 14, pages 557, 558, 561
friend std::istream& operator>>(std::istream&, Sales_item&);
friend std::ostream& operator<<(std::ostream&, const Sales_item&);
friend bool operator<(const Sales_item&, const Sales_item&);
friend bool
operator==(const Sales_item&, const Sales_item&);
public:
// constructors are explained in section 7.1.4, pages 262 - 265
// default constructor needed to initialize members of built-in type
Sales_item() = default;
Sales_item(const std::string &book): bookNo(book) { }
Sales_item(std::istream &is) { is >> *this; }
public:
// operations on Sales_item objects
// member binary operator: left-hand operand bound to implicit this pointer
Sales_item& operator+=(const Sales_item&);
// operations on Sales_item objects
std::string isbn() const { return bookNo; }
double avg_price() const;
// private members as before
private:
std::string bookNo; // implicitly initialized to the empty string
unsigned units_sold = 0; // explicitly initialized
double revenue = 0.0;
};
// used in chapter 10
inline
bool compareIsbn(const Sales_item &lhs, const Sales_item &rhs)
{
return lhs.isbn() < rhs.isbn();
}
// nonmember binary operator: must declare a parameter for each operand
Sales_item operator+(const Sales_item&, const Sales_item&);
inline bool
operator==(const Sales_item &lhs, const Sales_item &rhs)
{
// must be made a friend of Sales_item
return lhs.units_sold == rhs.units_sold &&
lhs.revenue == rhs.revenue &&
lhs.isbn() == rhs.isbn();
}
inline bool
operator!=(const Sales_item &lhs, const Sales_item &rhs)
{
return !(lhs == rhs); // != defined in terms of operator==
}
// assumes that both objects refer to the same ISBN
Sales_item& Sales_item::operator+=(const Sales_item& rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
// assumes that both objects refer to the same ISBN
Sales_item
operator+(const Sales_item& lhs, const Sales_item& rhs)
{
Sales_item ret(lhs); // copy (|lhs|) into a local object that we'll return
ret += rhs; // add in the contents of (|rhs|)
return ret; // return (|ret|) by value
}
std::istream&
operator>>(std::istream& in, Sales_item& s)
{
double price;
in >> s.bookNo >> s.units_sold >> price;
// check that the inputs succeeded
if (in)
s.revenue = s.units_sold * price;
else
s = Sales_item(); // input failed: reset object to default state
return in;
}
std::ostream&
operator<<(std::ostream& out, const Sales_item& s)
{
out << s.isbn() << " " << s.units_sold << " "
<< s.revenue << " " << s.avg_price();
return out;
}
double Sales_item::avg_price() const
{
if (units_sold)
return revenue/units_sold;
else
return 0;
}
#endif // SALES_ITEM_H
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <iterator>
#include <algorithm>
#include <numeric>
#include "Sales_item.h"
using namespace std;
int main(int argc, const char *argv[])
{
ifstream in(argv[1]);
if(!in)
{
cout << "打开输入文件失败!" << endl;
exit(1);
}
//创建流迭代器从文件读入字符串
istream_iterator<string> in_iter(in);
//尾后迭代器
istream_iterator<string> eof;
vector<Sales_item> vs;
//读入交易记录,存入vector
while(in_iter != eof)
vs.push_back(*in_iter++);
if(vs.empty())
{
//设没有输入!读取数据错误
cerr << "No data?!" << endl;
return -1;//表示读取数据失败
}
//将交易记录接ISBN排序
sort(vs.begin(), vs.end(), compareIsbn);
auto start = vs.begin();
while(start != vs.end())
{
auto item = *start;//相同ISBN的交易记录中的第一条
//在后续记录中查找第一个ISBN与item不同者
auto r = find_if(start + 1, vs.end(), [item] (const Sales_item &item1) {return item1.isbn() != item.isbn();});
//将范围[start, r)间的交易记录累加并输出
cout << accumulate(start + 1, r, item) << endl;
//start指向下一个段交易记录中的第一个
start = r;
}
cout << endl;
return 0;
}
data10_32.txt文件内容为:
0-201-70353-X 24.99
0-201-82470-1 45.39
0-201-88954-4 15.00
0-201-88954-4 12.00
0-201-88954-4 12.00
0-201-88954-4 12.00
0-399-82477-1 45.39
0-399-82477-1 45.39
0-201-78345-X 20.00
0-201-78345-X 25.00
设置命令行参数,运行结果如下:
练习10.33:编写程序,接受三个参数:一个输入文件和两个输出文件的文件名。输入文件保存的应该是整数。使用istream_iterator读取输入文件。使用ostream_iterator将奇数写入第一个输出文件,每个值之后都跟一个空格。将偶数写入第二个输出文件,每个值都独占一行。
【出题思路】
本题通过一个稍大的例子巩固输入和输出流迭代器的使用。
【解答】
程序从命令行接受三个文件名参数,因此程序首先判断参数数目是否为4(包括程序名)。
然后依次打开输入文件和两个输出文件,再用这三个流初始化一个输入流迭代器和两个输出流迭代器。注意,第一个流迭代器输出以空格间隔的奇数,将" "作为第二个参数传递给构造函数;第二个流迭代器输出以换行间隔的偶数,将"\n"作为第二个参数传递给构造函数。
随后在循环中通过输入流迭代器读取文件中的整数,直至到达文件末尾。读入每个整数后,判断它是奇数还是偶数,分别写入两个输出文件。
#include <iostream>
#include <fstream>
#include <vector>
#include <iterator>
#include <algorithm>
using namespace std;
int main(int argc, const char *argv[])
{
if(4 != argc)
{
cout << "用法:在命令行参数里添加 in_file out_file1 out_file2" << endl;
return -1;
}
ifstream in(argv[1]);
if(!in)
{
cout << "打开输入文件失败!" << endl;
exit(1);
}
ofstream out1(argv[2]);
if(!out1)
{
cout << "打开输出文件1失败!" << endl;
exit(1);
}
ofstream out2(argv[3]);
if(!out2)
{
cout << "打开输出文件2失败!" << endl;
exit(1);
}
//创建流迭代器从文件读入整数
istream_iterator<int> in_iter(in);
//尾后迭代器
istream_iterator<int> eof;
//第一个输出文件以空格间隔整数
ostream_iterator<int> out_iter1(out1, " ");
//第二个输出文件以换行间隔整数
ostream_iterator<int> out_iter2(out2, "\n");
while(in_iter != eof)
{
if(*in_iter & 1) //奇数写入第一个输出文件
*out_iter1++ = *in_iter;
else //偶数写入第二个输出文件
*out_iter2++ = *in_iter;
in_iter++;
}
cout << "run success==========" << endl;
return 0;
}
设置命令行参数,
运行结果如下:
练习10.34:使用reverse_iterator逆序打印一个vector。
【出题思路】
本题练习反向迭代器的简单使用。
【解答】
我们可以用(c)rbegin获取反向遍历的起始位置(其实是容器的末尾元素位置),用(c)rend获取尾后迭代器(首元素之前的位置)。通过这两个迭代器控制循环,即可实现对容器的反向遍历。注意,循环中向前移动迭代器仍然用++,而非--。
#include <iostream>
#include <fstream>
#include <vector>
#include <algorithm>
using namespace std;
int main(int argc, const char *argv[])
{
if(2 != argc)
{
cout << "在命令行添加命令参数 文件名路径" << endl;
return -1;
}
ifstream in(argv[1]);
if(!in)
{
cout << "打开输入文件失败!" << endl;
exit(1);
}
vector<int> vi;
int v;
while(in >> v)//从文件中读取整数
{
vi.push_back(v);
}
cout << "原始顺序:" << endl;
for(auto iter = vi.cbegin(); iter != vi.cend(); ++iter)
cout << *iter << " ";
cout << endl;
cout << "新的顺序:" << endl;
for(auto r_iter = vi.crbegin(); r_iter != vi.crend(); ++r_iter)
cout << *r_iter << " ";
cout << endl;
return 0;
}
data10_34.txt文件内容如下:
12 3 65 69 8 74 96 64 52 15 16 68
设置命令行参数,运行结果如下:
练习10.35:使用普通迭代器逆序打印一个vector。
【出题思路】
体会反向迭代器和普通迭代器的差异。
【解答】
若使用普通迭代器反向遍历容器,首先通过cend获得容器的尾后迭代器,循环中递减该迭代器,直到它与begin相等为止。但需要注意的是,遍历所用迭代器的初值为尾后位置,终值为begin之后的位置。也就是说,在每个循环步中,它指向的都是我们要访问的元素之后的位置。因此,我们在循环中首先将其递减,然后通过它访问容器元素,而在循环语句的第三个表达式中就不再递减迭代器了。显然,对于反向遍历容器,使用反向迭代器比普通迭代器更清晰易懂。
#include <iostream>
#include <fstream>
#include <vector>
#include <algorithm>
using namespace std;
int main(int argc, const char *argv[])
{
if(2 != argc)
{
cout << "在命令行添加命令参数 文件名路径" << endl;
return -1;
}
ifstream in(argv[1]);
if(!in)
{
cout << "打开输入文件失败!" << endl;
exit(1);
}
vector<int> vi;
int v;
while(in >> v)//从文件中读取整数
{
vi.push_back(v);
}
cout << "原始顺序:" << endl;
for(auto iter = vi.cbegin(); iter != vi.cend(); ++iter)
cout << *iter << " ";
cout << endl;
cout << "新的顺序:" << endl;
for(auto r_iter = vi.cend(); r_iter != vi.begin();)
cout << *(--r_iter) << " ";
cout << endl;
return 0;
}
运行结果:
练习10.36:使用find在一个int的list中查找最后一个值为0的元素。
【出题思路】
练习反向迭代器和算法的结合。
【解答】
借助反向迭代器,可以扩展算法的能力。例如,使用普通迭代器,find能查找给定值在容器中第一次出现的位置。如果要查找最后一次出现的位置,还使用普通迭代器的话,代码会很复杂。但借助反向迭代器,find可以逆序遍历容器中的元素,从而“第一次出现位置”实际上也就是正常顺序的最后一次出现位置了。
注意:
1.由于list是链表数据结构,元素不连续存储,其迭代器不支持算术运算。因此,程序中用一个循环来计数位置编号。
2.由于程序计数的是正向位置编号,因此,需要将find找到的反向迭代器last_z转换为普通迭代器(使用base成员函数)。但要注意,反向迭代器与普通迭代器的转换是左闭合区间的转换,而非精确位置的转换。last_z.base()指向的并非最后一个0,而是它靠近容器尾方向的邻居。因此,首先将last_z向容器首方向推进一个位置(++),然后再调用base,得到的就是指向最后一个0的普通迭代器了。读着可尝试对本例画出类似图10.2所示的迭代器位置关系图。
#include <iostream>
#include <list>
#include <algorithm>
using namespace std;
int main()
{
list<int> li = {0, 1, 2, 0, 3, 4, 5, 0, 6};
//利用反向迭代器查找最后一个0
auto last_z = find(li.rbegin(), li.rend(), 0);
//将迭代器向链表头方向推进一个位置,转换为普通迭代器时,将回到最后一个0的位置
last_z++;
int p = 1;
//用base将last_z转换为普通迭代器,从链表头开始遍历,计数最后一个0的编号
for(auto iter = li.begin(); iter != last_z.base(); iter++, p++)
;
if(p >= li.size()) //未找到0
cout << "容器中没有0" << endl;
else
cout << "最后一个0在第 " << p << " 个位置。" << endl;
return 0;
}
运行结果:
练习10.37:给定一个包含10个元素的vector,将位置3到7之间的元素按逆序拷贝到一个list中。
【出题思路】
深入理解反向迭代器和普通迭代器间的差异及相互转换。
【解答】
反向迭代器和普通迭代器的转换是左闭合区间的转换。
对10个元素的vector vi,包含位置3〜7之间元素的迭代器区间如下所示:
0 1 2 3 4 5 6 7 8 9
i1 i2
第一个迭代器是vi.begin()+2,第二个迭代器指向位置8,即vi.begin()+7,当将这两个迭代器转换为反向迭代器时,位置如下:
0 1 2 3 4 5 6 7 8 9
re rb
虽然与正向迭代器的位置不同,但左闭合区间[rb, re)仍然对应位置3〜7之间的元素。显然,普通-反向迭代器间的这种错位,恰恰是因为标准库的范围概念是左闭合区间造成的。
另外,注意back_inserter和流迭代器的使用。
#include <iostream>
#include <vector>
#include <list>
#include <iterator>
#include <algorithm>
using namespace std;
int main()
{
ostream_iterator<int> out_iter(cout, " ");
vector<int> vi = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
//用流迭代器和copy输出int序列
copy(vi.begin(), vi.end(), out_iter);
cout << endl;
list<int> li;
//vi[2],也就是第3个元素的位置转换为反向迭代器
vector<int>::reverse_iterator re(vi.begin() + 2);
//vi[7],也就是第8个元素的位置转换为反向迭代器
vector<int>::reverse_iterator rb(vi.begin() + 7);
//用反向迭代器将元素逆序拷贝到list
copy(rb, re, back_inserter(li));
copy(li.begin(), li.end(), out_iter);
cout << endl;
return 0;
}
运行结果: