在C++编程中,当我们需要处理二维数据结构,例如矩阵、表格或网格时,std::vector 是一个非常灵活且强大的工具。而对于二维场景,我们常常会用到“二维vector”的概念。它并非一个独立的C++标准库类型,而是通过 std::vector 嵌套 std::vector 来实现的,即 std::vector<std::vector<T>>,其中 T 可以是任何数据类型(如 int, double, string 等)。

是什么:二维Vector的本质

从概念上讲,二维vector可以被视为一个由行和列组成的表格或矩阵。每一“行”本身是一个vector,而整个二维vector则是一个包含这些“行”vector的vector。

**在C++中,std::vector<std::vector<T>> 实际上是:**

  • 一个外层 std::vector,它的每个元素都是一个内层 std::vector<T>
  • 外层vector的大小代表“行”的数量。
  • 内层vector的大小代表对应行的“列”的数量。

需要注意的是,这种结构与传统的C风格二维数组(如 int arr[3][4];)在内存布局上有所不同。C风格二维数组通常在内存中是连续存储的,而二维vector虽然每个内层vector的元素是连续存储的,但不同的内层vector(不同的行)在内存中不一定相互紧挨着。它们可能分散在堆内存的不同位置,外层vector只存储指向这些内层vector起始位置的指针或相关管理信息。

为什么使用二维Vector?

相比于传统的C风格二维数组或手动管理的动态二维数组(如使用 new T*[rows]),使用 std::vector<std::vector<T>> 有着显著的优势:

  • 动态大小: 这是vector最大的优势。你无需在编译时确定二维vector的行数和列数。可以在程序运行时根据需要添加或删除行,甚至每行的列数都可以不同(创建“锯齿状”或“不规则”的二维结构)。
  • 自动内存管理: std::vector 会自动处理内存的分配和释放。当你创建一个二维vector时,它会自动分配所需的内存;当它超出作用域或被销毁时,内存也会被自动回收,有效避免了内存泄漏的问题。这比手动使用 newdelete 来管理二维数组要安全得多。
  • 方便的操作: vector提供了丰富的成员函数,如 push_back()(在末尾添加元素或行)、pop_back()(移除末尾元素或行)、size()(获取大小/行数或列数)、empty()(检查是否为空)、clear()(清空所有元素或行)等,使得对二维数据的操作更加直观和便捷。
  • 与标准库算法兼容: 因为内层和外层都是vector,你可以方便地将它们与C++标准库中的算法(如排序、查找等)结合使用。

虽然二维vector在内存布局上可能不如连续的二维数组紧凑,这在某些高性能计算场景下可能略有开销,但对于大多数应用来说,其带来的动态性、安全性和便利性是更重要的考量。

如何创建和初始化二维Vector?

创建和初始化二维vector有多种方式,取决于你的需求。

创建空的二维Vector

这只会创建一个外层vector,其中不包含任何内层vector(即没有行)。

std::vector<std::vector<int>> empty_matrix;

此时 empty_matrix.size() 为 0。

创建指定行数但每行为空的二维Vector

创建外层vector,指定其大小(行数),但内层vector(列)是空的。

int num_rows = 5;

std::vector<std::vector<int>> matrix_with_rows(num_rows);

此时 matrix_with_rows.size() 为 5,但 matrix_with_rows[0].size() 为 0。

创建指定行数和列数,并用默认值初始化

创建一个 M 行 N 列的二维vector,所有元素会被初始化为对应类型的默认值(例如,intdouble 默认为 0,bool 默认为 false,对象类型调用默认构造函数)。

int num_rows = 3;

int num_cols = 4;

std::vector<std::vector<int>> zero_matrix(num_rows, std::vector<int>(num_cols));

这里,外层vector被初始化为包含 num_rows 个元素,每个元素都是一个大小为 num_colsstd::vector<int>

创建指定行数和列数,并用特定值初始化

创建一个 M 行 N 列的二维vector,所有元素都被初始化为指定的值。

int num_rows = 2;

int num_cols = 5;

int initial_value = 99;

std::vector<std::vector<int>> filled_matrix(num_rows, std::vector<int>(num_cols, initial_value));

此时,filled_matrix 是一个 2×5 的矩阵,所有元素都是 99。

使用初始化列表初始化

对于已知固定内容的二维数据,可以使用初始化列表。

std::vector<std::vector<int>> init_matrix = {

{1, 2, 3},

{4, 5, 6},

{7, 8, 9}

};

注意,使用初始化列表可以很容易创建“锯齿状”vector:

std::vector<std::vector<int>> ragged_matrix = {

{1, 2},

{3, 4, 5, 6},

{7}

};

运行时构建二维Vector(逐行或逐元素添加)

可以先创建一个空的二维vector,然后动态地添加行。

std::vector<std::vector<int>> dynamic_matrix;



// 添加第一行

dynamic_matrix.push_back({10, 20, 30});



// 添加第二行,并逐个添加元素

std::vector<int> second_row;

second_row.push_back(40);

second_row.push_back(50);

dynamic_matrix.push_back(second_row);



// 或者直接在现有行中添加元素(如果行已存在且大小合适)

// dynamic_matrix[0].push_back(35); // 如果第一行不是用初始化列表创建的,可能需要确保它不是空的或者已经有足够的容量。如果第一行是空的,直接push_back可行。

如何访问和操作二维Vector?

访问和操作二维vector的元素类似于操作普通vector,但需要使用两个索引:一个用于选择行,一个用于选择列。

访问元素

使用下标运算符 [] 或成员函数 .at()

std::vector<std::vector<int>> matrix = {{1, 2}, {3, 4}};



// 使用 [] 访问 (不执行边界检查)

int element1 = matrix[0][1]; // 获取第一行(索引0)的第二个元素(索引1),即 2



// 使用 .at() 访问 (执行边界检查,越界抛出 std::out_of_range 异常)

int element2 = matrix.at(1).at(0); // 获取第二行(索引1)的第一个元素(索引0),即 3



// 修改元素

matrix[0][1] = 99; // 现在 matrix 是 {{1, 99}, {3, 4}}

推荐在不确定索引是否有效时使用 .at() 以增加程序的健壮性。

获取维度(行数和列数)

std::vector<std::vector<int>> matrix = {{1, 2, 3}, {4, 5, 6}};



size_t num_rows = matrix.size(); // 获取行数 (这里是 2)



// 获取列数。注意:对于非锯齿状vector,所有行的列数相同。

// 需要先确保 matrix 不为空,否则 matrix[0] 是无效的。

size_t num_cols = (num_rows > 0) ? matrix[0].size() : 0; // 获取第一行的列数 (这里是 3)

遍历二维Vector

通常使用嵌套循环来遍历所有元素。

使用索引遍历

std::vector<std::vector<int>> matrix = {{1, 2}, {3, 4}, {5, 6}};



for (size_t i = 0; i < matrix.size(); ++i) { // 外层循环遍历行

for (size_t j = 0; j < matrix[i].size(); ++j) { // 内层循环遍历列

// 访问 matrix[i][j]

// std::cout << matrix[i][j] << " ";

}

// std::cout << std::endl;

}

使用范围-based for 循环遍历

这是C++11及更高版本推荐的方式,代码更简洁。

std::vector<std::vector<int>> matrix = {{1, 2}, {3, 4}, {5, 6}};



for (const auto& row : matrix) { // 遍历每一行(row 是一个 vector<int>)

for (int element : row) { // 遍历当前行的每个元素

// 访问 element

// std::cout << element << " ";

}

// std::cout << std::endl;

}

使用 const auto& 可以避免不必要的拷贝,提高效率。

添加/删除行或元素

使用vector的成员函数进行操作:

  • **添加行:** push_back() 将一个vector添加到二维vector的末尾。

    std::vector<std::vector<int>> matrix = {{1, 2}};

    std::vector<int> new_row = {3, 4, 5};

    matrix.push_back(new_row); // matrix 现在是 {{1, 2}, {3, 4, 5}}

  • **删除末尾行:** pop_back() 移除二维vector的最后一个元素(即最后一行)。

    std::vector<std::vector<int>> matrix = {{1, 2}, {3, 4}};

    matrix.pop_back(); // matrix 现在是 {{1, 2}}

  • **在特定行添加元素:** 在内层vector上使用 push_back()

    std::vector<std::vector<int>> matrix = {{1, 2}};

    matrix[0].push_back(3); // matrix 现在是 {{1, 2, 3}}

  • **删除特定行:** 使用外层vector的 erase() 函数,结合迭代器。

    std::vector<std::vector<int>> matrix = {{1, 2}, {3, 4}, {5, 6}};

    // 删除索引为 1 的行 (第二行)

    matrix.erase(matrix.begin() + 1); // matrix 现在是 {{1, 2}, {5, 6}}

  • **删除特定行内的元素:** 使用内层vector的 erase() 函数。

    std::vector<std::vector<int>> matrix = {{1, 2, 3}, {4, 5}};

    // 删除第一行(索引 0)的第二个元素(索引 1)

    matrix[0].erase(matrix[0].begin() + 1); // matrix 现在是 {{1, 3}, {4, 5}}

调整大小 (Resizing)

可以使用 resize() 成员函数来改变二维vector的行数。如果新指定的行数大于当前行数,新添加的行将是默认初始化的空vector。如果小于当前行数,多余的行会被删除。

std::vector<std::vector<int>> matrix = {{1, 2}, {3, 4}}; // 2x2



matrix.resize(3); // 现在 matrix 有 3 行,第三行是空的: {{1, 2}, {3, 4}, {}}



// 也可以 resize 并指定新添加的内层vector的大小和初始化值

matrix.resize(4, std::vector<int>(2, 10)); // 现在 matrix 有 4 行,最后一行是 {10, 10}

// matrix 现在是 {{1, 2}, {3, 4}, {}, {10, 10}}

要调整列数,需要遍历每一行并单独 resize。对于非锯齿状vector,可以这样做:

std::vector<std::vector<int>> matrix = {{1, 2, 3}, {4, 5, 6}}; // 2x3

int new_cols = 2;

for (auto& row : matrix) {

row.resize(new_cols);

}

// matrix 现在是 {{1, 2}, {4, 5}}

内存开销有多少?

理解二维vector的内存布局有助于估算其开销。

一个 std::vector<std::vector<T>> 的总内存开销大致包括:

  1. **外层vector的开销:** 外层vector自身需要一定的内存来存储其大小、容量以及一个指向其元素(即内层vector)数组的指针。这个开销相对固定,取决于系统架构和库实现,通常是几十个字节。
  2. **内层vector对象的开销:** 每一个内层 std::vector<T> 对象也需要一定的内存来存储其大小、容量以及一个指向其实际数据(T 类型元素)数组的指针。如果二维vector有 M 行,就会有 M 个内层vector对象,总开销约为 M * sizeof(内层vector对象)
  3. **实际数据元素的开销:** 这是主要的内存消耗部分。所有内层vector中存储的 T 类型元素的总大小。如果是一个 M 行 N 列的非锯齿状二维vector,元素总数为 M * N。总开销约为 M * N * sizeof(T)。对于锯齿状vector,需要累加所有行的元素数量。

总内存开销 ≈ 外层vector开销 + M * 内层vector对象开销 + 所有元素总大小。

与同样大小的C风格二维数组 T arr[M][N] 相比,二维vector通常会有更多的内存开销,主要是因为每个内层vector都有自己的管理开销。但好处是提供了动态性和自动管理。

此外,如前所述,内层vector的数据是连续的,但不同行的数据块可能不连续。这可能影响缓存的局部性,对于需要高度优化内存访问的算法(如大型矩阵运算),C风格数组或特制的内存管理方案可能更优。但在大多数常见应用中,二维vector的性能是完全可以接受的。

典型应用场景在哪里?

二维vector因其灵活性和便利性,在多种编程场景中被广泛应用:

  • 矩阵和线性代数: 表示和操作数学上的矩阵,进行加、减、乘等运算。
  • 图像处理: 存储简单的灰度图像像素数据(每个元素代表一个像素的灰度值),或多通道图像(如果 T 是一个包含多个通道值的结构体或小vector)。
  • 游戏开发: 表示游戏地图、棋盘状态、单元格属性等网格状数据。
  • 动态规划: 构建二维DP表格,存储子问题的解。
  • 图的表示: 使用邻接列表来表示图,其中外层vector的索引代表顶点,内层vector存储与该顶点相邻的所有顶点。例如 std::vector<std::vector<int>> adj_list;
  • 表格数据: 存储简单的二维表格数据,如从文件读取的CSV数据(每行是一个vector<string>)。

  • 迷宫或路径查找算法: 表示迷宫的布局,每个元素代表一个单元格的状态(可通过/不可通过)。

总而言之,任何需要以行和列的形式组织和访问数据的场景,二维vector都是一个自然且强大的选择,尤其是在数据规模不确定或可能发生变化的情况下。


二维vector