【C++】【C++ Primer】9-顺序容器

1 顺序容器概述

容器就是特定类型对象的集合。

顺序容器提供了控制元素存储和访问顺序的能力。这种顺序不依赖于元素值,而是与元素加入容器时的位置相对应。有序和无序关联容器则根据关键字的值来存储元素。

所有顺序容器都提供了快速顺序访问元素的能力。但这些容器在以下两方面有不同的性能折中:

  • 添加或删除元素的代价
  • 非顺序访问容器中元素的代价

1.1 顺序容器类型及特点

表1 顺序容器类型
vector可变大小数组。支持快速随机访问。在尾部之外的位置插入或删除元素可能会很慢
deque双端队列。支持快速随机访问。在头尾位置插入/删除速度很快
list双向链表。只支持双向顺序访问。在list中任何位置插入/删除速度都很快
forward_list单向链表。只支持单向顺序访问。在任何位置插入/删除速度都很快
array固定大小数组。支持快速随机访问。不能增加/删除元素
string与vector相似的容器,但专门用于保存字符。随机访问快。在尾部插入/删除速度快

除了固定大小的array,其他容器都提供高效、灵活的内存管理。可以添加、删除元素,扩张、收缩容器大小。容器保存元素的策略对容器操作的效率有重大影响。某些情况下,存储策略还会影响特定容器是否支持特定操作。

list和forward_list的设计目的是令容器可以在任何位置快速添加和删除元素,代价是不支持随机访问——为了访问一个元素,需要遍历整个容器。和vector、deque、array相比,这两个容器的额外内存开销也很大。

deque支持快速随机访问。在deque的中间位置添加或删除元素的代价可能很高,但在deque两端添加或删除元素非常快,与list和forward_list相当。

与内置数组相比,array更安全、更容易使用。array对象的大小是固定的,不支持添加或删除元素以及改变容器大小的操作。

forward_list没有size操作,因为保存或计算其大小会增加额外开销。对其他容器而言,size是一个快速的常量时间操作。

1.2 顺序容器的选择

选择容器的基本原则如下:

  • 除非有很好的理由选择其他容器,否则使用vector
  • 如果程序中有很多小元素,且空间的额外空间很重要,不要使用list及forward_list
  • 如果程序需要随机访问元素,应使用vector或deque
  • 如果程序要求在容器中间插入或删除元素,应使用list或forward_list
  • 如果程序需要在头尾位置插入或删除元素,但不会在中间位置插入或删除,则使用deque
  • 如果程序仅在读取输入时在容器中间位置插入元素,随后需要随机访问元素,则:
    • 首先确认是否真的需要在容器中间位置插入元素。处理输入数据时,可以向vector追加数据,然后再调用标准库sort函数排序,从而避免在中间位置添加元素
    • 如果必须在中间位置插入,考虑在输入阶段使用list,输入完成后将list中的内容拷贝到vector中

如果程序既需要随机访问元素,又需要在容器中间位置插入元素,就要在list或forward_list中访问元素与vector或deque插入、删除元素的性能之间做取舍。

2 容器库概览

容器通常定义在和类型同名的头文件中,且容器均定义为模板类。对大多数容器(不是所有)来说,需要额外提供元素类型信息。

顺序容器几乎可以保存任意类型的元素,其元素甚至可以是另一个容器。

vector<vector<int>> ivec;
vector<vector<int> > ivec2;    // 较旧的编译器需要在最后两个>中加个空格

尽管可以在容器中保存几乎所有类型,但某些容器操作对元素类型有特殊要求。我们可以用不支持特定要求的类型定义容器,但此时就不能使用依赖这些特定要求的容器操作了。譬如,顺序容器有一个仅接收容器大小参数的构造函数,它会使用元素类型的默认构造函数。如果元素类型没有定义默认构造函数,我们可以定义该类型的顺序容器,但不能使用这种构造函数。

表2 容器操作
类型别名
iterator此容器类型的迭代器类型
const_iterator只读迭代器类型
size_type无符号整数类型,足够保存此容器类型最大大小
difference_type带符号整数类型,足够保存两个迭代器之间的距离
value_type元素类型
reference元素的左值类型,与value_type&含义相同
const_reference元素的const左值类型,与const value_type&含义相同
构造函数
C c;默认构造函数,构造空容器
C c1(c2);构造c2的拷贝c1
C c(b, e);构造c,将迭代器b和e指定范围内的元素拷贝到c(array不支持)
C c{a, b, c, ...};列表初始化c
赋值与swap
c1 = c2;将c1中的元素替换为c2中的元素
c1 = {a, b, c, ...};将c1中的元素替换为列表中的元素(不适用于array)
a.swap(b);交换a和b的元素
swap(a, b);与a.swap(b)等价
大小
c.size()c中元素的数目(不支持forward_list)
c.max_size()c可保存的最大元素数目
c.empty()c为空则返回true
添加/删除元素(不适用于array,且在不同容器中这些操作的接口不同)
c.insert(args)将args中的元素拷贝进c
c.emplace(inits)使用inits构造c中的一个元素
c.erase(args)删除args指定的元素
c.clear()删除c中的所有元素,返回void
关系运算符
==, !=所有容器都支持相等/不相等运算符
<, <=, >, >=关系运算符(无序容器不支持)
获取迭代器
c.begin()、c.end()返回指向c的首元素和尾后元素位置的迭代器
c.cbegin()、c.cend()返回const_iterator
反向容器的额外成员(不支持forward_list)
reverse_iterator按逆序寻址元素的迭代器
const_reverse_iterator不能修改元素的逆序迭代器
c.rbegin()、c.rend()返回指向c的尾元素和首前元素位置的迭代器
c.crbegin()、c.crend()返回const_reverse_iterator

2.1 迭代器

迭代器有公共接口——如果一个迭代器提供某个操作,则所有提供相同操作的迭代器对这个操作的实现方式都是相同的。

2.1.1 标准容器迭代器的运算符

表3列出了容器迭代器支持的所有操作,其中只有一个例外,不符合公共接口特点——forward_list迭代器不支持递减运算符(–)。

表3 标准容器迭代器的运算符
*iter返回迭代器iter所指元素的引用
iter->mem解引用iter并获取该元素名为mem的成员,等价于(*iter).mem
++iter令iter指向容器中的下一个元素
--iter令iter指向容器中的上一个元素
iter1 == iter2判断两个迭代器是否相等
iter1 != iter2判断两个迭代器是否不等

2.1.2 迭代器支持的算术运算

表4列出了迭代器支持的算术运算,这些运算只能应用于string、vector、deque和array的迭代器,不能应用于其他任何容器类型的迭代器。

表4 vector与string迭代器支持的运算
iter + n迭代器加上一个整数值,即将迭代器向前移动对应个数。结果仍是迭代器,指向容器中的某个元素,或指向容器尾元素的下一位置
iter - n迭代器减去一个整数值,即将迭代器向后移动对应个数。结果仍是迭代器,指向容器中的某个元素,或指向容器尾元素的下一位置
iter1 += n迭代器加法的复合赋值语句
iter1 -= n迭代器减法的复合赋值语句
iter1 - iter2两个迭代器相减的结果是它们之间的距离,类型为diffrence_type(带符号)
>、>=、<、<=如果某迭代器指向的容器位置在另一迭代器所指位置之前,则称前者小于后者

2.1.3 迭代器范围

迭代器范围由一对迭代器表示,这两个迭代器分别指向同一个容器中的元素或尾后元素。这两个迭代器通常被称为begin和end。end可以和begin指向相同的位置,但不能指向begin之前的位置。元素范围的数学表示为左闭合区间[begin, end)。

2.1.4 使用左闭合范围蕴含的编程假定

标准库使用左闭合区间有三个性质:

  • 如果begin==end,则范围为空
  • 如果begin与end不等,则范围至少包含一个元素,且begin指向该范围中的第一个元素
  • 对begin递增若干次,可以使得begin==end

以上性质意味着可以用循环来处理元素范围:

while (begin != end) {
	*begin = val;
	++begin;
}

2.2 容器类型成员

每个容器都定义了多个类型,我们已经见过其中三种:size_type、iterator、const_iterator。

除了已经使用过的迭代器,大多数容器还提供反向迭代器。反向迭代器是一种反向遍历容器的迭代器,与正向迭代器相比,各种操作的含义都发生颠倒。譬如对反向迭代器执行++操作,会得到上一个元素。

剩下的就是类型别名。通过类型别名,可以在不了解容器中元素类型的情况下使用它。如果需要元素类型,可以使用容器的value_type。如果需要元素类型的引用,可以使用reference或const_reference。这些元素相关的类型别名在泛型编程中非常有用。

为了使用这些类型,必须显式使用其类名:

list<string>::iterator iter;

2.3 begin和end成员

begin和end操作生成指向容器中第一个元素和尾后元素的迭代器。

begin和end有多个版本:

  • begin()和end()
  • rbegin()和rend():返回反向迭代器
  • cbegin()和cend():返回const迭代器

begin()、end()、rbegin()、rend()这四个接口,在非常量对象上调用时,得到的是iterator。在const对象上调用时,得到的是const iterator。所以当auto和这四个接口结合使用时,获得的迭代器类型依赖于容器类型。

iterator可以转换为对应的const_iterator,反之则不行。

不需要写访问时,应使用cbegin和cend。

2.4 容器的定义和初始化

每个容器类型都定义了默认构造函数。除array外,其他容器的默认构造函数会创建一个指定类型的空容器,且都可以接受指定容器大小和元素初始值的参数。

表5 容器定义和初始化
C c;默认构造函数。如果C是一个array,则c中元素按默认方式初始化。否则c为空
C c1(c2);c1初始化为c2的拷贝。c1和c2必须是相同的容器类型,且保存相同元素类型。对于array类型来说,两者还必须具有相同大小。
C c1 = c2;
C c{a, b, c, ...};c初始化为初始化列表中元素的拷贝。列表中元素的类型必须与C的元素类型相容。对于array类型,列表中元素数目必须等于或小于array的大小,任何遗漏的元素都进行值初始化
C c = {a, b, c, ...};
C c(b, e);c初始化为迭代器b和e指定范围中元素的拷贝。范围中元素的类型必须和C的元素类型相容(array不适用)
只有顺序容器(不包括array)的构造函数才能接受大小参数
C seq(n);seq包含n个元素,这些元素进行了值初始化。此构造函数是explicit的
C seq(n, t);seq包含n个初始化为值t的元素

2.4.1 将一个容器初始化为另一个容器的拷贝

将一个新容器创建为另一个容器的拷贝的方法有两种:

  • 直接拷贝整个容器
    • 两个容器的类型及元素类型必须匹配
  • 拷贝由一对迭代器指定的元素范围(array除外)
    • 不要求容器类型相同,元素类型也可以不同,只要能够转换即可
在这里插入代码片

7 练习

1. 对于下面的程序任务,vector、deque和list哪个容器最为适合?请解释理由。如果没有哪一种容器优于其他容器,也请解释理由。
(a)读取固定数量的单词,将它们按字典序插入到容器中。我们将在下一章看到,关联容器更适合这个问题。
(b)读取未知数量的单词,总是将新单词插入到末尾。删除操作在头部进行。
(c)从一个文件读取未知数量的整数。将这些数排序,然后打印到标准输出

(a)按字典序插入,意味着需要频繁插入操作,list更为适合。如果不是必须边读取边插入,也可以先插入到vector的尾部,读取完成后再排序。
(b)由于需要在容器两端增删元素,deque更为适合。
(c)快速排序算法需要频繁随机存取数据,使用vector更合适。

2. 定义一个list对象,其元素类型是int的deque。

list<deque<int>> a;

3. 构成迭代器范围的迭代器有何限制?

  • begin和end指向同一容器中的元素或尾后元素;
  • end不能在begin之前,但可以指向同一位置;
  • begin递增后总能到达end所指位置。

4. 编写函数,接受一对指向vector的迭代器和一个int值。在两个迭代器指定的范围中查找给定的值,返回布尔型结果。

#include <iostream>
#include <vector>

using namespace std;

bool search_vector(vector<int>::iterator begin,
    vector<int>::iterator end, int val)
{
    for (; begin != end; ++begin) {
        if (*begin == val) return true;
    }
    return false;
}

int main(int argc, char **argv)
{
    vector<int> vec = {1, 2, 3, 4, 5, 6, 7};

    cout << search_vector(vec.begin(), vec.end(), 3) << endl;
    cout << search_vector(vec.begin(), vec.end(), 8) << endl;

    return 0;
}

5. 重写上一题的函数,返回一个迭代器指向找到的元素。注意,必须处理未找到给定值的情况。

#include <iostream>
#include <vector>

using namespace std;

vector<int>::iterator search_vector(vector<int>::iterator begin,
    vector<int>::iterator end, int val)
{
    for (; begin != end; ++begin) {
        if (*begin == val) return begin;
    }
    return end;
}

int main(int argc, char **argv)
{
    vector<int> vec = {1, 2, 3, 4, 5, 6, 7};

    cout << search_vector(vec.begin(), vec.end(), 3) - vec.begin()<< endl;
    cout << search_vector(vec.begin(), vec.end(), 8) - vec.begin()<< endl;

    return 0;
}

6. 下面程序有何错误?应如何修改?
list lst1;
list::iterator iter1 = lst1.begin(),
iter2 = lst1.end();
while (iter1 < iter2) {}

list是链式存储结构,不能使用<。可以改用!=来判断。

7. 为了索引int的vector中的元素,应该使用什么类型?

应使用vector::iterator。

8. 为了读取string的list中的元素,应该使用什么类型?如果写入list,又该使用什么类型?

读取时使用list<string>::value_type。

list<string> slist = {"aaa", "bbb", "ccc"};
list<string>::value_type tmp = *slist.begin();

写入时使用list<string>::reference。

list<string> slist = {"aaa", "bbb", "ccc"};
list<string>::reference tmp = *slist.begin();
tmp = "albert";

9. begin和cbegin两个函数有何不同?

begin有两个重载版本,其一返回普通迭代器,其二返回const迭代器。取决于发起调用的对象是否为const。

cbegin返回const迭代器。当程序没有写入需求时,应使用cbegin。

10. 下面六个对象分别是什么类型?
vector<int> v1;
const vector<int> v2;
auto it1 = v1.begin(), it2 = v2.begin();
auto it3 = v1.cbegin(), it4 = v2.cbegin();

v1是vector<int>类型。

v2是const vector类型。

it1是普通迭代器。

it2是const迭代器。

it3是const迭代器。

it4是const迭代器。

11. 对6种创建和初始化vector对象的方法,每种都给出一个实例。解释每个vector包含什么值。

vector<int> vec1;
vector<int> vec2(vec1);          // 等价于vector<int> vec2 = vec1;
vector<int> vec3 = {1, 2, 3};    // 等价于vector<int> vec3{1, 2, 3};
vector<int> vec4(vec3.begin() + 1, vec3.end() - 1);
vector<int> vec5(7);
vector<int> vec6(7, 5);

12. 对于接受一个容器创建其拷贝的构造函数,和接受两个迭代器创建拷贝的构造函数,解释它们的不同。

接受已有容器的构造函数会拷贝此容器中的所有元素。

接受两个迭代器的构造函数可以拷贝容器中指定范围的元素。

13. 如何从一个list<int>初始化一个vector<double>?从一个vector<int>又该如何创建?

由于list<int>和vector<double>是不同的容器类型,因此无法采用容器拷贝的方式来初始化。但int和double是相容的,所以可以用范围初始化来构造。对vector<int>来说也是这种思路。

list<int> ilist = {1, 2, 3, 4, 5};
vector<int> ivec = {1, 2, 3, 4, 5};
vector<double> dvec1(ilist.begin(), ilist.end());
vector<double> dvec2(ivec.begin(), ivec.end());

14. 编写程序,将一个list中的char *指针元素付给一个vector中的string

list<char *>和vector<string>是不同类型的容器,所以不能直接采用赋值运算符来赋值。但char *可以转换为string,所以可以用范围赋值方式来实现。

list<char *> slist = {"aaa", "bbb", "ccc"};
vector<string> svec;
svec.assign(slist.begin(), slist.end());

15. 编写程序,判断两个vector<int>是否相等。

vector<int> ivec1 = {1, 2, 3};
vector<int> ivec2 = {1, 2, 3};
cout << (ivec1 == ivec2) << endl;

16. 重写上一题程序,比较一个list<int>中的元素和一个vector<int>中的元素

bool list_vector_equal(const list<int> &ilist, const vector<int> &ivec)
{
    if (ilist.size() != ivec.size())
        return false;

    auto lbegin = ilist.begin();
    auto lend = ilist.end();
    auto vbegin = ivec.begin();

    while (lbegin != lend) {
        if (*lbegin != *vbegin) return false;
        lbegin++;
        vbegin++;
    }

    return true;
}

17. 假定c1和c2是两个容器,比较操作if (c1 < c2)有何限制?

  • 容器类型必须相同;
  • 元素类型必须相同;
  • 元素类型必须支持<运算符。

18. 编写程序,从标准输入读取string序列,存入一个deque中。编写一个循环,用迭代器打印deque中的元素。

int main(int argc, char **argv)
{
    deque<string> sdeq;
    string word;
    while (cin >> word) {
        sdeq.push_back(word);
    }

    for (auto begin = sdeq.begin(); begin != sdeq.end(); begin++) {
        cout << *begin << endl;
    }

    return 0;
}

19. 重写上题的程序,用list代替deque。列出程序需要做出哪些改变。

并无太大差异。

int main(int argc, char **argv)
{
    list<string> slist;
    string word;
    while (cin >> word) {
        slist.push_back(word);
    }

    for (auto begin = slist.begin(); begin != slist.end(); begin++) {
        cout << *begin << endl;
    }

    return 0;
}

20. 编写程序,从list<int>拷贝元素到两个deque中。值为偶数的所有元素都拷贝到一个deque中,值为奇数的元素都拷贝到另一个deque中。

void print_vector(vector<int> &vec) {
    for (auto beg = vec.begin(); beg != vec.end(); beg++) {
        cout << *beg << " ";
    }
    cout << endl;
}

int main(int argc, char **argv)
{
    list<int> ilist = {1, 2, 3, 4, 5};
    vector<int> odd_vec;
    vector<int> even_vec;

    for (auto beg = ilist.begin(); beg != ilist.end(); beg++) {
        auto &vec = (*beg % 2) ? odd_vec : even_vec;
        vec.push_back(*beg);
    }

    print_vector(odd_vec);
    print_vector(even_vec);

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值