文章目录
一、题目概述
给定一个 m x n 的二维矩阵,如果某个元素为 0,则将其所在行与列的所有元素都设为 0。你需要 原地修改 输入矩阵(即不使用额外的矩阵存储结果)。
示例
输入:
[
[1,1,1],
[1,0,1],
[1,1,1]
]
输出:
[
[1,0,1],
[0,0,0],
[1,0,1]
]
说明:第 2 行第 2 列的元素为 0,因此第 2 行和第 2 列都需要置零。
二、思维导图
下面的思维导图展示了问题的整体思路,从暴力法到最优解的演进路线:
Set Matrix Zeroes
子问题分析
如果某元素为0 → 整行整列置0
解法演进
暴力解法
使用辅助数组
使用标记集合
原地标记法(最优)
复杂度
m*n
空间复杂度: 不同方法从 O(m+n) 降到 O(1)
三、解法一:暴力法(不推荐)
原理说明
当我们发现某个元素为 0 时,立即将其所在行和列全部设为 0。但是这样会有一个问题 ------ 更改后的 0 会影响后续判断。
改进思路
考虑用另一个矩阵来保存哪些位置需要被置 0,最后再统一修改。
实现步骤
- 遍历输入矩阵,记录所有为
0的位置(i, j); - 再次遍历矩阵,将这些位置对应的行列全部置零。
时间与空间复杂度
- 时间复杂度:
O(m * n * (m + n)) - 空间复杂度:
O(1)(若直接修改),但逻辑会错误;若使用记录列表,约为O(m * n)
四、解法二:使用辅助数组标记行列
原理说明
可以利用两个数组:
row[m]:标记每一行是否包含零;col[n]:标记每一列是否包含零。
步骤流程
是
否
是
否
开始
初始化 row[], col[]
第一次遍历矩阵
元素为0?
标记行与列
继续遍历
第二次遍历矩阵
行或列标记为真?
设0
保持原值
结束
时间与空间复杂度
- 时间复杂度:
O(m * n) - 空间复杂度:
O(m + n)
Java代码实现
java
public void setZeroes(int[][] matrix) {
int m = matrix.length;
int n = matrix[0].length;
boolean[] row = new boolean[m];
boolean[] col = new boolean[n];
// 记录哪些行和列需要置0
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == 0) {
row[i] = true;
col[j] = true;
}
}
}
// 根据标记置0
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (row[i] || col[j]) {
matrix[i][j] = 0;
}
}
}
}
五、解法三:原地标记(最优方法)
核心思想
不使用额外空间,而直接利用矩阵的 第一行与第一列 作为标记数组的替代。
步骤解析
- 首先记录第一行和第一列是否本身含
0; - 从第二行第二列开始,如果某个元素为
0,就在所在的第一行和第一列位置标记为0; - 再次遍历矩阵,当某行或某列的标记位为
0时,就将该元素置零; - 最后根据第一行和第一列的记录情况处理它们。
算法流程图
是
否
初始化
记录第一行是否有0
记录第一列是否有0
遍历剩余元素
元素为0?
标记第一行与第一列
继续遍历
根据标记置0
处理第一行列
结束
时间与空间复杂度
- 时间复杂度:
O(m * n) - 空间复杂度:
O(1)
Java代码实现
java
public void setZeroes(int[][] matrix) {
int m = matrix.length;
int n = matrix[0].length;
boolean firstRowZero = false, firstColZero = false;
// 检查第一行
for (int j = 0; j < n; j++) {
if (matrix[0][j] == 0) {
firstRowZero = true;
break;
}
}
// 检查第一列
for (int i = 0; i < m; i++) {
if (matrix[i][0] == 0) {
firstColZero = true;
break;
}
}
// 使用第一行和第一列标记零
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (matrix[i][j] == 0) {
matrix[i][0] = 0;
matrix[0][j] = 0;
}
}
}
// 根据标记置零
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (matrix[i][0] == 0 || matrix[0][j] == 0) {
matrix[i][j] = 0;
}
}
}
// 处理第一行
if (firstRowZero) {
for (int j = 0; j < n; j++) matrix[0][j] = 0;
}
// 处理第一列
if (firstColZero) {
for (int i = 0; i < m; i++) matrix[i][0] = 0;
}
}
六、各方法性能对比表
| 方法 | 时间复杂度 | 空间复杂度 | 是否原地修改 |
|---|---|---|---|
| 暴力法 | O(mn(m+n)) | 高 | 否 |
| 辅助数组 | O(m*n) | O(m+n) | 否 |
| 原地标记 | O(m*n) | O(1) | 是 |
七、总结
- 暴力法容易想到,但效率低;
- 辅助数组平衡了性能与可读性,是较常见的做法;
- 原地标记法是最优解:在不增加额外空间的条件下,完成高效的矩阵置零。
这道题体现了从"额外空间辅助"到"原地替代标记"的优化思维,也是很多二维数组操作题目的共性思路。