目录

题目描述

73. 矩阵置零

image-20260329111315822

image-20260329111659642

题意分析

给定一个 $m \times n$ 的矩阵,只要某个位置的元素是 0,就把它所在的整行和整列全部置为 0,并且要求就地修改原矩阵,不返回新矩阵。

关键在于「触发置零的必须是原始的 0」。一旦边扫描边写 0,新写进去的 0 会被后面的扫描当成原始 0,从而引发连锁扩散,最终整个矩阵都变成 0。所以「先把要清的行列全部记下来,再统一执行」是绕不开的两阶段结构。

题目的进阶要求点明了空间预算:用 $O(mn)$ 的额外矩阵是显然做法,用两个长度分别为 $m$ 和 $n$ 的布尔数组是 $O(m + n)$ 的改进做法,而进阶要求的是只用常数额外空间,即 $O(1)$。这句话直接决定了解法形态——标记信息必须存回矩阵自身。

边界情况:矩阵只有一行或只有一列;matrix[0][0] 本身就是 0;整个矩阵没有 0;整个矩阵全是 0。

解法:第一行第一列原地标记

核心思路

难点不是“哪些位置要变成 0”,而是不能让新写入的 0 影响后续判断。因此流程必须分成两阶段:先记录原始 0 对应的行列,再统一置零。

常规做法用两个布尔数组记录行、列,空间为 $O(m+n)$。要做到 $O(1)$,可以直接借用矩阵的第一列记录“这一行是否清零”,借用第一行记录“这一列是否清零”。内部位置 (row, col) 为 0 时,只写入 matrix[row][0]matrix[0][col],暂不修改其他格子。

matrix[0][0] 同时属于第一行和第一列,无法保存两个状态,所以额外用 firstRowZerofirstColZero 记录它们最初是否含 0。内部区域处理完后,再根据这两个变量清理第一行和第一列。

循环不变量是:标记阶段结束后,对任意 row > 0matrix[row][0] == 0 表示第 row 行应清零;对任意 col > 0matrix[0][col] == 0 表示第 col 列应清零。首行首列必须最后处理,否则会提前破坏这些标记。

解题步骤

  1. 扫描第一行和第一列,分别保存它们原本是否含 0。
  2. 扫描不含第一行、第一列的内部区域;遇到 0,就把对应的行标记和列标记置为 0。
  3. 根据第一列的行标记,清零内部各行。
  4. 根据第一行的列标记,清零内部各列。
  5. 根据 firstRowZerofirstColZero,最后清零第一行和第一列。

例如 [[1,1,1],[1,0,1],[1,1,1]]:内部的 0 把 matrix[1][0]matrix[0][1] 设为 0;第二阶段只清第 1 行和第 1 列,不会把新产生的 0 再次扩散。

代码实现

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)$,标记复用第一行、第一列,只额外使用两个布尔变量。

关键点总结

  • 先标记、后置零,避免新写入的 0 污染判断。
  • 第一行和第一列就是原地版的行、列标记数组。
  • matrix[0][0] 无法同时表达两个状态,因此需额外保存首行、首列信息。
  • 清理顺序必须是内部区域在前,第一行和第一列在后。

易错点总结

  • 边扫描边清零会产生连锁扩散,例如中心为 0 的 3 × 3 矩阵会被错误地全部清空。
  • 忘记单独记录第一行或第一列,会丢失它们原本是否需要清零的信息。
  • 先清第一行、第一列会破坏标记,使内部位置漏清。
  • 行列维度不要写反,非方阵最容易暴露这类错误。

相似题目

题目 难度 考察点
面试题 01.08. 零矩阵 中等 同题的原地标记复刻
289. 生命游戏 中等 原地状态编码避免污染
48. 旋转图像 中等 矩阵原地翻转与坐标映射
54. 螺旋矩阵 中等 四边界收缩遍历