课程 · C++ 速查 · 数组与 vector

数组与 vector 例题与练习

把「读 n → 开数组 → 读进去 → 扫一遍」这套本书每道题都要写一遍的开头,练到不用想。

2 道例题3 道练习5 份可运行代码回速查看这一组的 8 条 →
怎么用这一页

例题给全套:题面、样例、参考代码、以及一步步的讲解。先自己读题想一遍, 再看代码,最后对着讲解核一遍自己想的对不对。

练习只给题面、样例和一句提示,答案是折叠起来的 —— 请先自己写一版跑通了再展开。写不出来也别直接看答案,先回速查那一组找找。

每份代码都能直接点运行:输入框里已经填好了样例输入, 把它改一改再跑,是这一页最值钱的用法。

先把地基摆一遍

4 段

你已经会写 int a[100]; 了。vector 就是同一个东西,只多给你三件事:长度可以是运行时才知道的变量可以随时往后加它自己知道自己有多长。除此之外用法一模一样 —— 照样 a[0]a[i],下标照样从 0 开始。

★ 为什么本书几乎全用它而不用裸数组:一是题目的 n 是读进来的,int a[n]; 在标准 C++ 里其实不合法(GCC 容忍而已);二是 vector<int> a(n); 保证全是 0,而函数里的 int a[100]; 里面是垃圾值。

⚠ 但有一个场合反过来用裸数组:几百万个元素的大数组要开成全局的。写在 main 里的数组放在栈上(一般只有 8 MB),开大了直接段错误;全局数组不占栈,而且自动清零。竞赛代码里 const int N = 100005; int a[N]; 就是这个习惯。

★ 还有一条贯穿全书的:数组越界 C++ 不会报错。 它只是安静地读写了别人的内存,于是程序可能在一个毫不相干的地方崩掉,或者答案莫名其妙 —— 查这类问题的第一招是临时把 a[i] 换成 a.at(i)

例题(2 道)

每道都给全套:题面 → 样例 → 参考代码 → 一步步的讲解。

例题 ① 读一组数,报四个数

vecEx1.cpp

n 个整数,请依次输出它们的最大值、最小值、总和,再把它们倒序打印一行。

输入格式第一行一个整数 n1 ≤ n ≤ 10⁵);第二行 n 个整数 aᵢ|aᵢ| ≤ 10⁹)。
输出格式第一行三个整数,用空格隔开;第二行 n 个整数,倒序,用空格隔开,行末不要有空格

输入

5
3 1 4 1 5

输出

5 1 14
5 1 4 1 3
3 1 4 1 5 的最大 5、最小 1、和 14,倒过来是 5 1 4 1 3
先想一想

这道题的形状就是本书每道题的开头。唯一要想一下的是:n读进来的,那数组该怎么开?

vecEx1.cpp参考代码
// 例题 ①「读一组数,报四个数」—— 数组与 vector 组
//
// 本书每道题的开头几乎都长这样:先读 n,再开一个长度 n 的 vector,然后读进去。
// 注意 n 是**读进来的**,普通数组写 int a[n]; 在标准 C++ 里其实不合法 —— 这正是用 vector 的理由。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> a(n); // 长度 n,全部初始化成 0
for (int i = 0; i < n; i++) cin >> a[i];
int mx = a[0], mn = a[0];
long long sum = 0;
for (int x : a) { // 范围 for:把 a 里每个元素依次取出来叫 x
mx = max(mx, x);
mn = min(mn, x);
sum += x;
}
cout << mx << ' ' << mn << ' ' << sum << "\n";
// 倒序输出:要用到下标,就老老实实写普通 for
for (int i = n - 1; i >= 0; i--) {
cout << a[i];
if (i > 0) cout << ' '; // 行末不留空格
}
cout << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
这段代码是怎么想出来的
  1. vector<int> a(n); —— 读成「一个装 int 的 vector,名字叫 a,长度 n」。尖括号里写装什么,圆括号里写装多少个。它保证每一格都是 0。
  2. ⚠ 为什么不写 int a[n];n 是运行时才知道的,这种写法在标准 C++ 里其实不合法(GCC 容忍而已)。而且函数里的普通数组不会自动清零,里面是上一个用这块内存的人留下的垃圾值。
  3. 只读地扫一遍用范围 for:for (int x : a),读成「把 a 里每个元素依次取出来叫 x」。⚠ x一份复制品 —— 要改元素得写 for (int &x : a)
  4. 倒序输出要用到下标 ⇒ 老老实实写 for (int i = n - 1; i >= 0; i--)。⇒ 判据很简单:用得到下标就写普通 for,用不到就写范围 for。
  5. ⚠ 和最后那个 if (i > 0) cout << ' '; —— 行末不留空格。这不是洁癖:本书第 47 章量过,「行末多一个空格」在 300 轮对拍里被抓了 284 次。
记住这一句

顺带一条:n 个 10⁹ 加起来可以到 10¹⁴ ⇒ sum 必须是 long long,而数组本身用 int 就够。「要不要 long long」永远是按那一次运算问的。

用到的写法:vector<int> a(n) for (int x : a) a.size() / a.push_back(x) / a.back()

例题 ② 矩阵转置

vecEx2.cpp

给一个 RC 列的矩阵,请输出它的转置 —— 也就是把行和列对调之后的那个 CR 列的矩阵。

说人话:原来第 i 行第 j 列的那个数,转置之后跑到第 j 行第 i 列。

输入格式第一行两个整数 RC1 ≤ R, C ≤ 100);接下来 R 行,每行 C 个整数。
输出格式C 行,每行 R 个整数,用空格隔开,行末不要有空格。

输入

2 3
1 2 3
4 5 6

输出

1 4
2 5
3 6
2 行 3 列转置之后是 3 行 2 列 —— 注意两个维度换过来了
先想一想

二维 vector 的括号里有两个数字,而它们不能写反。先想清楚:vector<vector<int>> f(R, vector<int>(C)) 里哪个是行数?

vecEx2.cpp参考代码
// 例题 ②「矩阵转置」—— 数组与 vector 组
//
// 二维 vector 从里往外读:vector<int>(C) 是「一行,C 个格子」,
// 外面那层是「R 个这样的行」。⇒ 第一个数永远是**行数**。
// ⚠ 行列写反了程序照样能跑,只是答案错 —— 而小数据常常正好是方阵,看不出来。
#include <bits/stdc++.h>
using namespace std;
int main() {
int R, C;
cin >> R >> C;
vector<vector<int>> a(R, vector<int>(C)); // R 行 C 列,全 0
for (int i = 0; i < R; i++)
for (int j = 0; j < C; j++) cin >> a[i][j];
// 转置之后是 C 行 R 列 —— 新表的两个维度正好换过来
vector<vector<int>> b(C, vector<int>(R));
for (int i = 0; i < R; i++)
for (int j = 0; j < C; j++) b[j][i] = a[i][j];
for (int i = 0; i < C; i++)
for (int j = 0; j < R; j++)
cout << b[i][j] << (j + 1 == R ? '\n' : ' ');
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
这段代码是怎么想出来的
  1. 里往外读这一句:vector<int>(C) 是「一行,C 个格子」;外面那层 vector<vector<int>> a(R, …) 是「R 个这样的行」。⇒ 第一个数永远是行数。 取元素照样写 a[i][j]
  2. ⚠ 行列写反了程序照样跑,只是答案错 —— 而且小数据常常正好是方阵,一测就过。这是本书专门记下来的一个坑。
  3. 转置之后的表要开成 vector<vector<int>> b(C, vector<int>(R)) ——两个维度跟着换过来。这一步写错就会越界。
  4. 搬数据只有一句:b[j][i] = a[i][j]; —— 把定义原样翻译过来就行。
记住这一句

二维 vector 是第 21 章之后每一章的主角(DP 的表格、网格图、邻接矩阵全是它)。⇒ 这一页要留下的肌肉记忆只有一条:第一个数是行数。

用到的写法:vector<vector<int>> f(R, vector<int>(C, 0)) vector<int> a(n)

练习(3 道)

答案折叠着,先自己写一版。

练习 ① 每个数出现了几次

vecTry1.cpp

n 个整数(都在 0 ~ 100 之间),统计每个值各出现了多少次,按值从小到大输出所有出现过的值及其次数。

输入格式第一行一个整数 n1 ≤ n ≤ 10⁵);第二行 n 个整数 aᵢ0 ≤ aᵢ ≤ 100)。
输出格式若干行,每行两个整数:值、出现次数。没出现过的值不要输出。

输入

6
3 1 3 2 1 3

输出

1 2
2 1
3 3
1 出现 2 次、2 出现 1 次、3 出现 3 次。
提示

值域只有 0~100 ⇒ 可以开一个数组,用值本身当下标。⚠ 那个数组要开多长?(想清楚 a 能取到的最大值。)

vecTry1.cpp参考答案
记住这一句

这一招叫「桶」,贯穿全书:第 6 章的计数、第 47 章的 cnt[26]、第 38 章离散化的前身都是它。⇒ 而它的前提是值域不能太大 —— 值域到 10⁹ 时开不出这个数组,那时候才轮到 map

用到的写法:vector<int> a(n, 7) vector<int> a(n)

练习 ② 读到 0 为止

vecTry2.cpp

不断读入整数,直到读到一个 0 为止(这个 0 不算在内)。请输出一共读到了几个数,以及最后一个数是多少。

⚠ 如果一个数都没读到(第一个就是 0),只输出一行 0

输入格式若干个整数,以 0 结尾。
输出格式第一行一个整数,表示个数;如果个数不为 0,第二行输出最后一个数。

输入

4 8 15 0

输出

3
15
读到 4 8 15 三个数就遇到了 0,最后一个是 15。
提示

事先不知道有多少个数 ⇒ 开不出固定长度的数组。vector 比普通数组多出来的第二件本事正好派上用场。

vecTry2.cpp参考答案

用到的写法:a.size() / a.push_back(x) / a.back() vector<int> a(n)

练习 ③ 行和与列和

vecTry3.cpp

给一个 RC 列的矩阵,输出每一行的和,再输出每一列的和。

输入格式第一行两个整数 RC1 ≤ R, C ≤ 500);接下来 R 行,每行 C 个整数(|aᵢⱼ| ≤ 10⁹)。
输出格式第一行 R 个整数(各行的和);第二行 C 个整数(各列的和)。均用空格隔开,行末不留空格。

输入

2 3
1 2 3
4 5 6

输出

6 15
5 7 9
两行的和是 6 和 15;三列的和是 5、7、9。
提示

不用先把整个矩阵存下来 —— 边读边累加就行。⚠ 两个累加器数组的长度不一样,写反了就是越界。

vecTry3.cpp参考答案
记住这一句

⚠ 还有一件事:500 × 500 个 10⁹ 加起来是 2.5 × 10¹⁴ ⇒ 累加器得是 long long

用到的写法:vector<int> a(n, 7) vector<vector<int>> f(R, vector<int>(C, 0))