在算法面试和日常练习中,矩阵(二维数组)类的题目出现频率非常高。这类题目通常不涉及特别复杂的数据结构,考察的重点往往是空间优化技巧 和逻辑模拟能力。
今天我们通过两道经典题目------"矩阵置零"和"螺旋矩阵",来深入探讨一下如何优雅地处理矩阵变换与遍历。
第一题:矩阵置零 (Set Matrix Zeroes)
题目分析
题目的要求很简单:给定一个 m x n 的矩阵,如果一个元素为 0,则将其所在行和列的所有元素都设为 0。
常见的痛点
最直观的做法是:新建一个同样的矩阵,或者用两个数组分别记录哪一行、哪一列需要置零。但这会占用额外的空间(O(mn) 或 O(m+n))。如果题目要求我们使用 O(1) 的额外空间,即"原地"修改,该怎么办呢?
优化的解题思路
我们可以利用矩阵本身的第一行 和第一列作为"记事本",用来标记剩余部分的行或列是否需要置零。
核心步骤:
-
预判首行首列 :首先检查第一行和第一列本身是否包含 0,用两个布尔变量
rowZero和colZero记录下来。这是因为我们后面要征用这两块地盘做标记,得先记住它们"原本"的状态。 -
利用首行首列做标记 :遍历剩余的矩阵(从
i=1, j=1开始)。如果发现matrix[i][j] == 0,就在它对应的行头matrix[i][0]和列头matrix[0][j]填入 0 做记号。 -
根据标记置换:再次遍历矩阵(不含首行首列),只要发现行头或列头是 0,就把当前元素置为 0。
-
处理首行首列:最后,根据第 1 步记录的布尔变量,决定是否把第一行和第一列全部刷成 0。
代码实现
C++代码实现:
cpp
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
bool rowZero = false, colZero = false;
// 1. 遍历并标记
// 判断第一行和第一列本身是否有0,并利用它们标记内部的0
for (int i = 0; i < matrix.size(); ++i) {
for (int j = 0; j < matrix[0].size(); ++j) {
if (matrix[i][j] == 0) {
matrix[0][j] = matrix[i][0] = 0; // 在首行首列做记号
if (i == 0) rowZero = true; // 记录第一行原本状态
if (j == 0) colZero = true; // 记录第一列原本状态
}
}
}
// 2. 根据标记,处理内部矩阵(从下标1开始,保护标记位)
for (int i = 1; i < matrix.size(); ++i) {
for (int j = 1; j < matrix[0].size(); ++j) {
if (matrix[i][0] == 0 || matrix[0][j] == 0) {
matrix[i][j] = 0;
}
}
}
// 3. 最后处理第一行和第一列
for (int j = 0; rowZero && j < matrix[0].size(); ++j) {
matrix[0][j] = 0;
}
for (int i = 0; colZero && i < matrix.size(); ++i) {
matrix[i][0] = 0;
}
}
};
第二题:螺旋矩阵 (Spiral Matrix)
题目分析
这就好比玩"贪吃蛇",要求我们按照右 -> 下 -> 左 -> 上的顺时针顺序,从外向内螺旋式地打印出矩阵的所有元素。
解题思路:模拟法
这道题不需要复杂的算法,核心在于模拟行走的路径。
-
方向控制 :我们可以定义两个数组
dx和dy来表示移动的方向。-
dx代表行的变化(垂直移动),dy代表列的变化(水平移动)。 -
顺序是:右
{0, 1}, 下{1, 0}, 左{0, -1}, 上{-1, 0}。
-
-
边界判断 :我们需要一个"试探"机制。在真正移动之前,先计算下一步的坐标
x和y。-
如果下一步越界(比如撞墙了)。
-
或者下一步走到了已访问过的格子(这就需要我们标记走过的路)。
-
只要满足上述任一条件,就通过
(di + 1) % 4来切换下一个方向。
-
-
防止走回头路 :为了节省空间,我们可以在遍历过的格子上直接赋值为一个特殊值(例如
INT_MAX),这样就不用开辟额外的visited数组了。
代码实现
C++代码实现:
cpp
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
// 定义四个方向:右、下、左、上
// dx控制行(i)的变化,dy控制列(j)的变化
vector<int> dx = {0, 1, 0, -1};
vector<int> dy = {1, 0, -1, 0};
vector<int> ans;
int di = 0; // 当前方向的索引
int i = 0; // 当前行坐标
int j = 0; // 当前列坐标
// 矩阵总元素个数即为循环次数
for (int k = 0; k < matrix.size() * matrix[0].size(); ++k) {
ans.push_back(matrix[i][j]);
matrix[i][j] = INT_MAX; // 标记已访问,避免走回头路
// 试探下一步的位置
int x = i + dx[di];
int y = j + dy[di];
// 如果越界 或者 遇到了已访问过的格子(INT_MAX)
if (x < 0 || x >= matrix.size() ||
y < 0 || y >= matrix[0].size() ||
matrix[x][y] == INT_MAX) {
// 顺时针换方向
di = (di + 1) % 4;
}
// 正式移动到下一个位置
i = i + dx[di];
j = j + dy[di];
}
return ans;
}
};
总结
这两道题目展示了处理矩阵问题的两个重要技巧:
-
原地标记:在不破坏逻辑的前提下,利用输入数据本身的结构(如首行首列)来存储状态,从而节省空间。
-
方向数组 :在处理由于路径移动(如旋转、迷宫、搜索)的问题时,使用
dx/dy数组配合取模运算(di + 1) % 4,可以让代码逻辑比写一大堆if-else清晰得多。