首页 / 第十一章 · STL 标准模板库 / A. 序列容器

十一 A、序列容器

string 字符串 · vector 动态数组 · list 双向链表 · deque 双端队列 —— STL 工具箱里最常用的四种"装数据的箱子"。

本节目录

STL 是什么

想象一下,你有一大堆乐高积木,每次想搭一个城堡都要自己从零开始做每一块砖——这太累了!如果有人提前给你准备好了各种形状的积木块:长方形的基础块、可以转动的轮子、连接用的铰链……你只需要把它们拼起来就行了。

STL 就是 C++ 给程序员准备的"积木工具箱"。它里面装好了编程时最常用的一些数据结构:可以自动变长的数组、可以两头操作的队列、可以快速查找的字典……我们不再需要从零开始写这些复杂的东西,直接用就好。

🧰
STL 的全称是 Standard Template Library(标准模板库)。它最厉害的地方在于:数据存储和数据操作是分开的。比如排序算法写好了之后,既可以用来排序数组,也可以用来排序链表——因为算法不关心数据存在哪里,只关心怎么访问数据。
三大核心组件
📦
容器 Containers
装数据的"箱子"
vector · list · deque
stack · queue
set · map …
⚙️
算法 Algorithms
对数据做操作的"工具"
sort · find
reverse · count …
🔗
迭代器 Iterators
连接容器和算法的"桥梁"
begin() · end()
就像一个智能指针
💡
迭代器可以理解为一个"智能指针",它知道如何在你选用的容器中移动。begin() 指向第一个元素,end() 指向最后一个元素之后的位置(标记"结束了")。
所有容器的"通用技能"

好消息是,几乎所有 STL 容器都共享一套相同的操作。就像一个工具箱里的所有工具都有"拿起来"和"放下去"这两个动作一样,无论你用 vector 还是 list,下面这些操作你都能用:

函数 / 类型功能返回值
.size()返回容器中元素的个数size_t(无符号整数)
.empty()判断容器是否为空bool(空为 true)
.clear()清空所有元素
.begin()返回指向第一个元素的迭代器迭代器
.end()返回指向尾后(最后一个元素之后)迭代器
.front()返回第一个元素的引用元素的引用
.back()返回最后一个元素的引用元素的引用
.swap(b)交换两个同类容器的全部内容无(void)
::iterator容器的迭代器类型类型名

string 字符串

C++ 标准库提供了 string 类型,专门用来处理文字信息。它比 C 风格的字符数组(比如 char s[100])更安全、更灵活,是处理字符串的首选工具。使用前需引入 <string> 头文件。

11.1.1 定义与初始化
C++ · string 的几种初始化方式
1#include <iostream>
2#include <string> // 必须包含这个头文件
3using namespace std;
4
5int main()
6{
7 string s1; // 空字符串
8 string s2 = "Hello"; // 从 C 风格字符串初始化
9 string s3(s2); // 拷贝构造
10 string s4(5, 'a'); // "aaaaa"——5 个字符 'a'
11 // string s5 = "Hello"s; // C++14 字面量语法,需 using namespace std::string_literals;
12
13 return 0;
14}
⚠️
注意:
· 未初始化的 string 默认为空字符串(长度为 0),不会像字符数组那样出现乱码。
· 虽然 string 支持通过下标 [] 访问字符,但不能给还不存在的位置赋值
11.1.2 输入与输出

读字符串有两种常见方式:只要一个单词用 cin >>,要一整行(包括空格)用 getline()。两者混用时有一个经典的坑,一定要小心。

C++ · cin 与 getline 的区别
1string word, line;
2
3// 情况一:读一个单词(遇到空格就停)
4cin >> word; // 输入 "Hello World" → word 得到 "Hello"
5
6// 情况二:读一整行(包括空格)
7getline(cin, line); // 输入 "Hello World" → line 得到 "Hello World"
8
9// ⚠️ 混用时的大坑!
10int n;
11cin >> n; // 输入 "42\n",读到 42,但缓冲区还剩一个 '\n'
12cin.ignore(); // 🔑 把残留的换行符清掉
13getline(cin, line); // 现在才能正常读下一行
📖
为什么混用会出问题?
cin >> n 读取完数字后,你按下的回车键(\n)会残留在输入缓冲区里。紧接着 getline 一上来就看到了这个换行符,以为"哦,这行结束了",于是直接返回一个空字符串——看起来就像程序"跳过"了输入。

解决办法:cin 之后、getline 之前,加一句 cin.ignore(),把残留的换行符清掉即可。
💡
选择指南:
· 只读单个单词(无空格)→ cin >> str;
· 需要读含空格的整行 → getline(cin, str);
· 混用 cingetline 时,务必用 cin.ignore() 清理换行符
11.1.3 常用操作

🔗 拼接与比较 string 支持用 ++= 进行拼接,也支持直接用比较运算符进行字典序比较。

C++ · 拼接与字典序比较
1string s = "Hello";
2s = s + " World"; // "Hello World"
3s += "!"; // "Hello World!"
4
5// 字典序比较:从左到右逐字符比较 ASCII 码
6string a = "abc", b = "abd";
7cout << (a < b); // 1 (true),因为 'c' < 'd'
8cout << (a == b); // 0 (false)
📐
字典序规则:从左到右逐个字符比较,遇到第一个不同字符时,ASCII 值小的字符串更小;若一方为另一方的前缀,则较短的更小(如 "ab" < "abc")。

🔍 元素访问:[ ] vs at()

字符串 "Hello" 的内存格子
H
[0]
e
[1]
l
[2]
l
[3]
o
[4]
\0
[5]
C++ · 元素访问
1string s = "Hello";
2char c1 = s[0]; // 'H',不检查越界——快速,但需自行保证安全
3char c2 = s.at(0); // 'H',越界时抛出异常——安全,但略有开销
4char c3 = s.front(); // 'H'
5char c4 = s.back(); // 'o'
方式建议
[]更快但不安全,越界访问是未定义行为
at()更安全但稍慢,越界时会抛出异常提醒你
🎯
建议:竞赛中确定不越界时用 [],调试阶段或不确定时用 at()

✏️ 修改操作

C++ · 增删改字符串
1string s = "Hello";
2s.append(" World"); // "Hello World"——在末尾追加
3s.push_back('!'); // "Hello World!"——在末尾追加一个字符
4s.pop_back(); // "Hello World"——删除最后一个字符
5s.insert(5, " C++"); // "Hello C++ World"——在下标 5 处插入
6s.erase(2, 3); // 删除从下标 2 开始的 3 个字符
7s.replace(0, 5, "Hi"); // 把下标 0 起的 5 个字符替换为 "Hi"
8s1.swap(s2); // 交换两个字符串的全部内容(速度极快)

🔎 查找操作

C++ · find / rfind / find_first_of
1string s = "Hello World";
2size_t pos = s.find("World"); // 查找 "World",返回首次出现位置(这里是 6)
3size_t pos2 = s.rfind("l"); // 从后往前找 'l',返回最后一次出现的位置
4size_t pos3 = s.find_first_of("aeiou"); // 查找元音字母第一次出现的位置
5
6if (s.find("x") == string::npos) { // npos 表示"没找到"
7 cout << "没找到'x'" << endl;
8}
📖
string::npos 是什么?你可以把它理解为"无效位置"。当 find() 找不到你要的东西时,就返回 string::npos 来告诉你"抱歉,没找到"。使用时只需要记住:拿 find() 的返回值和 string::npos 比较,相等就说明没找到。

✂️ 截取与转换

C++ · substr 与数字互转
1string s = "Hello World";
2string sub = s.substr(0, 5); // "Hello"——从下标 0 开始,截取 5 个字符
3
4// 数字 ↔ 字符串互转(C++11 起支持)
5string numStr = to_string(42); // 数字 → 字符串:"42"
6int n = stoi("123"); // 字符串 → 整数:123
7double d = stod("3.14159"); // 字符串 → 小数:3.14159

🔄 迭代器遍历

string 想象成一排连续的格子,每个格子里放一个字符。迭代器就像夹在格子之间的"书签"——它记住了你当前所在的位置。你可以用 *it 读取当前格子的字符,用 ++it 把书签往后移一格。

C++ · 用迭代器和范围 for 遍历
1string s = "Hi";
2// begin() 指向第一个字符,end() 指向尾后位置
3for (auto it = s.begin(); it != s.end(); ++it)
4{
5 cout << *it << " "; // 输出:H i
6}
7
8// C++11 范围 for 循环(底层也是用迭代器)
9for (char c : s)
10{
11 cout << c; // 输出:Hi
12}
📌
小贴士:s.begin() 返回指向第一个字符的迭代器,s.end() 返回指向最后一个字符后面的迭代器——它不指向任何字符,只表示"已经遍历完了"。

vector 动态数组

vector 是 STL 中最常用的容器,可以把它理解成一个能自动伸缩的数组。当你需要一个可以随时增减元素、且能用下标快速访问任意位置的序列时,vector 是第一选择。使用前需引入 <vector> 头文件。

11.2.1 定义与初始化
C++ · vector 的几种初始化方式
1#include <iostream>
2#include <vector>
3using namespace std;
4
5int main()
6{
7 vector<int> v1; // 空数组,里面什么都没有
8 vector<int> v2(5); // 5 个元素,全部自动初始化为 0
9 vector<int> v3(5, 7); // 5 个元素,全部初始化为 7
10 vector<int> v4 = {1, 2, 3, 4, 5}; // 直接用花括号列出元素(最直观的写法)
11 vector<int> v5(v4); // 拷贝构造——复制一个一模一样的数组
12
13 // 二维数组:3 行 4 列,全部初始化为 0
14 vector<vector<int>> matrix(3, vector<int>(4, 0));
15 return 0;
16}
⚠️
注意事项:
· vector<int> v(n) 创建的 n 个元素会被自动初始化为 0,不会像局部数组那样留下"脏数据"。
· vector<int> v(n, val) 创建的 n 个元素会被初始化为指定的 val
· 初始化列表 {...} 是 C++11 引入的语法,简洁直观,优先使用。
11.2.2 常用操作

📏 容量管理

size() 与 capacity() 的区别
已用(size = 3)
预留空间(未用)
实际存储的元素 — size() 已分配但还没用 — capacity() 多出的部分
C++ · 容量相关操作
1vector<int> v = {1, 2, 3};
2v.size(); // 3——当前有多少个元素
3v.capacity(); // 已分配的内存最多能装多少个(通常 ≥ size)
4v.empty(); // false——判断是否为空
5v.clear(); // 清空所有元素,size 变成 0
6v.resize(5); // 把大小调整为 5,多出来的位置填 0
7v.resize(10, -1); // 把大小调整为 10,多出来的位置填 -1
8v.reserve(1000); // 提前预留 1000 个位置的空间
9v.shrink_to_fit(); // 把多余的内存释放掉
💡
reserve() 有什么用?
vector 扩容时,需要重新找一块更大的内存,然后把旧数据全部搬过去——这很花时间。如果你预先知道大概要存 1000 个元素,提前调用 reserve(1000) 就可以避免反复搬家,显著提升性能。这是竞赛中常见的优化手段。
⚠️
clear() 的陷阱:clear() 只把元素清掉,让 size() 变成 0,但已经占用的内存不会释放。如果想真正释放内存,可以用 vector<int>().swap(v) 这个小技巧。

🔍 元素访问

C++ · 元素访问
1vector<int> v = {10, 20, 30};
2int x = v[0]; // 10——用下标访问,不检查越界
3int y = v.at(0); // 10——用 at 访问,越界时会报错
4int first = v.front(); // 10——第一个元素
5int last = v.back(); // 30——最后一个元素
6int* p = v.data(); // 获取底层数组的原始指针(需要调用 C 函数时有用)
🔌
data() 的妙用:有些老旧的 C 语言函数需要传入数组指针(比如 memcpyscanf),这时可以用 v.data() 把 vector 当成普通数组来用。
🎯
效率须知:vector尾部增删元素是 O(1),非常快。但在头部或中间插入/删除是 O(n),因为需要把后面的元素全部挪位置。如果需要频繁在中间操作,应该考虑用 list

✏️ 增删修改

C++ · push_back / insert / erase
1vector<int> v = {1, 2, 3};
2v.push_back(4); // 在末尾追加:{1, 2, 3, 4}
3v.pop_back(); // 删除最后一个:{1, 2, 3}
4v.insert(v.begin() + 1, 99); // 在下标 1 处插入 99:{1, 99, 2, 3}
5v.erase(v.begin() + 1); // 删除下标 1 的元素:{1, 2, 3}
6v1.swap(v2); // 交换两个 vector 的全部内容(O(1),极快)
7v.emplace_back(10, 'a'); // 在末尾原地构造元素(比 push_back 更高效)
💡
emplace_back vs push_back:push_back 是先造好一个临时对象再拷贝进去,emplace_back 是直接在末尾原地构造,省去了拷贝这一步。对于简单的 int 差别不大,但对于复杂的类型(比如 pair、结构体),emplace_back 更高效。
🔥 经典陷阱:删除时迭代器失效
C++ · erase 与迭代器失效
1// ❌ 错误写法——erase 之后迭代器已经"过期"了
2for (auto it = v.begin(); it != v.end(); ++it)
3{
4 if (*it == target) v.erase(it); // 危险!it 已失效,++it 会出错
5}
6
7// ✅ 正确写法——利用 erase 返回的新迭代器
8for (auto it = v.begin(); it != v.end(); )
9{
10 if (*it == target)
11 it = v.erase(it); // erase 返回被删除元素的下一个位置
12 else
13 ++it;
14}
🔥
这是竞赛和工程中最经典的 STL 陷阱之一:删除元素后,原来的迭代器立刻失效,再对它做 ++ 是未定义行为,轻则结果错误,重则程序崩溃。记住口诀:erase 返回新位置,用它接着往下走,不要再手动 ++it
11.2.3 完整使用示例
C++ · vector 综合示例
1#include <iostream>
2#include <vector>
3using namespace std;
4
5int main()
6{
7 vector<int> v = {1, 2, 3, 4, 5};
8
9 // 遍历方式一:下标
10 for (size_t i = 0; i < v.size(); i++) {
11 cout << v[i] << " ";
12 }
13 cout << endl;
14
15 // 遍历方式二:范围 for(最简洁)
16 for (int x : v) {
17 cout << x << " ";
18 }
19 cout << endl;
20
21 // 删除偶数
22 for (auto it = v.begin(); it != v.end(); ) {
23 if (*it % 2 == 0)
24 it = v.erase(it);
25 else
26 ++it;
27 }
28 // v 变成 {1, 3, 5}
29
30 cout << "size: " << v.size() << endl; // 输出:3
31
32 return 0;
33}

list 双向链表

list 是一个双向链表。链表不像数组那样所有元素挤在一块连续的内存里,而是每个元素各住各的,彼此用"指针"串起来。这带来的好处是:在任意位置插入或删除元素都是 O(1),快到飞起。代价是:不能用下标直接跳到第 n 个元素,只能顺着链条一个一个找。使用前需引入 <list> 头文件。

list 的内存结构:每个节点都有自己的位置,靠箭头串联
head →
1
2
3
4
→ nullptr
每个节点散落在内存的不同位置(不连续),通过指针互相连接。在中间插入新节点只需改两个指针,不用搬动其它元素。
⚠️
注意:
· list 不支持随机访问,不能像 vector 那样用 l[0]l.at(0) 访问元素,必须通过迭代器遍历。
· list 支持 O(1) 的头部插入/删除(push_front / pop_front),这是 vector 不具备的。
11.3.1 定义与初始化
C++ · list 的几种初始化方式
1#include <iostream>
2#include <list> // list 位于此头文件中
3using namespace std;
4
5int main()
6{
7 list<int> l1; // 空链表
8 list<int> l2(5); // 5 个元素,全部初始化为 0
9 list<int> l3(5, 7); // 5 个元素,全部初始化为 7
10 list<int> l4 = {1, 2, 3, 4, 5}; // 用花括号初始化
11 list<int> l5(l4); // 拷贝构造
12 return 0;
13}
11.3.2 常用操作

🔍 元素访问

C++ · list 的元素访问
1list<int> l = {10, 20, 30};
2int first = l.front(); // 10——第一个元素
3int last = l.back(); // 30——最后一个元素
4// ❌ l[0] —— 编译错误!list 不支持下标访问

✏️ 增删修改

C++ · push_front / push_back / insert / erase
1list<int> l = {2, 3, 4};
2l.push_front(1); // {1, 2, 3, 4}——头部插入
3l.push_back(5); // {1, 2, 3, 4, 5}——尾部插入
4l.pop_front(); // {2, 3, 4, 5}——头部删除
5l.pop_back(); // {2, 3, 4}——尾部删除
6
7auto it = l.begin();
8++it; // it 指向第二个元素(3)
9l.insert(it, 99); // 在 it 前面插入 99:{2, 99, 3, 4}
10l.erase(it); // 删除 it 指向的元素:{2, 99, 4}
11l1.swap(l2); // 交换两个链表的内容

🔄 迭代器遍历

C++ · list 的遍历方式
1list<int> l = {1, 2, 3};
2// 用迭代器遍历(list 的迭代器只支持 ++ 和 --,不支持 it + 3 这种跳转)
3for (auto it = l.begin(); it != l.end(); ++it)
4{
5 cout << *it << " "; // 输出:1 2 3
6}
7
8// 范围 for 更简洁
9for (int x : l) {
10 cout << x << " "; // 输出:1 2 3
11}
11.3.3 完整使用示例
C++ · list 综合示例
1#include <iostream>
2#include <list>
3using namespace std;
4
5int main()
6{
7 list<int> l;
8
9 // 在头部和尾部随意插入
10 l.push_back(2);
11 l.push_front(1);
12 l.push_back(3);
13 // l = {1, 2, 3}
14
15 cout << "链表内容: ";
16 for (int x : l) {
17 cout << x << " "; // 输出:1 2 3
18 }
19 cout << endl;
20
21 // 删除中间的偶数
22 for (auto it = l.begin(); it != l.end(); ) {
23 if (*it % 2 == 0)
24 it = l.erase(it); // list 的 erase 也返回下一个有效迭代器
25 else
26 ++it;
27 }
28 // l = {1, 3}
29
30 cout << "首元素: " << l.front() << endl; // 输出:1
31 cout << "尾元素: " << l.back() << endl; // 输出:3
32 cout << "大小: " << l.size() << endl; // 输出:2
33
34 return 0;
35}

deque 双端队列

deque 是 C++ 标准库中的双端队列,全称 Double-Ended Queue。是 vectorlist 的"混血儿":它既支持下标随机访问,又支持在头部和尾部快速增删。底层由多段连续空间拼接而成,所以头部操作也是 O(1)。使用前需引入 <deque> 头文件。

deque:两端都能 O(1) 操作
1
2
3
4
⬅ push_front / pop_front push_back / pop_back ➡
⚠️
注意:
· deque 支持 O(1) 的头部和尾部插入/删除,弥补了 vector 头部操作慢的缺点。
· deque 支持 operator[]at() 随机访问,弥补了 list 不能随机访问的缺点。
· deque 的底层是分段连续空间,不是一整块连续内存,因此某些需要连续内存指针的场景(如传给 C 风格接口)不适合用 deque
11.4.1 定义与初始化
C++ · deque 的几种初始化方式
1#include <iostream>
2#include <deque> // 必须包含这个头文件
3using namespace std;
4
5int main()
6{
7 deque<int> dq1; // 空双端队列
8 deque<int> dq2(5); // 5 个元素,全部初始化为 0
9 deque<int> dq3(5, 7); // 5 个元素,全部初始化为 7
10 deque<int> dq4 = {1, 2, 3, 4, 5}; // 初始化列表
11 deque<int> dq5(dq4); // 拷贝构造
12 return 0;
13}
11.4.2 常用操作

deque 拥有 vector 的全部功能([]at()size()empty() 等),同时额外支持头部操作:

C++ · deque 的双端操作 + 随机访问
1deque<int> dq = {2, 3, 4};
2// 头部操作(vector 没有的!)
3dq.push_front(1); // {1, 2, 3, 4}
4dq.pop_front(); // {2, 3, 4}
5// 尾部操作(和 vector 一样)
6dq.push_back(5); // {2, 3, 4, 5}
7dq.pop_back(); // {2, 3, 4}
8// 随机访问(list 没有的!)
9cout << dq[1] << endl; // 输出:3——支持下标访问
10cout << dq.at(0) << endl; // 输出:2——支持 at 访问
⚠️
注意:deque 底层是分段连续空间,不是一整块连续内存,所以没有 data() 函数。如果需要把数据传给要求连续内存的 C 函数,不能用 deque,请用 vector
11.4.3 完整使用示例
C++ · deque 综合示例
1#include <iostream>
2#include <deque>
3using namespace std;
4
5int main()
6{
7 deque<int> dq;
8
9 // 头部和尾部都能插入
10 dq.push_back(2); // {2}
11 dq.push_front(1); // {1, 2}
12 dq.push_back(3); // {1, 2, 3}
13 dq.push_front(0); // {0, 1, 2, 3}
14
15 // 随机访问
16 cout << "第2个元素: " << dq[1] << endl; // 输出:1
17 cout << "首元素: " << dq.front() << endl; // 输出:0
18 cout << "尾元素: " << dq.back() << endl; // 输出:3
19
20 // 遍历
21 cout << "遍历: ";
22 for (int x : dq) {
23 cout << x << " "; // 输出:0 1 2 3
24 }
25 cout << endl;
26
27 // 头部和尾部删除
28 dq.pop_front(); // {1, 2, 3}
29 dq.pop_back(); // {1, 2}
30
31 cout << "大小: " << dq.size() << endl; // 输出:2
32
33 return 0;
34}
四种序列容器对比
容器随机访问头部增删尾部增删中间增删典型场景
vectorO(1)O(n)O(1)O(n)大多数场景的默认选择
list不支持O(1)O(1)O(1)频繁在中间插入/删除
dequeO(1)O(1)O(1)O(n)两端都要快速操作(如滑动窗口)
stringO(1)O(n)O(1)O(n)文本处理(本质是特化的 vector<char>)