C++STL:顺序容器

1. 顺序容器概述

所谓顺序容器,即以线性排列(类似普通数组的存储方式)来存储某一指定类型(例如 int、double 等)的数据,需要特殊说明的是,该类容器并不会自动对存储的元素按照值的大小进行排序。

顺序容器主要包含以下几类容器:

  • array<T,N>(数组容器):表示可以存储 N 个 T 类型的元素,是 C++ 本身提供的一种容器。此类容器一旦建立,其长度就是固定不变的,这意味着不能增加或删除元素,只能改变某个元素的值。
  • vector<T>(向量容器):用来存放 T 类型的元素,是一个长度可变的序列容器,即在存储空间不足时,会自动申请更多的内存。使用此容器,在尾部增加或删除元素的效率最高(时间复杂度为 O(1) 常数阶),在其它位置插入或删除元素效率较差(时间复杂度为 O(n) 线性阶,其中 n 为容器中元素的个数)。
  • deque<T>(双端队列容器):和 vector 非常相似,区别在于使用该容器不仅尾部插入和删除元素高效,在头部插入或删除元素也同样高效,时间复杂度都是 O(1) 常数阶,但是在容器中某一位置处插入或删除元素,时间复杂度为 O(n) 线性阶。
  • list<T>(链表容器):是一个长度可变的、由 T 类型元素组成的序列,它以双向链表的形式组织元素,在这个序列的任何地方都可以高效地增加或删除元素(时间复杂度都为常数阶 O(1)),但访问容器中任意元素的速度要比前三种容器慢,这是因为 list 必须从第一个元素或最后一个元素开始访问,需要沿着链表移动,直到到达想要的元素。
  • forward_list<T>(正向链表容器):和 list 容器非常类似,只不过它以单链表的形式组织元素,它内部的元素只能从第一个元素开始访问,是一类比链表容器快、更节省内存的容器。

注意,其实除此之外,stack 和 queue 本质上也属于顺序容器,只不过它们都是在 deque 容器的基础上改头换面而成,通常更习惯称它们为容器适配器。

下图说明了各种顺序容器及它们之间的区别:

在这里插入图片描述


2. Array

2.1 概述

Array 容器是 C++ 11 标准中新增的顺序容器,简单地理解,它就是在 C++ 普通数组的基础上,添加了一些成员函数和全局函数。在使用上,它比普通数组更安全,且效率并没有因此变差。和其它容器不同,array 容器的大小是固定的,无法动态的扩展或收缩,这也就意味着,在使用该容器的过程无法借由增加或移除元素而改变其大小,它只允许访问或者替换存储的元素。

array 容器以类模板的形式定义在 <array> 头文件,并位于命名空间 std 中,如下所示:

namespace std{
    template <typename T, size_t N>
    class array;
}

因此,在使用该容器之前,代码中需引入 <array> 头文件,并默认使用 std 命令空间,如下所示:

#include <array>
using namespace std;

在 array<T,N> 类模板中,T 用于指明容器中存储的具体数据类型,N 用于指明容器的大小,需要注意的是,这里的 N 必须是常量,不能用变量表示。

  1. array 容器有多种初始化方式,如下代码展示了如何创建具有 10 个 double 类型元素的 array 容器:
std::array<double, 10> values;

由此,就创建好了一个名为 values 的 array 容器,其包含 10 个浮点型元素。但是,由于未显式指定这 10 个元素的值,因此使用这种方式创建的容器中,各个元素的值是不确定的(array 容器不会做默认初始化操作)。

  1. 通过如下创建 array 容器的方式,可以将所有的元素初始化为 0 或者和默认元素类型等效的值:
std::array<double, 10> values {};

使用该语句,容器中所有的元素都会被初始化为 0.0。

  1. 当然,在创建 array 容器的实例时,也可以像创建常规数组那样对元素进行初始化:
std::array<double, 10> values {0.5,1.0,1.5,,2.0};

2.2 成员函数

成员函数功能
begin()返回指向容器中第一个元素的随机访问迭代器。
end()返回指向容器最后一个元素之后一个位置的随机访问迭代器,通常和 begin() 结合使用。
rbegin()返回指向最后一个元素的随机访问迭代器。
rend()返回指向第一个元素之前一个位置的随机访问迭代器。
cbegin()和 begin() 功能相同,只不过在其基础上增加了 const 属性,不能用于修改元素。
cend()和 end() 功能相同,只不过在其基础上增加了 const 属性,不能用于修改元素。
crbegin()和 rbegin() 功能相同,只不过在其基础上增加了 const 属性,不能用于修改元素。
crend()和 rend() 功能相同,只不过在其基础上增加了 const 属性,不能用于修改元素。
size()返回容器中当前元素的数量,其值始终等于初始化 array 类的第二个模板参数 N。
max_size()返回容器可容纳元素的最大数量,其值始终等于初始化 array 类的第二个模板参数 N。
empty()判断容器是否为空,和通过 size()==0 的判断条件功能相同,但其效率可能更快。
at(n)返回容器中 n 位置处元素的引用,该函数自动检查 n 是否在有效的范围内,如果不是则抛出 out_of_range 异常。
front()返回容器中第一个元素的直接引用,该函数不适用于空的 array 容器。
back()返回容器中最后一个元素的直接应用,该函数不适用于空的 array 容器。
data()返回一个指向容器首个元素的指针。利用该指针,可实现复制容器中所有元素等类似功能。
fill(val)将 val 这个值赋值给容器中的每个元素。
array1.swap(array2)交换 array1 和 array2 容器中的所有元素,但前提是它们具有相同的长度和类型。

除此之外,C++ 11 标准库还新增加了 begin() 和 end() 这 2 个函数,和 array 容器包含的 begin() 和 end() 成员函数不同的是,标准库提供的这 2 个函数的操作对象,既可以是容器,还可以是普通数组。当操作对象是容器时,它和容器包含的 begin() 和 end() 成员函数的功能完全相同;如果操作对象是普通数组,则 begin() 函数返回的是指向数组第一个元素的指针,同样 end() 返回指向数组中最后一个元素之后一个位置的指针(注意不是最后一个元素)。

另外,在 <array> 头文件中还重载了 get() 全局函数,该重载函数的功能是访问容器中指定的元素,并返回该元素的引用。

2.3 迭代器

STL 为 array 容器配备了随机访问迭代器,该类迭代器是功能最强大的迭代器。在 array 容器的模板类中,和随机访问迭代器相关的成员函数如下表所示:

成员函数功能
begin()返回指向容器中第一个元素的正向迭代器;如果是 const 类型容器,则该函数返回的是常量正向迭代器。
end()返回指向容器最后一个元素之后一个位置的正向迭代器;如果是 const 类型容器,则该函数返回的是常量正向迭代器。此函数通常和 begin() 搭配使用。
rbegin()返回指向最后一个元素的反向迭代器;如果是 const 类型容器,则该函数返回的是常量反向迭代器。
rend()返回指向第一个元素之前一个位置的反向迭代器。如果是 const 类型容器,则该函数返回的是常量反向迭代器。此函数通常和 rbegin() 搭配使用。
cbegin()和 begin() 功能类似,只不过其返回的迭代器类型为常量正向迭代器,不能用于修改元素。
cend()和 end() 功能相同,只不过其返回的迭代器类型为常量正向迭代器,不能用于修改元素。
crbegin()和 rbegin() 功能相同,只不过其返回的迭代器类型为常量反向迭代器,不能用于修改元素。
crend()和 rend() 功能相同,只不过其返回的迭代器类型为常量反向迭代器,不能用于修改元素。

除此之外,C++ 11 标准新增的 begin() 和 end() 函数,当操作对象为 array 容器时,也和迭代器有关,其功能分别和上表中的 begin()、end() 成员函数相同。

2.3.1 begin()/end() 和 cbegin()/cend()

  1. array 容器模板类中的 begin() 和 end() 成员函数返回的都是正向迭代器,它们分别指向「首元素」和「尾元素+1」 的位置。在实际使用时,我们可以利用它们实现初始化容器或者遍历容器中元素的操作。

例如,可以在循环中显式地使用迭代器来初始化 values 容器的值:

#include <iostream>
#include <array>
using namespace std;

int main() {
    array<int, 5> values;
    int h = 1;
    auto first = values.begin();
    auto last = values.end();
    // 初始化 values 容器为{1,2,3,4,5}
    while (first != last) {
        *first = h;
        ++first;
        ++h;
    }
  
    first = values.begin();
    while (first != last) {
        cout << *first << " "; // 输出结果为:1 2 3 4 5
        ++first;
    }
    return 0; 
}

可以看出,迭代器对象是由 array 对象的成员函数 begin() 和 end() 返回的。我们可以像使用普通指针那样使用迭代器对象。比如代码中,在保存了元素值后,使用前缀 ++ 运算符对 first 进行自增,当 first 等于 end 时,所有的元素都被设完值,循环结束。

与此同时,还可以使用全局的 begin() 和 end() 函数来从容器中获取迭代器,因为当操作对象为 array 容器时,它们和 begin()/end() 成员函数是通用的。所以上面代码中,first 和 last 还可以像下面这样定义:

auto first = std::begin(values);
auto last = std::end (values);

这样,容器中的一段元素可以由迭代器指定,这让我们有了对它们使用算法的可能。

需要注意的是,STL 中不是只有 array 容器,当迭代器指向容器中的一个特定元素时,它们不会保留任何关于容器本身的信息,所以我们无法从迭代器中判断,它是指向 array 容器还是指向 vector 容器。

  1. 除此之外,array 模板类还提供了 cbegin() 和 cend() 成员函数,它们和 begin()/end() 唯一不同的是,前者返回的是 const 类型的正向迭代器,这就意味着,有 cbegin() 和 cend() 成员函数返回的迭代器,可以用来遍历容器内的元素,也可以访问元素,但是不能对所存储的元素进行修改。例如下面的代码:
#include <iostream>
#include <array>
using namespace std;

int main() {
    array<int, 5> values{ 1,2,3,4,5 };
    int h = 1;
    auto first = values.cbegin();
    auto last = values.cend();

    // 由于 *first 为 const 类型,不能用来修改元素,因此下面的代码错误
    // *first = 10;

    // 遍历容器并输出容器中所有元素
    while (first != last) {
        // 可以使用 const 类型迭代器访问元素
        cout << *first << " "; // 输出结果为:1 2 3 4 5
        ++first;
    }
    return 0;
}

2.3.2 rbegin()/rend() 和 crbegin()/crend()

array 模板类中还提供了 rbegin()/rend() 和 crbegin()/crend() 成员函数,它们每对都可以分别得到指向最一个元素和第一个元素前一个位置的随机访问迭代器,又称它们为反向迭代器。也就是说,在使用反向迭代器进行 ++-- 运算时,++ 指的是迭代器向左移动一位,-- 指的是迭代器向右移动一位,即这两个运算符的功能“互换”了。

反向迭代器用于以逆序的方式处理元素。例如:

#include <iostream>
#include <array>
using namespace std;

int main() {
    array<int, 5> values;
    int h = 1;
    auto first = values.rbegin();
    auto last = values.rend();
    // 初始化 values 容器为 {5,4,3,2,1}
    while (first != last) {
        *first = h;
        ++first;
        ++h;
    }

    // 逆序遍历容器,并输出各个元素
    first = values.rbegin();
    while (first != last) {
        cout << *first << " "; // 输出结果为:1 2 3 4 5
        ++first;
    }
    return 0;
}

可以看到,从最后一个元素开始循环,不仅完成了容器的初始化,还遍历输出了容器中的所有元素。结束迭代器指向第一个元素之前的位置,所以当 first 指向第一个元素并 +1 后,循环就结朿了。

当然,在上面程序中,我们也可以使用 for 循环:

for (auto first = values.rbegin(); first != values.rend(); ++first) {
    cout << *first << " ";
}

crbegin()/crend() 组合和 rbegin()/rend() 组合的功能唯一的区别在于,前者返回的迭代器为 const 类型,即不能用来修改容器中的元素,除此之外在使用上和后者完全相同。

2.4 访问元素

2.4.1 访问array容器中单个元素

  1. 首先,可以通过容器名[]的方式直接访问和使用容器中的元素,这和 C++ 标准数组访问元素的方式相同,例如:
values[4] = values[3] + 2.O*values[1];

此行代码中,第 5 个元素的值被赋值为右边表达式的值。需要注意的是,使用如上这种方式,由于没有做任何边界检查,所以即便使用越界的索引值去访问或存储元素,也不会被检测到。

为了能够有效地避免越界访问的情况,可以使用 array 容器提供的 at() 成员函数,例如 :

values.at(4) = values.at(3) + 2.O*values.at(1);

这行代码和前一行语句实现的功能相同,但当传给 at() 的索引是一个越界值时,程序会抛出 std::out_of_range 异常。因此当需要访问容器中某个指定元素时,建议使用 at(),除非确定索引没有越界。

我们可能会有这样一个疑问,即为什么 array 容器在重载 [] 运算符时,没有实现边界检查的功能呢?答案很简单:因为性能。如果每次访问元素,都去检查索引值,无疑会产生很多开销。当不存在越界访问的可能时,就能避免这种开销。

  1. 除此之外,array 容器还提供了 get<n> 模板函数,它是一个辅助函数,能够获取到容器的第 n 个元素。需要注意的是,该模板函数中,参数的实参必须是一个在编译时可以确定的常量表达式,所以它不能是一个循环变量。也就是说,它只能访问模板参数指定的元素,编译器在编译时会对它进行检查。

下面代码展示了如何使用 get<n> 模板函数:

#include <iostream>
#include <array>
#include <string>
using namespace std;

int main() {
    array<string, 5> words{ "one","two","three","four","five" };
    cout << get<3>(words) << endl; // 输出 words[3],即 "four"
    //cout << get<6>(words) << endl; // 越界,会发生编译错误
    return 0;
}
  1. 另外,array 容器还提供了 data() 成员函数,通过调用该函数可以得到指向容器首个元素的指针。通过该指针,我们可以获得容器中的各个元素,例如:
#include <iostream>
#include <array>
using namespace std;

int main() {
    array<int, 5> words{1,2,3,4,5};
    cout << *( words.data()+1); // 输出结果为:2
    return 0;
}

2.4.2 访问array容器中多个元素

  1. 我们知道,array 容器提供的 size() 函数能够返回容器中元素的个数(函数返回值为 size_t 类型),所以能够像下面这样去逐个提取容器中的元素,并计算它们的和:
double total = 0;
for(size_t i = 0 ; i < values.size() ; ++i)
{
    total += values[i];
}

size() 函数的存在,为 array 容器提供了标准数组所没有的优势,即能够知道它包含多少元素。并且,接受数组容器作为参数的函数,只需要通过调用容器的成员函数 size(),就能得到元素的个数。除此之外,通过调用 array 容器的 empty() 成员函数,即可知道容器中有没有元素(如果容器中没有元素,此函数返回 true),如下所示:

if(values.empty())
    std::cout << "The container has no elements.\n";
else
    std::cout << "The container has " << values.size() << " elements.\n";

然而,很少会创建空的 array 容器,因为当生成一个 array 容器时,它的元素个数就固定了,而且无法改变,所以生成空 array 容器的唯一方法是将模板的第二个参数指定为 0,但这种情况基本不可能发生。

array 容器之所以提供 empty() 成员函数的原因是因为:对于其他元素可变或者元素可删除的容器(例如 vector、deque 等)来说,它们使用 empty() 时的机制是一样的,因此为它们提供了一个一致性的操作。

  1. 除了借助 size() 外,对于任何可以使用迭代器的容器,都可以使用基于范围的循环,因此能够更加简便地计算容器中所有元素的和,比如:
double total = 0;
for(const auto& value : values)
    total += value;
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: C++ STL容器中的栈(stack)是一种后进先出(LIFO)的数据结构。栈的基本操作包括入栈(push)、出栈(pop)、查看栈顶元素(top)和判断栈是否为空(empty)。栈可以用数组或链表实现,但使用STL容器可以更方便地实现栈的操作。在STL中,栈是由适配器(adapter)stack实现的,可以使用push、pop、top和empty等成员函数来操作栈。栈的应用场景包括函数调用、表达式求值、括号匹配等。 ### 回答2: 栈(stack)是C++ STL(标准模板库)中的一个容器,是一个后入先出(Last In, First Out,LIFO)的数据结构。堆栈的基本操作是退栈(Pop)和入栈(Push),即在栈顶插入和删除元素。除了这两个基本操作,堆栈还提供了访问栈顶元素的方法,即Top()。 堆栈可以通过STL中的std::stack<T>来使用,其中T是元素的类型。堆栈的定义非常简单,只需要使用一个std::stack<T>对象即可。在使用之前,需要包含头文件<stack>。 堆栈的主要特性是插入和删除元素的时间复杂度为常数时间O(1),因为栈只需要在栈顶进行操作。堆栈一般用于实现递归、表达式求值、内存分配等。例如,在递归深度优先搜索中,可以使用堆栈来存储遍历的路径。 堆栈的操作非常简单,以下是常用的操作列表: 1. push():将一个元素插入栈顶。 2. pop():删除栈顶元素。 3. top():返回栈顶元素。 4. empty():判断堆栈是否为空。 5. size():返回堆栈中元素的个数。 下面是一个简单的堆栈的例子,可以更好地理解堆栈的基本操作: #include <iostream> #include <stack> using namespace std; int main() { stack<int> s; // 定义一个int类型的栈 s.push(10); // 将10入栈 s.push(20); // 将20入栈 s.push(30); // 将30入栈 while (!s.empty()) { cout << s.top() << " "; // 输出栈顶元素 s.pop(); // 删除栈顶元素 } return 0; } 在上面的例子中,我们首先定义了一个堆栈s,然后在堆栈s中依次插入了三个元素10、20和30。接下来使用while循环,栈顶元素依次输出,同时删除栈顶元素,直到堆栈为空。由于堆栈是后进先出的,所以输出的顺序是30、20和10。 总之,堆栈是一个非常常用的数据结构,STL中的栈(stack)提供了非常方便的使用,可以减轻我们对堆栈数据结构进行操作的负担,提高代码的可读性和复用性。 ### 回答3: 栈(stack)是 C++ STL(Standard Template Library)中常见的一种容器数据结构,它可以在一端进行元素的插入和删除操作,遵循“后进先出”(LIFO,Last-In-First-Out)的原则。栈的操作不需要访问元素中间的部分,只需要在栈顶执行操作,保证了操作效率。栈可以用数组或链表等数据结构实现,但 C++ STL 提供了封装好的栈容器,使用起来方便且安全。 C++ STL 中栈容器的定义方式为:`std::stack`。栈默认情况下使用双端队列(deque)实现,用户也可以指定其他底层容器,如 vector、list 等。可以使用`push()`向栈顶插入元素,使用`pop()`弹出栈顶元素,使用`top()`获取栈顶元素。栈的元素个数可以使用`size()`来获取,判断栈是否为空可以使用`empty()`,在栈容器中查找某个元素的功能则不支持。 在实际应用中,栈容器可以用来实现函数的递归调用、表达式求值、括号匹配等操作。例如,可以使用栈来判断一个字符串中的括号是否匹配,具体做法是将左括号入栈,遇到右括号时弹出栈顶元素检查是否为相应的左括号。如果不匹配或者栈已经为空,则括号不匹配;如果字符串中所有的括号都匹配,则最后栈为空。 总之,栈作为一种容器数据结构,在实际应用中有着广泛的应用场景,C++ STL 提供的封装好的栈容器,具有使用方便、效率高等特点,可以帮助我们更快更方便地实现各种数据处理和算法设计。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值