在 C++ 中,平时处理单排数据时,我们使用一维 vector 就足够了。如果要保存类似课程表、棋盘这样的多行多列数据,我们可以把容器里的每个元素也换成一维 vector,写成 std::vector<std::vector<数据类型>>,这就是 “二维 vector”。
与行列长度固定的二维数组不同,你可以把二维 vector 看成一张行列都能自由伸缩的动态表格,行数和列数都可以在运行期间动态决定。
创建与初始化二维 vector
如果我们只想先创建一个空的二维 vector,直接将两层 std::vector 嵌套写成 std::vector<std::vector<int>> grid; 即可。
不过在实际开发中,我们通常会在创建的同时直接初始化,最常用的有两种方式:一种是 “使用花括号 {} 初始化”,另一种是 “指定行数和列数初始化”。
1. 使用花括号 {} 初始化
如果我们一开始就已经知道表格里每一行具体有哪些数据,可以使用两层花括号 {} 直接列出所有元素。
语法:
std::vector<std::vector<数据类型>> 容器名 = {
{第1行元素1, 第1行元素2, ...},
{第2行元素1, 第2行元素2, ...}
};说明:
外层花括号表示整个二维表格,内层的每一对花括号分别对应表格中的 “一行”。行与行之间、同一行的各个元素之间,都使用英文逗号(,)隔开。
示例 1:使用花括号初始化
#include <iostream>
#include <vector>
int main() {
std::vector<std::vector<int>> grid = {
{10, 20, 30},
{40, 50, 60}
};
std::cout << grid[0][0] << '\n'; // 第 1 行第 1 列
std::cout << grid[1][2] << '\n'; // 第 2 行第 3 列
return 0;
}运行结果如下。
10
60分析:
在这个例子中,grid 包含 2 个内层花括号,代表一共有 2 行。每一行里面又有 3 个整数,代表有 3 列。
按行换行书写后,代码外观就和真实的二维表格一样直观。其中,grid[0][0] 输出第 1 行第 1 列的 10,grid[1][2] 输出第 2 行第 3 列的 60。
2. 指定行数和列数初始化
很多时候,我们在创建二维 vector 时还不知道每个格子里具体放什么数,只希望先建好一个 “m 行 n 列” 的表格,并把所有格子统一设为同一个初始值。这时我们可以结合前面在 “C++ vector 初始化” 一节中学过的圆括号写法来实现。
语法:
std::vector<std::vector<数据类型>> 容器名(m, std::vector<数据类型>(n));
std::vector<std::vector<数据类型>> 容器名(m, std::vector<数据类型>(n, 初始值));说明:
第一次看到这行代码时,小伙伴们可能会觉得括号有点多。其实只要对照一维容器的 容器名(元素数量, 初始值) 语法,分两步拆开看就非常清楚了:
- 外层第 1 个参数 m:表示外层
vector一共有m个元素,也就是表格一共有m行。 - 外层第 2 个参数:写成
std::vector<数据类型>(n, 初始值),表示把每一行都初始化为一个 “包含n个元素的一维容器”。如果不传内层的第 2 个参数,数值元素默认初始化为0。
示例 2:按指定行列数初始化
#include <iostream>
#include <vector>
int main() {
int m = 2;
int n = 4;
// 创建 2 行 4 列且初值全为 1 的二维 vector
std::vector<std::vector<int>> v(m, std::vector<int>(n, 1));
std::cout << "行数:" << v.size() << '\n';
std::cout << "列数:" << v[0].size() << '\n';
std::cout << v[0][0] << ' ' << v[1][3] << '\n';
return 0;
}运行结果如下。
行数:2
列数:4
1 1分析:
在这个例子中,我们直接创建出了一个 2 行 4 列、所有元素初值均为 1 的二维表格。对于这个例子来说,上面那句嵌套初始化代码其实等价于下面两步。
std::vector<int> row(n, 1); // 先准备 1 行数据
std::vector<std::vector<int>> v(m, row); // 再复制成 m 行把第 1 行的 std::vector<int>(n, 1) 直接代入第 2 行的 row 位置,就是示例里写的合并形式。而且由于 m 和 n 都是普通变量,二维 vector 的行数和列数完全可以在运行期间根据输入动态决定。
访问元素与获取行列数
创建好二维 vector 之后,我们可以按 “行下标” 和 “列下标” 来读写任意位置的元素,也可以随时查询当前表格有多少行、每一行有多少列。
1. 访问和修改元素
与普通二维数组一样,访问二维 vector 的元素需要连续使用两个中括号 [][],或者连续调用两次 at()。
语法:
容器名[行下标][列下标]
容器名.at(行下标).at(列下标)说明:
无论是行下标还是列下标,都是从 0 开始计算的:第 1 行的行下标是 0,第 1 列的列下标也是 0。其中,[][] 写法最简洁,而 at() 会自动检查行列下标是否越界。
示例 3:读写二维 vector 元素
#include <iostream>
#include <vector>
int main() {
std::vector<std::vector<int>> grid = {
{10, 20, 30},
{40, 50, 60}
};
std::cout << "修改前:" << grid[0][2] << '\n';
grid[1][1] = 99; // 修改第 2 行第 2 列元素
std::cout << "修改后:" << grid.at(1).at(1) << '\n';
return 0;
}运行结果如下。
修改前:30
修改后:99分析:
grid[0][2] 会先通过 grid[0] 找到第 1 行 {10, 20, 30},再从这一行中取出下标为 2 的元素 30。同理,grid[1][1] = 99; 会定位到第 2 行第 2 列的位置,把原来的 50 修改为 99。
2. 获取行数与列数
既然二维 vector 是由外层容器(保存行)和内层容器(保存列)组成的,获取行数和列数只需要分别调用外层和内层的 size() 函数即可。
语法:
容器名.size()
容器名[i].size()说明:
外层 容器名.size() 返回里面一共有多少行。而 容器名[i] 本身就是一个保存第 i 行数据的独立一维 vector,因此不同行的元素个数可以相同,也可以不同。
对某一行调用 容器名[i].size(),就能得到该行实际有多少列。如果表格每一行的列数都相同,直接写 容器名[0].size() 即可代表总列数。
示例 4:获取行数与各行列数
#include <iostream>
#include <vector>
int main() {
std::vector<std::vector<int>> grid = {
{10, 20},
{30, 40, 50, 60}
};
std::cout << "总行数:" << grid.size() << '\n';
std::cout << "第 1 行列数:" << grid[0].size() << '\n';
std::cout << "第 2 行列数:" << grid[1].size() << '\n';
return 0;
}运行结果如下。
总行数:2
第 1 行列数:2
第 2 行列数:4分析:
grid.size() 返回 2,表示一共有 2 行。由于每一行都是独立的一维容器,第 1 行包含 2 个整数,所以 grid[0].size() 返回 2。第 2 行包含 4 个整数,所以 grid[1].size() 返回 4。
注意 调用 grid[0].size() 获取列数的前提是二维 vector 至少包含一行,我们可以先通过 !grid.empty() 判断容器是否非空。如果二维容器本身是空的,里面连第 1 行 grid[0] 都不存在,直接调用 grid[0].size() 会导致下标越界。
遍历二维 vector
遍历二维 vector 需要使用 “双层循环”:外层循环负责一行一行往下走,内层循环负责把当前这一行里的每个元素从左到右访问一遍。
1. 使用双层下标 for 循环遍历
如果在遍历时需要用到每个元素的行下标 i 和列下标 j,我们可以使用双层下标 for 循环。
语法:
for (int i = 0; i < 容器名.size(); i++) {
for (int j = 0; j < 容器名[i].size(); j++) {
// 循环体
}
}说明:
外层循环条件 i < 容器名.size() 控制行下标变化,内层循环条件 j < 容器名[i].size() 控制当前行的列下标变化,我们在循环体内通过 容器名[i][j] 即可读写对应位置的元素。
示例 5:双层下标循环遍历
#include <iostream>
#include <vector>
int main() {
std::vector<std::vector<int>> grid = {
{10, 20, 30},
{40, 50, 60}
};
for (int i = 0; i < grid.size(); i++) {
for (int j = 0; j < grid[i].size(); j++) {
std::cout << grid[i][j] << ' ';
}
std::cout << '\n';
}
return 0;
}运行结果如下。
10 20 30
40 50 60分析:
在这个例子中,外层循环依次遍历第 0 行和第 1 行,内层循环把当前行的 3 个整数依次打印出来。每当内层循环把一整行打印完后,执行一句 std::cout << '\n'; 换行,再继续打印下一行。
这里小伙伴们要注意一个细节:内层循环的边界条件推荐写成 j < grid[i].size(),而不是固定写成 grid[0].size()。因为二维 vector 每一行的列数可能不同,写成 grid[i].size() 能准确获取当前第 i 行的实际长度,避免发生越界。
2. 使用双层范围 for 循环遍历(推荐)
如果我们只是想按顺序把所有元素读取出来,不需要关心行列下标,改用双层范围 for 循环会让代码简洁很多。
语法:
for (const auto& row : 容器名) {
for (auto val : row) {
// 循环体
}
}说明:
外层循环中的 row 代表每次取出来的 “一整行”,它的类型是一维 vector。内层循环再从 row 中依次取出每一个具体元素 val。
和一维容器的循环遍历一样,外层加上 const auto& 可以直接读取原行数据,省去每次把整行容器重新复制一遍的开销。
示例 6:双层范围 for 循环遍历
#include <iostream>
#include <vector>
int main() {
std::vector<std::vector<int>> grid = {
{10, 20, 30},
{40, 50, 60}
};
for (const auto& row : grid) {
for (auto val : row) {
std::cout << val << ' ';
}
std::cout << '\n';
}
return 0;
}运行结果如下。
10 20 30
40 50 60分析:
for (const auto& row : grid) 省去了我们手动维护 i、j 下标的麻烦,外层每次拿到一行 row,内层直接把这一行里的整数依次打印出来。
如果小伙伴们希望在双层范围 for 循环中直接修改原表格里的元素,只需去掉外层的 const,给内外两层循环变量都加上 &,比如写成下面这样:
for (auto& row : grid) {
for (auto& val : row) {
val *= 2; // 将每个元素乘以 2
}
}动态添加新行与元素
二维 vector 相比普通二维数组最大的优势,就在于创建好之后,我们依然可以随时调用 push_back() 动态添加数据:既可以往表格末尾追加新的一行,也可以单独往某一行的末尾追加新元素。
示例 7:动态添加新行与元素
#include <iostream>
#include <vector>
int main() {
std::vector<std::vector<int>> grid = {
{10, 20, 30},
{40, 50, 60}
};
grid.push_back({70, 80, 90}); // 末尾追加新的一行
grid[0].push_back(35); // 向第 1 行末尾追加 35
for (const auto& row : grid) {
for (auto val : row) {
std::cout << val << ' ';
}
std::cout << '\n';
}
return 0;
}运行结果如下。
10 20 30 35
40 50 60
70 80 90分析:
在这个例子中,grid.push_back({70, 80, 90}); 直接把包含 3 个整数的一维列表作为第 3 行追加到了表格末尾。
接着执行 grid[0].push_back(35); 时,因为 grid[0] 本身就是第 1 行的那个一维 vector,对它调用 push_back(35) 就会在第 1 行末尾单独增加一个 35。
从运行结果可以看出,此时第 1 行变成了 4 个元素,而后两行保持 3 个元素,在实际开发中非常灵活。
