C++ 竞赛入门
vector 动态数组
长度能变的数组:按下标读写,尾部增删很快,中间插删要移动元素。
vector 动态数组
读完能做什么
- 能创建
vector,用下标读出、改写里面的元素,并说清下标从几开始。 - 能用
push_back、pop_back、insert、erase增删元素,用assign整体换一批数据,并说出每一步操作后size()是多少。 - 能区分“长度
size()”和“预留空间capacity()”,知道reserve不会增加元素、resize/assign才会改变元素个数。 - 能解释为什么在尾部增删很快,而在中间插入、删除要移动元素。
为什么需要它
数组的长度在写代码时就定死了:
int a[10]; // 只能放 10 个数
开小了不够用,开大了浪费。更麻烦的是竞赛题常把长度放在输入里:先读一个 n,再读 n 个数。n 要运行到那一行才知道,用它开数组不是标准 C++ 的写法:
// 错误示例:数组长度必须是编译期常量,不能是运行时才读到的变量
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
int a[n]; // 变长数组不是标准 C++:本机编译器只当作扩展放过,严格模式会报错
return 0;
}
这段不能作为标准写法使用。本机编译器把它当成扩展接受了(在 -pedantic-errors 严格模式下会直接报 ISO C++ forbids variable length array),但换一个编译器、换一种编译选项就可能编不过。我们需要一个“长度可以变、用起来还像数组”的容器,这就是 vector。
它是什么
一句话:vector 是标准库提供的能改变长度的数组,也叫动态数组。
直观模型是一排连续的格子,每个格子放一个同类型的数据,格子从左到右编号 0、1、2……:
| 下标 | 0 | 1 | 2 |
|---|---|---|---|
| 元素 | 10 | 20 | 30 |
我们可以像数组一样用 v[1] 读写某个格子,也可以在右端接上新格子、把末尾的格子去掉。
模板尖括号。 vector<int> 中的 <int> 是在告诉编译器“每个格子放什么类型”。vector<int> 是整数的 vector,vector<double>、vector<string> 同理。这是 C++ 的模板语法:同一份 vector 的代码可以给不同类型使用,尖括号里填具体类型即可。vector<vector<int>> 表示每个格子本身也是一个 vector<int>(C++17 里最右边的 >> 不用加空格)。
下标。 第 i 个元素写作 v[i],下标从 0 开始,最后一个元素的下标是 size() - 1。
size() 与 capacity()。 size() 是“现在有几个元素”,capacity() 是“当前已经申请下来的存储空间能放几个元素”。size() 永远不超过 capacity()。reserve(n) 只把 capacity() 提到至少 n,元素个数一个也不会变。
适用场景:需要按下标随机访问(想读第几个就读第几个),并且主要在尾部增删数据。使用时要 #include <vector>,本页示例用 #include <bits/stdc++.h> 一次包含全部标准头文件。
最小可运行示例
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> v = {10, 20, 30}; // 3 个元素,下标是 0、1、2
cout << "size = " << v.size() << '\n';
for (int i = 0; i < (int)v.size(); i++) {
cout << "v[" << i << "] = " << v[i] << '\n';
}
cout << "front = " << v.front() << ", back = " << v.back() << '\n';
v[1] = 25; // 用下标直接改元素
cout << "改后 v[1] = " << v[1] << '\n';
return 0;
}
输出:
size = 3
v[0] = 10
v[1] = 20
v[2] = 30
front = 10, back = 30
改后 v[1] = 25
逐行看:
vector<int> v = {10, 20, 30};创建一个vector<int>,按顺序放入 3 个元素。v.size()返回元素个数,类型是无符号整数size_t。循环条件写成(int)v.size()是为了把它转成int,和int i比较时不会出现“有符号与无符号比较”的警告。v[i]用下标取元素,可以用来读,也可以放在等号左边改写。v.front()返回第一个元素,v.back()返回最后一个元素。- 输出里
v[1] = 25说明下标 1 的位置被改掉了,长度仍是 3。
一步步看变化
下面用同一组数据 [10, 20, 30] 依次做四步。先把整条路线列出来:
| 步骤 | 操作 | 操作前 | 操作后 | size() |
|---|---|---|---|---|
| 0 | 初始 | — | [10,20,30] |
3 |
| 1 | v.push_back(40) |
[10,20,30] |
[10,20,30,40] |
4 |
| 2 | v.pop_back() |
[10,20,30,40] |
[10,20,30] |
3 |
| 3 | v.insert(v.begin()+1, 15) |
[10,20,30] |
[10,15,20,30] |
4 |
| 4 | v.erase(v.begin()+2) |
[10,15,20,30] |
[10,15,30] |
3 |
每一步都写成独立的小程序,输出就是这一步操作前后的状态。
第 1 步:尾部加入 40
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> v = {10, 20, 30};
cout << "操作前:";
for (int i = 0; i < (int)v.size(); i++) cout << v[i] << ' ';
cout << "size = " << v.size() << '\n';
v.push_back(40); // 在尾部加入 40
cout << "操作后:";
for (int i = 0; i < (int)v.size(); i++) cout << v[i] << ' ';
cout << "size = " << v.size() << '\n';
return 0;
}
输出:
操作前:10 20 30 size = 3
操作后:10 20 30 40 size = 4
push_back(40) 把 40 接到最右端,长度从 3 变成 4。原来的三个元素下标没变,新元素的 size()-1 就是它的下标。
第 2 步:删除尾部
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> v = {10, 20, 30, 40};
cout << "操作前:";
for (int i = 0; i < (int)v.size(); i++) cout << v[i] << ' ';
cout << "size = " << v.size() << '\n';
v.pop_back(); // 删掉最后一个元素 40
cout << "操作后:";
for (int i = 0; i < (int)v.size(); i++) cout << v[i] << ' ';
cout << "size = " << v.size() << '\n';
cout << "back = " << v.back() << '\n';
return 0;
}
输出:
操作前:10 20 30 40 size = 4
操作后:10 20 30 size = 3
back = 30
pop_back() 只能删最后一个元素,长度减 1,新的 back() 变成 30。它不返回被删掉的元素,删除前想用这个值就先 v.back() 取出来。
第 3 步:在下标 1 之前插入 15
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> v = {10, 20, 30};
cout << "操作前:";
for (int i = 0; i < (int)v.size(); i++) cout << v[i] << ' ';
cout << "size = " << v.size() << '\n';
v.insert(v.begin() + 1, 15); // 在下标 1 的元素之前插入 15
cout << "操作后:";
for (int i = 0; i < (int)v.size(); i++) cout << v[i] << ' ';
cout << "size = " << v.size() << '\n';
return 0;
}
输出:
操作前:10 20 30 size = 3
操作后:10 15 20 30 size = 4
这里出现了新写法 v.begin() + 1。v.begin() 返回一个迭代器:可以把它理解成“指向某个位置的游标”。v.begin() 指向下标 0,v.begin() + 1 指向下标 1,v.end() 指向最后一个元素后面一个位置。迭代器在容器、迭代器与区间里有系统讲解,这里先记住“它表示一个位置,可以和整数相加往后移”。
insert(pos, value) 的含义是“把 value 插到位置 pos 之前”。插入位置是下标 1,所以 15 落在 20 前面,原来的 20、30 各自往后挪一格:
| 时刻 | 下标 0 | 下标 1 | 下标 2 | 下标 3 |
|---|---|---|---|---|
| 插入前 | 10 | 20 | 30 | — |
| 插入 15 后 | 10 | 15 | 20 | 30 |
第 4 步:删除值为 20 的位置
上一步之后 20 在下标 2 上,所以删除的是 v.begin() + 2:
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> v = {10, 15, 20, 30};
cout << "操作前:";
for (int i = 0; i < (int)v.size(); i++) cout << v[i] << ' ';
cout << "size = " << v.size() << '\n';
v.erase(v.begin() + 2); // 删除下标 2 上的 20
cout << "操作后:";
for (int i = 0; i < (int)v.size(); i++) cout << v[i] << ' ';
cout << "size = " << v.size() << '\n';
return 0;
}
输出:
操作前:10 15 20 30 size = 4
操作后:10 15 30 size = 3
erase(pos) 删掉位置 pos 上的元素,后面的元素整体左移一格,长度减 1。注意 erase 接收的是位置,不是值:如果只知道“值为 20”而不知道它在下标几,需要先找到位置,查找可以用 find,见 find 查找。
逐项扩展核心操作
遍历:下标、范围 for 与引用
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> v = {10, 20, 30};
cout << "下标遍历:";
for (int i = 0; i < (int)v.size(); i++) cout << v[i] << ' ';
cout << '\n';
cout << "范围 for:";
for (int x : v) cout << x << ' ';
cout << '\n';
for (int& x : v) x *= 2; // 引用:改的是容器里的元素
cout << "每个元素乘 2 后:";
for (int x : v) cout << x << ' ';
cout << '\n';
return 0;
}
输出:
下标遍历:10 20 30
范围 for:10 20 30
每个元素乘 2 后:20 40 60
for (int x : v) 叫范围 for,意思是“把 v 里的元素逐个取出来,每次放到 x 里”。它从下标 0 走到最后一个元素,不用自己写循环变量。
int& x 中的 & 表示引用,即 x 就是容器里那个元素本身,不是它的复制品。所以 x *= 2 会真的改到 v 里的数据,输出变成 20 40 60。写成 for (int x : v) x *= 2; 只会改到一个临时副本,容器内容不变。
size() 与 capacity():长度和预留空间
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> v;
cout << "新建:size = " << v.size() << ", empty = " << v.empty() << '\n';
v.reserve(5); // 预留空间,但没有元素
cout << "reserve(5) 后:size = " << v.size() << '\n';
cout << "capacity 至少为 5:" << (v.capacity() >= 5) << '\n';
v.push_back(10);
v.push_back(20);
cout << "push_back 两次后:size = " << v.size() << '\n';
cout << "capacity 至少为 5:" << (v.capacity() >= 5) << '\n';
return 0;
}
输出:
新建:size = 0, empty = 1
reserve(5) 后:size = 0
capacity 至少为 5:1
push_back 两次后:size = 2
capacity 至少为 5:1
这段说明了三点:
- 空 vector 的
size()是 0,empty()返回true,打印成 1(bool输出默认是 0/1)。 reserve(5)之后size()还是 0:预留的只是空间,不是元素。此时v[0]是越界访问,不能读。- 加入两个元素后
size()变成 2,而capacity()仍然至少是 5,不需要再申请空间。
容量增长到多少由实现决定,标准只保证 capacity() >= size(),所以本页只打印“至少为 5”这个能确定的事实,不打印具体容量值。
clear() 与 empty()
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> v = {10, 20, 30};
cout << "clear 前:size = " << v.size() << ", empty = " << v.empty() << '\n';
v.clear();
cout << "clear 后:size = " << v.size() << ", empty = " << v.empty() << '\n';
v.push_back(99); // 清空之后还能继续用
cout << "再 push_back(99):size = " << v.size() << ", v[0] = " << v[0] << '\n';
return 0;
}
输出:
clear 前:size = 3, empty = 0
clear 后:size = 0, empty = 1
再 push_back(99):size = 1, v[0] = 99
clear() 一次删掉所有元素,size() 变成 0。清空后的容器可以继续使用,不需要重新创建。
assign():整体换一批数据
assign 的作用是整体换掉内容:它先去掉原来的所有元素,再按参数填新的。三种常用形式:
| 写法 | 含义 |
|---|---|
v.assign(n, x) |
变成 n 个 x |
v.assign({1, 2, 3}) |
用一组初值换掉内容 |
v.assign(first, last) |
用另一段区间 [first, last) 的值换掉内容 |
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> v = {10, 20, 30};
cout << "初始:size = " << v.size() << ",内容:";
for (int x : v) cout << x << ' ';
cout << '\n';
v.assign(4, 7); // 换成 4 个 7:原来的 10 20 30 全被丢掉
cout << "assign(4, 7) 后:size = " << v.size() << ",内容:";
for (int x : v) cout << x << ' ';
cout << '\n';
v.assign({1, 2, 3}); // 用一组初值整体替换
cout << "assign({1, 2, 3}) 后:size = " << v.size() << ",内容:";
for (int x : v) cout << x << ' ';
cout << '\n';
vector<int> src = {5, 6};
v.assign(src.begin(), src.end()); // 用另一段区间的值整体替换
cout << "assign(src 的区间) 后:size = " << v.size() << ",内容:";
for (int x : v) cout << x << ' ';
cout << '\n';
v.assign(0, 0); // 个数为 0:等于清空
cout << "assign(0, 0) 后:size = " << v.size() << ", empty = " << v.empty() << '\n';
return 0;
}
输出:
初始:size = 3,内容:10 20 30
assign(4, 7) 后:size = 4,内容:7 7 7 7
assign({1, 2, 3}) 后:size = 3,内容:1 2 3
assign(src 的区间) 后:size = 2,内容:5 6
assign(0, 0) 后:size = 0, empty = 1
三点要记住:
- 原来的元素一个不留。 第一次
assign(4, 7)之后10 20 30就没了,size()从 3 变成 4。 size()由参数决定。assign(0, 0)是“填 0 个元素”,效果就是清空,和clear()一样。- 旧位置全部失效。 内容整个换过,
assign之后要重新取v.begin()。
容易和它混的是 resize(n):resize 只把长度改成 n,原有元素保留,多出来的位置用默认值(或 resize(n, x) 里的 x)填;assign 则是把内容整体替换。想“按个数调整长度”用 resize,想“整批换掉”用 assign。
两种写法的复杂度都是 O(n):assign(n, x) 要写 n 个元素,assign(first, last) 要写区间长度那么多个。
竞赛写法:全局 vector 与运行时定长度
竞赛代码常把容器写成全局变量,因为它要绐 read_input()、solve()、dfs() 这些函数共用,写成全局就不用一层层传参;元素存在堆上,也不占函数的栈空间。
但全局声明的位置在 main 之前,那时 n 还没读进来,所以不能写 vector<int> a(n + 1);(n 不是常量)。做法是:先声明一个空的全局 vector,读入 n 之后用 resize 或 assign 把长度定下来。
#include <bits/stdc++.h>
using namespace std;
vector<int> a; // 全局容器:多个函数都能直接用,不用来回传参
int main() {
int n;
cin >> n;
a.resize(n + 1); // 长度定成 n+1:下标 0 空着,a[1]..a[n] 对应题目里的第 1..n 个数
cout << "resize 后:size = " << a.size() << ",a[1] = " << a[1] << '\n';
for (int i = 1; i <= n; i++) cin >> a[i];
int sum_value = 0;
for (int i = 1; i <= n; i++) sum_value += a[i];
cout << "读入 " << n << " 个数,和 = " << sum_value << '\n';
a.assign(3, 123); // 整体换掉:变成 3 个 123,原来的数据全丢
cout << "assign(3, 123) 后:size = " << a.size() << ",内容:";
for (int x : a) cout << x << ' ';
cout << '\n';
a.resize(5, 7); // 长度改成 5:前 3 个保留,后面补 7
cout << "resize(5, 7) 后:size = " << a.size() << ",内容:";
for (int x : a) cout << x << ' ';
cout << '\n';
return 0;
}
输入:
3
5 1 4
输出:
resize 后:size = 4,a[1] = 0
读入 3 个数,和 = 10
assign(3, 123) 后:size = 3,内容:123 123 123
resize(5, 7) 后:size = 5,内容:123 123 123 7 7
逐行看:
resize(n + 1)把size()定成n+1,新长出来的元素是默认值(int是 0,string是空串)——所以还没读入时a[1]就是0。下标 0 空着、数据放在a[1]..a[n],这就是竞赛里常见的“下标从 1 开始”写法。- 读入
5 1 4后求和得到 10,说明resize出来的格子可以直接当数组用。 assign(3, 123)把内容整体换成 3 个 123:长度变 3,原来的5 1 4全被丢掉。resize(5, 7)只把长度从 3 改到 5:前 3 个保留,新长出的两个位置用 7 填。
三个容易混的操作,放一起对比:
| 写法 | 长度 | 原有元素 | 典型用途 |
|---|---|---|---|
a.resize(n + 1) |
变成 n+1 |
保留 | 读入 n 后给全局容器定长度 |
a.resize(n + 1, x) |
变成 n+1 |
保留,新位置填 x |
把整张表清成同一个值,又不丢已有数据 |
a.assign(n + 1, x) |
变成 n+1 |
全部丢弃 | 把容器整批重置成 n+1 个 x |
最后提醒一句:resize 的参数就是新的元素个数。写成 a.resize(100) 就是长度变成 100,不要把它当成“下标”或“容量”。
什么时候选它、哪里容易错
下标必须有效。 只有 3 个元素时 v[3] 是越界访问,结果不确定,不能拿它当“读到 0”。v.at(3) 会做检查并在越界时抛异常,但竞赛代码一般用 [],越界责任在自己。用 v[i] 之前确认 i 在 [0, size()) 内。
reserve 不增加元素个数。 reserve(100) 之后 size() 仍是原来的值,v[0] 在 size() == 0 时不能读。
assign 会丢掉原来的内容。 v.assign(n, x) 之后 v 里只剩 n 个 x,原来的元素一个不留,size() 也跟着变。想保留元素只改长度用 resize,想追加内容用 push_back 或 insert。
全局容器要先声明、再定长度。 想给多个函数共用而把容器写成全局变量时,不能写 vector<int> a(n + 1);(那时 n 还没读入,不是常量);要先写 vector<int> a;,读入 n 后用 a.resize(n + 1); 或 a.assign(n + 1, 0); 把长度定下来。给全局容器忘了定长度就直接 a[i] 赋值,是典型的“空容器越界”。
插删之后不要盲用旧的位置。 扩容、insert、erase 都可能让之前记下的迭代器(begin() 这类游标)失效。要接着用就重新取一次 v.begin()。
复杂度要分清位置。 尾部的 push_back、pop_back 是摊还常数时间(“摊还”指偶尔一次扩容比较贵,但把这次开销平摊到前面许多次操作上,平均下来每次仍是常数);在中间 insert、erase 一般要把后面的元素整体搬动,是 O(n),和数组里挪元素一样慢。
扩容倍数不是标准规定。 不要依赖“翻倍”之类的具体值,也不要比较不同环境下的 capacity()。
选读。 vector 底层是一整块连续内存。元素超过现有空间时会申请一块更大的内存,把元素搬过去再释放旧的,这就是“旧迭代器失效”的原因。不了解这些细节也可以正常使用 vector。
操作速查表
| 操作 | 作用 | 备注 |
|---|---|---|
vector<int> v; |
创建空的 vector | 尖括号里填元素类型 |
vector<int> v = {10,20,30}; |
用一组初值创建 | 元素个数即 size() |
v.size() |
元素个数 | 类型是无符号整数 |
v.empty() |
是否为空 | 打印成 0/1 |
v[i] |
下标访问 | 不检查越界 |
v.at(i) |
下标访问 | 越界会抛异常 |
v.front() / v.back() |
首元素 / 末元素 | 容器为空时不能调用 |
v.push_back(x) |
尾部加入 x |
摊还 O(1) |
v.pop_back() |
删除末尾元素 | 不返回被删的值 |
v.insert(v.begin()+k, x) |
在下标 k 前插入 x |
后面元素后移,O(n) |
v.erase(v.begin()+k) |
删除下标 k 的元素 |
后面元素前移,O(n) |
v.clear() |
删除全部元素 | size() 变 0 |
v.assign(n, x) |
换成 n 个 x |
丢掉原有元素,O(n) |
v.assign({1,2,3}) |
用一组初值换掉内容 | 丢掉原有元素,O(n) |
v.assign(first, last) |
用区间 [first,last) 的值换掉内容 |
丢掉原有元素,O(区间长度) |
v.resize(n) / v.resize(n, x) |
把长度改成 n |
原有元素保留;多出的位置用默认值或 x 填(全局容器常用它定长度) |
v.reserve(n) |
预留至少 n 个元素的空间 |
不改变 size() |
v.capacity() |
当前空间能放几个元素 | 一定不小于 size() |
动手检查
第 1 题:预测输出
下面是一段片段(省略了头文件和 main),请先手推出输出,再运行核对:
vector<int> v = {1, 2, 3};
v.push_back(4); // 尾部加 4
v.insert(v.begin(), 0); // 头部插入 0
v.pop_back(); // 删掉尾部
v.assign(3, 9); // 整体换掉:变成 3 个 9
v.push_back(8); // 再在尾部加 8
cout << v.size() << ' ' << v[0] << ' ' << v.back() << '\n';
第 2 题:修改代码
下面这段片段把 4 个数存进 vector,请补上删除首个元素的操作,让输出变成 20 30 40,size() 变成 3:
vector<int> v = {10, 20, 30, 40};
// 在这里补一行:删掉首个元素
cout << "size = " << v.size() << '\n';
for (int i = 0; i < (int)v.size(); i++) cout << v[i] << ' ';
验证条件:先说出补完之后 size() 是多少、剩下哪些元素,再运行核对。
参考结果与解释
第 1 题:输出是 4 9 8。逐步推:
| 时刻 | 内容 | size |
|---|---|---|
| 初始 | [1,2,3] |
3 |
push_back(4) |
[1,2,3,4] |
4 |
insert(v.begin(), 0) |
[0,1,2,3,4] |
5 |
pop_back() |
[0,1,2,3] |
4 |
assign(3, 9) |
[9,9,9] |
3 |
push_back(8) |
[9,9,9,8] |
4 |
所以 size() 是 4,v[0] 是 9,v.back() 是 8。注意 assign(3, 9) 把前面的 0 1 2 3 全部丢掉,长度也跟着变成 3——它不是追加,而是整体替换。完整程序与实测输出:
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> v = {1, 2, 3};
v.push_back(4); // 尾部加 4
v.insert(v.begin(), 0); // 头部插入 0
v.pop_back(); // 删掉尾部
v.assign(3, 9); // 整体换掉:变成 3 个 9
v.push_back(8); // 再在尾部加 8
cout << v.size() << ' ' << v[0] << ' ' << v.back() << '\n';
return 0;
}
输出:
4 9 8
第 2 题:补上 v.erase(v.begin()); 即可。v.begin() 指向下标 0 的元素,erase 删除它,剩下的元素整体左移,长度减 1。注意不能写 v.erase(10)——erase 要的是位置而不是值。
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> v = {10, 20, 30, 40};
v.erase(v.begin()); // 删除首个元素
cout << "size = " << v.size() << '\n';
for (int i = 0; i < (int)v.size(); i++) cout << v[i] << ' ';
cout << '\n';
return 0;
}
输出:
size = 3
20 30 40
立即练习
| 难度 | 题目 | 题解 | 训练目标 |
|---|---|---|---|
| 入门 | 洛谷 P3156:【深基15.例1】询问学号 | 题解 | 用 vector 按下标保存 n 个学号,再按查询的下标直接读出答案,体会“长度由输入决定”。 |
| 入门 | 洛谷 P5727:【深基5.例3】冰雹猜想 | 题解 | 把循环里产生的每个数依次 push_back 进 vector,再按下标从后往前输出,练习“长度事先不知道”的存数据方式。 |
两道题都只需要数组、循环和本页讲过的 vector 操作,不需要其他数据结构。做完后请说明:为什么这里用 vector 比用固定长度的数组更省事。