《C++ Primer》第9章 9.3节习题答案

《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;
}

运行结果:

 

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值