《C++ Primer》第9章 顺序容器
9.3节顺序容器操作习题答案
练习9.18:编写程序,从标准输入读取string序列,存入一个deque中。编写一个循环,用迭代器打印deque中的元素。
【出题思路】
本题练习向容器中添加元素,继续练习遍历容器中的元素。重点:不同容器在不同位置添加元素的性能是有差异的。
【解答】
对deque来说,在首尾位置添加新元素性能最佳,在中间位置插入新元素性能会很差。对遍历操作,可高效完成。
#include <iostream>
#include <deque>
#include <string>
using namespace std;
int main()
{
deque<string> sd; //string的deque
string word;
while(cin >> word) //读取字符串,直至遇到文件结束符
{
sd.push_back(word);
}
cout << "word===" << word << endl;
cout << "sd.size====" << sd.size() << endl;
//用cbegin()获取deque首元素迭代器,遍历deque中所有元素
for(auto si = sd.cbegin(); si != sd.cend(); si++)
cout << *si << endl;
return 0;
}
运行结果:
练习9.19:重写上题的程序,用list替代deque。列出程序要做出哪些改变。
【出题思路】
练习不同容器的添加操作的异同。
【解答】
对list来说,在任何位置添加新元素都有很好的性能,遍历操作也能高效完成,因此程序与上一题并无太大差异。
#include <iostream>
#include <list>
#include <string>
using namespace std;
int main()
{
list<string> sd; //string的deque
string word;
while(cin >> word) //读取字符串,直至遇到文件结束符
{
sd.push_back(word);
}
//用cbegin()获取deque首元素迭代器,遍历deque中所有元素
for(auto si = sd.cbegin(); si != sd.cend(); ++si)
cout << *si << endl;
cout << "hello world" << endl;
return 0;
}
运行结果:
练习9.20:编写程序,从一个list<int>拷贝元素到两个deque中。值为偶数的所有元素都拷贝到一个deque中,而奇数值元素都拷贝到另一个deque中。
【出题思路】
这是一个很简单的数据处理问题的练习。读者可练习多个容器间数据的处理、拷贝。
【解答】
通过遍历list<int>,可检查其中每个元素的奇偶性,并用push_back分别添加到目的deque的末尾。程序中用位与运算检查元素最低位的值,若为1,表明是奇数,否则即为偶数。
#include <iostream>
#include <deque>
#include <list>
using namespace std;
int main()
{
list<int> ilist = {1, 2, 3, 4, 5, 6, 7, 8};//初始化整数list
deque<int> odd_d, even_d;
//遍历整数list
for(auto iter = ilist.cbegin(); iter != ilist.cend(); ++iter)
{
if(*iter & 1)//查看最低位,1:奇数,0:偶数
odd_d.push_back(*iter);
else
even_d.push_back(*iter);
}
cout << "奇数值有:";
for(auto iter = odd_d.cbegin(); iter != odd_d.cend(); ++iter)
cout << *iter << " ";
cout << endl;
cout << "偶数值有:";
for(auto iter = even_d.cbegin(); iter != even_d.cend(); ++iter)
cout << *iter << " ";
cout << endl;
return 0;
}
运行结果:
练习9.21:如果我们将第308页中使用insert返回值将元素添加到list中的循环程序改写为将元素插入到vector中,分析循环将如何工作。
【出题思路】
本题练习用insert向容器中添加元素的方法。理解这是最通用的方法,可以实现push_back和push_front这些特殊插入操作的效果。
【解答】
在循环之前,vector为空,此时将iter初始化为vector首位置,与初始化为尾后位置效果是一样的。循环中第一次调用insert会将读取的第一个string插入到iter指向位置之前的位置,即,令新元素成为vector的首元素。而insert的返回指向此元素的迭代器,我们将它赋予iter,从而使得iter始终指向vector的首元素。接下来的每个循环步均是如此,将新string插入到vector首元素之前的位置,成为新的首元素,并使iter始终指向vector首。这样,string在vector排列的顺序将与它们的输入顺序恰好相反。整个循环执行的过程和最后的结果都与list版本没有什么区别。但要注意,在list首元素之前插入新元素性能很好,但对于vector,这样的操作需要移动所有现有元素,导致性能很差。
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int main()
{
vector<string> svec;//string的vector
string word;
auto iter = svec.begin();//获取vector首位置迭代器
while(cin >> word) //读取字符串,直至遇到文件结束符
{
//cout << "iter old======" << *iter << endl;
iter = svec.insert(iter, word);//更新iter的位置,指向的新的元素
cout << "iter new======" << *iter << endl;
}
cout << "svec.size======" << svec.size() << endl;
//用cbegin()获取vector首元素迭代器,遍历vector中所有元素
for(auto iter = svec.cbegin(); iter != svec.cend(); ++iter)
{
cout << "*iter=====" << *iter << endl;
}
cout << endl;
return 0;
}
运行结果:
练习9.22:假定iv是一个int的vector,下面的程序存在什么错误?你将如何修改?
vector<int>::iterator iter = iv.begin(), mid = iv.begin() + iv.size() / 2;
while(iter != mid)
if(*iter == som_val)
iv.insert(iter, 2 * some_val);
【出题思路】
首先,理解容器插入操作的副作用——向一个vector、string或deque插入元素会使现有指向容器的迭代器、引用和指针失效。其次,练习如何利用insert返回的迭代器,使得在向容器插入元素后,仍能正确在容器中进行遍历。
【解答】
循环中未对iter进行递增操作,iter无法向中点推进。其次,即使加入了iter++语句,由于向iv插入元素后,iter已经失效,iter++也不能起到将迭代器向前推进一个元素的作用。修改方法如下:
首先,将insert返回的迭代器赋予iter,这样,iter将指向新插入的元素y。我们知道,insert将y插入到iter原来指向的元素x之前的位置,因此,接下来我们需要进行两次iter++才能将iter推进到x之后的位置。其次,insert()也会使mid失效,因此,只正确设置iter仍不能令循环在正确的时候结束,我们还需设置mid使之指向iv原来的中央元素。在未插入任何新元素之前,此位置是iv.begin()+iv.size()/2,我们将此时的iv.size()的值记录在变量org_size中。然后在循环过程中统计新插入的元素的个数new_ele,则在任何时候,iv.begin()+org_size/2+new_ele都能正确指向iv原来的中央元素。
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> ivec = {1, 1, 2, 1};//int的vector
int some_val = 1;
vector<int>::iterator iter = ivec.begin();
int org_size = ivec.size(), new_ele = 0;//原大小和新素个数
//每个循环步都重新计算“mid”,保证正确指向ivec中原中央元素
while(iter != (ivec.begin() + org_size / 2) + new_ele)
{
if(*iter == some_val)
{
iter = ivec.insert(iter, 2 * some_val);//iter指向新元素
new_ele++;
iter++;
iter++;//将iter推进到旧元素的下一个位置
}
else
iter++;//简单推进iter
}
//用begin()犯获取vector首元素迭代器,遍历vector中的所有元素
for(iter = ivec.begin(); iter != ivec.end(); ++iter)
{
cout << "*iter============" << *iter << endl;
}
return 0;
}
运行结果:
练习9.23:在本节第一个程序(第309页)中,若c.size()为1,则val、val2、val3和val4的值会是什么?
【出题思路】
理解获取容器首、尾元素的不同方法。
【解答】
4个变量的值会一样,都等于容器中唯一一个元素的值。
练习9.24:编写程序,分别使用at、下标运算符、front和begin提取一个vector中的第一个元素。在一个空vector上测试你的程序。
【出题思路】
练习获取容器首元素的不同方法,以及如何安全访问容器元素。
【解答】
下面的程序会异常终止。因为vector为空,此时用at访问容器的第一个元素会抛出一个out_of_range异常,而此程序未捕获异常,因此程序会因异常退出。正确的编程方式是,捕获可能的out_of_range异常,进行相应的处理。但对于后三种获取容器首元素的方法,当容器为空时,不会抛出out_of_range异常,而是导致程序直接退出(注释掉前几条语句即可看到后面语句的执行效果)。因此,正确的编程方式是,在采用这几种获取容器的方法时,检查下标的合法性(对front和begin只需检查容器是否为空),确定没有问题后再获取元素。当然这种方法对at也适用。
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> iv; //int的vector
cout << iv.at(0) << endl;
cout << iv[0] << endl;
cout << iv.front() << endl;
cout << *(iv.begin()) << endl;
return 0;
}
练习9.25:对于第312页中删除一个范围内的元素的程序,如果elem1与elem2相等会发生什么?如果elem2是尾后迭代器,或者elem1和elem2皆为尾后迭代器,又会发生什么?
【出题思路】
理解范围删除操作的两个迭代器参数如何决定删除操作的结果。
【解答】
如果两个迭代器elem1和elem2相等,则什么也不会发生,容器保持不变。哪怕两个迭代器是指向尾后位置(例如end()+1),也是如此,程序也不会出错。因此elem1和elem2都是尾后迭代器时,容器保持不变。如果elem2为尾后迭代器,elem1指向之前的合法位置,则会删除从elem1开始直至容器末尾的所有元素。
练习9.26:使用下面代码定义的ia,将ia拷贝到一个vector和一个list中。使用单迭代器版本的erase从list中删除奇数元素,从vector中删除偶数元素。
int ia[] = {0,1,1,2,3,5,8,21,55,89}
【出题思路】练习删除指定位置元素的操作,理解操作对迭代器的影响。
【解答】
当从vector中删除元素时,会导致删除点之后位置的迭代器、引用和指针失效。而erase返回的迭代器指向删除元素之后的位置。因此,将erase返回的迭代器赋予iiv,使其正确向前推进。且尾后位置每个循环步中都用end重新获得,保证其有效。对于list,删除操作并不会令迭代器失效,但上述方法仍然是适用的。
#include <iostream>
#include <vector>
#include <list>
using namespace std;
int main()
{
int ia[] = {0, 1, 1, 2, 3, 5, 8, 13, 21, 55, 89};
vector<int> iv;
list<int> il;
iv.assign(ia, ia + 11); //将数据拷贝到vector
il.assign(ia, ia + 11); //将数据拷贝到list
vector<int>::iterator iiv = iv.begin();
while(iiv != iv.end())
{
if(!*iiv & 1) //偶数
{
iiv = iv.erase(iiv); //删除偶数,返回下一个位置迭代器
}
else
{
++iiv; //推进到下一人位置
}
}
list<int>::iterator iil = il.begin();
while(iil != il.end())
{
if(*iil & 1) //奇数
{
iil = il.erase(iil); //删除奇数,返回下一个位置迭代器
}
else
{
++iil; //推进到下一人位置
}
}
for(iiv = iv.begin(); iiv != iv.end(); ++iiv)
cout << *iiv << " ";
cout << endl;
for(iil = il.begin(); iil != il.end(); ++iil)
cout << *iil << " ";
cout << endl;
return 0;
}
运行结果:
练习9.27:编写程序,查找并删除forward_list<int>中的奇数元素。
【出题思路】
练习forward_list特殊的删除操作。
【解答】
关键点是理解forward_list其实是单向链表数据结构,只有前驱节点指向后继节点的指针,而没有反向的指针。因此,在forward_list中可以高效地从前驱转到后继,但无法从后继转到前驱。而当我们删除一个元素后,应该调整被删元素的前驱指针指向被删元素的后继,起到将该元素从链表中删除的效果。因此,在forward_list中插入、删除元素既需要该元素的迭代器,也需要前驱迭代器。为此,forward_list提供了before_begin来获取首元素之前位置的迭代器,且插入、删除都是_after形式,即,删除(插入)给定迭代器的后继。
#include <iostream>
#include <forward_list>
using namespace std;
int main()
{
forward_list<int> iflst = {1, 2, 3, 4, 5, 6, 7, 8};
auto prev = iflst.before_begin(); //前骊元素
auto curr = iflst.begin(); //当前元素
while(curr != iflst.end())
{
if(*curr & 1) //奇数
{
curr = iflst.erase_after(prev); //删除,移动到下一个元素
}
else
{
prev = curr; //前驱和当前迭代器都向前推进
++curr;
}
}
for(curr = iflst.begin(); curr != iflst.end(); ++curr)
cout << *curr << " ";
cout << endl;
return 0;
}
运行结果:
练习9.28:编写函数,接受一个forward_list<string>和两个string共三个参数。函数应在链表中查找第一个string,并将第二个string插入到紧接着第一个string之后的位置。若第一个string未在链表中,则将第二个string插入到链表末尾。
【出题思路】
练习forward_list特殊的添加操作。
【解答】
与删除相同的是,forward_list的插入操作也是在给定元素之后。不同的是,插入一个新元素后,只需将其后继修改为给定元素的后继,然后修改给定元素的后继为新元素即可,不需要前驱迭代器参与。但对于本题,当第一个string不在链表中时,要将第二个string插入到链表末尾。因此仍然需要维护前驱迭代器,当遍历完链表时,“前驱”指向尾元素,“当前”指向尾后位置。若第一个string不在链表中,此时只需将第二个string插入到“前驱”之后即可。总体来说,单向链表由于其数据结构上的局限,为实现正确插入、删除操作带来了困难。标准库的forward_list容器为我们提供了一些特性,虽然(与其他容器相比)我们仍需维护一些额外的迭代器,但已经比直接用指针来实现链表的插入、删除方便了许多。
#include <iostream>
#include <forward_list>
#include <string>
using namespace std;
void test_and_insert(forward_list<string> &sflst, const string &s1, const string &s2)
{
auto prev = sflst.before_begin(); //前驱元素
auto curr = sflst.begin(); //当前元素
bool inserted = false;
while(curr != sflst.end())
{
if(*curr == s1) //找到给定字符串
{
curr = sflst.insert_after(curr, s2); //插入新字符串,curr指向它
inserted = true;
}
prev = curr; //当驱迭代器向前推进
++curr; //当前迭代器向前推进
}
if(!inserted)
sflst.insert_after(prev, s2); //未找到给定字符串,插入尾后
}
int main()
{
forward_list<string> sflst = {"Hello", "!", "world", "!"};
test_and_insert(sflst, "Hello", "你好");
for(auto curr = sflst.cbegin(); curr != sflst.cend(); ++curr)
cout << *curr << " ";
cout << endl;
test_and_insert(sflst, "!", "?");
for(auto curr = sflst.cbegin(); curr != sflst.cend(); ++curr)
cout << *curr << " ";
cout << endl;
test_and_insert(sflst, "Bye", "再见");
for(auto curr = sflst.cbegin(); curr != sflst.cend(); ++curr)
cout << *curr << " ";
cout << endl;
return 0;
}
运行结果:
练习9.29:假定vec包含25个元素,那么vec.resize(100)会做什么?如果接下来调用vec.resize(10)会做什么?
【出题思路】
本题练习改变容器大小的操作。
【解答】
调用vec.resize(100)会向vec末尾添加75个元素,这些元素将进行值初始化。接下来调用vec.resize(10)会将vec末尾的90个元素删除。
练习9.31:第316页中删除偶数值元素并复制奇数值元素的程序不能用于list或forward_list。为什么?修改程序,使之也能用于这些类型。
【出题思路】
本题继续练习list和forward_list的插入、删除操作,理解与其他容器的不同,理解对迭代器的影响。
【解答】
list和forward_list与其他容器的一个不同是,迭代器不支持加减运算,究其原因,链表中元素并非在内存中连续存储,因此无法通过地址的加减在元素间远距离移动。因此,应多次调用++来实现与迭代器加法相同的效果。
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> ilst = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto curr = ilst.begin(); //首节点
while(curr != ilst.end())
{
if(*curr & 1) //奇数
{
curr = ilst.insert(curr, *curr); //插入到当前元素之前
++curr;
++curr; //移动到下一个元素
}
else //偶数
{
curr = ilst.erase(curr); //删除,指向下一元素
}
}
for(curr = ilst.begin(); curr != ilst.end(); ++curr)
cout << *curr << " ";
cout << endl;
return 0;
}
运行结果:
练习9.32:在第316页的程序中,像下面语句这样调用insert是否合法?如果不合法,为什么?
iter = vi.insert(iter, *iter++);
【出题思路】
本题复习实参与形参的关系,进一步熟悉迭代器的处理对容器操作的关键作用。
【解答】
很多编译器(例如作者所使用的tdm-gcc)对实参求值、向形参传递的处理顺序是由右至左的。这意味着,编译器在编译上述代码时,首先对*iter++求值,传递给insert第二个形参,此时iter已指向当前奇数的下一个元素,因此传递给insert的第一个参数的迭代器指向的是错误位置,程序执行会发生混乱,最终崩溃。因此,若将代码改为iter = vi.insert(iter++, *iter);,或是使用由左至右求值、传递参数的编译器,代码的运行结果是正确的。当然,这样的代码在逻辑上是毫无道理的。
练习9.33:在本节最后一个例子中,如果不将insert的结果赋予begin,将会发生什么?编写程序,去掉此赋值语句,验证你的答案。
【出题思路】
进一步理解容器插入、删除操作会使迭代器失效。
【解答】
向vector中插入新元素后,原有迭代器都会失效。因此,不将insert()返回的迭代器赋予begin,会使begin失效。继续使用begin会导致程序崩溃。对此程序,保存尾后迭代器和不向begin赋值两个错误存在其一,程序都会崩溃。
练习9.34:假定vi是一个保存int的容器,其中有偶数值也有奇数值,分析下面循环的行为,然后编写程序验证你的分析是否正确。
iter = vi.begin();
while(iter != vi.end())
if(*iter % 2)
iter = vi.insert(iter, *iter);
++iter;
【出题思路】
继续熟悉容器插入、删除操作与迭代器的关系,以及编程中容易出现的错误。
【解答】
此段代码的第一个错误是忘记使用花括号,使得++iter变成循环后的第一条语句,而非所期望的循环体的最后一条语句。因此,除非容器为空,否则程序会陷入死循环:1.若容器的第一个元素是偶数,布尔表达式为假,if语句真分支不会被执行,iter保持不变。循环继续执行,真分支仍然不会执行,iter继续保持不变,如此陷入死循环。
2.若容器的第一个元素是奇数,insert语句被调用,将该值插入到首元素之前,并将返回的迭代器(指向新插入元素)赋予iter,因此iter指向新首元素。继续执行循环,会继续将首元素复制到容器首位置,并令iter指向它,如此陷入死循环。示例(粗体代表迭代器位置):
初始:{1,2,3,4,5,6,7,8,9}
第一步:{1,1,2,3,4,5,6,7,8,9}
第二步:{1,1,1,2,3,4,5,6,7,8,9}
......
下面的程序可展示程序执行效果。其中,我们在循环体最后加入了一个循环,打印容器中所有元素,即可观察程序执行效果。这是一种简单的程序调试方法。cout>> tmp是为了让程序暂停,程序员有时间观察输出,需要继续执行程序时,随意输入一个字符串即可。
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int main()
{
vector<int> vi = {1, 2, 3, 4, 5, 6, 7, 8, 9};
auto iter = vi .begin();
string tmp;
while(iter != vi.end())
{
if(*iter % 2)
iter = vi.insert(iter, *iter);
for(auto begin = vi.begin(); begin != vi.end(); ++begin)
cout << *begin << " ";
cout << endl;
cin >> tmp;
}
++iter;
return 0;
}
运行结果:
当我们将++iter放入循环体后,程序仍然是错误的,除非容器为空或仅包含偶数,否则程序仍然会陷入死循环。原因是,当遍历到奇数时,执行insert将该值插入到旧元素之前,将返回指向新元素的迭代器赋予iter,再递增iter,此时iter将指向旧元素。继续执行循环仍会重复这几个步骤,程序陷入死循环。正确的程序应该是将++iter移入循环体,再增加一个++iter,令iter指向奇数之后的元素。示例(粗体代表迭代器位置):
初始:{0,1,2,3,4,5,6,7,8,9}
第一步:{0,1,1,1,2,3,4,5,6,7,8,9}
第二步:{0,1,1,1,1,1,2,3,4,5,6,7,8,9}
.......
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int main()
{
vector<int> vi = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto iter = vi.begin();
string tmp;
while(iter != vi.end())
{
if(*iter % 2)
iter = vi.insert(iter, *iter);
++iter;
for(auto begin = vi.begin(); begin != vi.end(); ++begin)
cout << *begin << " ";
cout << endl;
cin >> tmp;
}
return 0;
}
运行结果: