LeetCode 73. 矩阵置零:标记数组详解
1. 这道题属于什么算法思想
这题主要归类为:
text
矩阵 / 标记数组 / 两阶段处理
核心不是立刻把某一行或某一列改成 0,而是:
text
第一阶段:记录哪些行、哪些列需要置零。
第二阶段:根据记录统一置零。
这样可以避免"后来写进去的 0 被误认为原始 0"而发生错误扩散。
题目进阶要求使用原地算法,但本文主解法采用更直观的:
text
O(m + n) 额外空间标记数组版本。
它能正确通过题目结果校验,时间复杂度同样是 O(m * n);区别只是没有满足进阶的 O(1) 额外空间要求。
2. 这道题到底在问什么
题目给定一个 m x n 矩阵:
text
如果某个元素是 0,就把它所在的整行和整列全部置为 0。
例如:
text
matrix =
[
[1, 1, 1],
[1, 0, 1],
[1, 1, 1]
]
原始 0 位于:
text
第 1 行、第 1 列。
因此答案应为:
text
[
[1, 0, 1],
[0, 0, 0],
[1, 0, 1]
]
3. 为什么不能边扫描边直接置零
最容易想到的做法是:
text
扫描到一个 0,就立刻把对应行和列全部改为 0。
这个做法会出错。
仍然使用刚才的例子:
text
[
[1, 1, 1],
[1, 0, 1],
[1, 1, 1]
]
扫描到中间的原始 0 后,立刻清零第 1 行、第 1 列,矩阵会暂时变成:
text
[
[1, 0, 1],
[0, 0, 0],
[1, 0, 1]
]
但此时:
text
matrix[0][1] 也变成了 0。
它不是原矩阵中的 0,只是我们为了答案新写进去的 0。
如果后续扫描又把它误认为原始 0,就会继续清零第 0 行,错误会扩散。
因此必须做到:
text
先完整读取原始 0 的位置。
再统一修改矩阵。
4. 用 rowZero 和 colZero 分别记录行、列
创建两个布尔数组:
java
boolean[] rowZero = new boolean[m];
boolean[] colZero = new boolean[n];
它们的含义是:
text
rowZero[i] == true:第 i 行原本出现过 0,最终整行需要置零。
colZero[j] == true:第 j 列原本出现过 0,最终整列需要置零。
第一遍扫描矩阵时,发现:
text
matrix[i][j] == 0
只做记录:
java
rowZero[i] = true;
colZero[j] = true;
不要修改 matrix。
这样第一阶段结束后,rowZero 和 colZero 保存的仍然完全是"原始 0"带来的影响。
5. 用示例完整推导
text
matrix =
[
[1, 1, 1],
[1, 0, 1],
[1, 1, 1]
]
矩阵有:
text
m = 3 行
n = 3 列
初始化:
text
rowZero = [false, false, false]
colZero = [false, false, false]
第一阶段扫描时,发现:
text
matrix[1][1] == 0
记录:
text
rowZero[1] = true
colZero[1] = true
扫描结束:
text
rowZero = [false, true, false]
colZero = [false, true, false]
第二阶段重新遍历每个位置。
对于 matrix[i][j],只要:
text
rowZero[i] == true 或 colZero[j] == true
就将它置为 0。
例如:
text
matrix[0][1]:colZero[1] 为 true,所以置 0。
matrix[1][0]:rowZero[1] 为 true,所以置 0。
matrix[2][2]:所在行和所在列都没有标记,保持 1。
最终结果:
text
[
[1, 0, 1],
[0, 0, 0],
[1, 0, 1]
]
6. Java 代码完整注释
java
class Solution {
public void setZeroes(int[][] matrix) {
int m = matrix.length;
int n = matrix[0].length;
// rowZero[i] 为 true,表示第 i 行最终需要全部置为 0。
boolean[] rowZero = new boolean[m];
// colZero[j] 为 true,表示第 j 列最终需要全部置为 0。
boolean[] colZero = new boolean[n];
// 第一阶段:只记录原矩阵中哪些行、列出现过 0。
// 此阶段不修改 matrix,避免新写入的 0 干扰后续判断。
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == 0) {
rowZero[i] = true;
colZero[j] = true;
}
}
}
// 第二阶段:根据第一阶段的行、列标记统一置 0。
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 只要当前元素所在行或所在列原本存在 0,
// 当前元素就需要置为 0。
if (rowZero[i] || colZero[j]) {
matrix[i][j] = 0;
}
}
}
}
}
7. 为什么这版时间是 O(m * n)
矩阵有:
text
m 行,n 列
第一阶段要扫描每个元素一次:
text
O(m * n)
第二阶段也要扫描每个元素一次:
text
O(m * n)
总时间:
text
O(m * n) + O(m * n)
= O(2 * m * n)
= O(m * n)
大 O 忽略常数 2。
8. 为什么额外空间是 O(m + n)
额外创建了:
text
rowZero:长度为 m。
colZero:长度为 n。
因此额外空间是:
text
O(m + n)
它不满足题目进阶要求的常数级额外空间,但这是最直观、最适合先理解的解法。
9. 原地版本只是后续优化
题目的进阶原地版本,会把:
text
rowZero 和 colZero
压缩进矩阵的第一列和第一行中,并额外用两个布尔变量保存第一行、第一列原本是否有 0。
它的本质没有变,仍然是:
text
先标记,再修改。
变化只是:
text
原来用两个额外数组标记。
后来改为复用矩阵第一行、第一列标记。
因此先掌握本篇标记数组版本即可。理解"原地版本"前,首先要把"两阶段处理、防止 0 扩散"这个核心逻辑稳定下来。
10. 总结
这题属于:
text
矩阵 / 标记数组 / 两阶段处理
最核心的原则是:
text
不能边扫描边直接清零。
必须先记录原始 0 影响哪些行、哪些列,再统一修改。
用两个数组表达就是:
text
rowZero[i] 记录第 i 行是否需要清零。
colZero[j] 记录第 j 列是否需要清零。
最后对于每个位置:
text
rowZero[i] || colZero[j] 为 true,就将 matrix[i][j] 置为 0。
可以把整题记成一句话:
text
先标记要清零的行和列,再统一清零,避免新写入的 0 继续扩散。