C++ 二维 vector

在 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 个元素,在实际开发中非常灵活。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号