LeetCode 73. 矩阵置零
题目描述
✅ 73. 矩阵置零


题意分析
以修改前的矩阵为准:某个元素为 0,就将它所在的整行、整列置为 0。新写入的 0 不能再触发清零,否则影响范围会不断扩大。题目要求原地修改,进阶要求额外空间为常数。
解法:第一行第一列原地标记
核心思路
[!blue]
直接的做法是用两个数组,分别记录哪些行、哪些列原本含有 0,再根据标记清零。要省掉这 $O(m+n)$ 的空间,可以借用矩阵的第一列记录行标记、第一行记录列标记:
matrix[row][0] == 0表示该行需要清零,matrix[0][col] == 0表示该列需要清零。第一行、第一列既是标记区,也有自己是否需要整行、整列清零的状态;共享的
matrix[0][0]不能同时表达这两个独立条件。因此在写入标记前,用firstRowZero、firstColZero分别保存第一行、第一列原本是否含有 0。接着只扫描内部区域。遇到原始的 0,就把它对应的行、列标记置为 0,内部元素保持不动,因此不会把新写入的标记误当作新的零源。边界上原有的 0 本身也是有效标记,不需要清除。扫描完成后,对每个非首行,行标记为 0 当且仅当该行原本含有 0;非首列的列标记同理。
再扫描内部,只要行标记或列标记为 0,就清零当前格。此时判断只读取标记区,修改内部不会污染后面的判断。最后按两个布尔值处理第一行、第一列,避免提前清空标记区而扩大清零范围。
若首行或首列的布尔值为假,不必整行或整列清零,但仍保留其中作为标记写入的 0:这些边界格所在的列或行原本就含有 0,最终也应该为 0。
解题步骤
- 在修改任何元素前,分别扫描第一行、第一列,记录
firstRowZero和firstColZero。- 遍历
row >= 1、col >= 1的内部区域。遇到 0,就令matrix[row][0] = 0、matrix[0][col] = 0。- 再遍历内部区域,行标记或列标记任意一个为 0,就将当前格置为 0。
- 若
firstRowZero为真,将第一行清零;若firstColZero为真,将第一列清零。题目保证矩阵非空。只有一行或一列时,内部遍历自然跳过,原始边界标志仍能正确决定最终结果。
代码实现
class Solution {
public void setZeroes(int[][] matrix) {
int m = matrix.length;
int n = matrix[0].length;
boolean firstRowZero = false;
boolean firstColZero = false;
for (int col = 0; col < n; col++) {
// 先保存首行原始状态,之后它还要被用作列标记。
firstRowZero |= matrix[0][col] == 0;
}
for (int row = 0; row < m; row++) {
firstColZero |= matrix[row][0] == 0;
}
for (int row = 1; row < m; row++) {
for (int col = 1; col < n; col++) {
if (matrix[row][col] == 0) {
matrix[row][0] = 0;
matrix[0][col] = 0;
}
}
}
for (int row = 1; row < m; row++) {
for (int col = 1; col < n; col++) {
// 内部读取已完成的行列标记,首行首列此时还不能清空。
if (matrix[row][0] == 0 || matrix[0][col] == 0) {
matrix[row][col] = 0;
}
}
}
// 内部处理完后,才按最初记录清理首行和首列。
if (firstRowZero) {
for (int col = 0; col < n; col++) {
matrix[0][col] = 0;
}
}
if (firstColZero) {
for (int row = 0; row < m; row++) {
matrix[row][0] = 0;
}
}
}
}
func setZeroes(matrix [][]int) {
m, n := len(matrix), len(matrix[0])
firstRowZero, firstColZero := false, false
for col := 0; col < n; col++ {
// 先保存首行原始状态,之后它还要被用作列标记。
firstRowZero = firstRowZero || matrix[0][col] == 0
}
for row := 0; row < m; row++ {
firstColZero = firstColZero || matrix[row][0] == 0
}
for row := 1; row < m; row++ {
for col := 1; col < n; col++ {
if matrix[row][col] == 0 {
matrix[row][0] = 0
matrix[0][col] = 0
}
}
}
for row := 1; row < m; row++ {
for col := 1; col < n; col++ {
// 内部读取已完成的行列标记,首行首列此时还不能清空。
if matrix[row][0] == 0 || matrix[0][col] == 0 {
matrix[row][col] = 0
}
}
}
// 内部处理完后,才按最初记录清理首行和首列。
if firstRowZero {
for col := 0; col < n; col++ {
matrix[0][col] = 0
}
}
if firstColZero {
for row := 0; row < m; row++ {
matrix[row][0] = 0
}
}
}
复杂度分析
- 时间复杂度:$O(mn)$,每个格子只被常数次访问。
- 空间复杂度:$O(1)$,标记复用第一行、第一列,只额外使用两个布尔变量。
关键点总结
[!green]
- 把行、列标记数组存进第一列、第一行,即可将额外空间降到 $O(1)$。
- 标记阶段只改边界,内部清零阶段只读边界,保证后续判断始终依据原始零的位置。
- 首行、首列的原始状态单独保存,内部处理完后才清理这两个边界。
易错点总结
[!yellow]
- 发现 0 就立刻清空整行整列,会让新写入的 0 成为错误的清零依据。
- 必须先保存首行、首列原始状态;写入标记后再判断,就分不清原始 0 和新标记。
- 读取标记前不能清空第一行或第一列,否则会误判其他行列也需要清零。
- 最后首行、首列的整行清零由两个布尔值决定,不能仅看
matrix[0][0]。m是行数,n是列数;所有行循环与列循环都应使用各自的维度。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 289. 生命游戏 | 中等 | 同样要避免当前写入污染后续读取,可通过标记数组或原地编码区分旧状态与新状态。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!