73. 矩阵置零

难度:中等 | 主题:数组、矩阵、原地修改

题目

给定一个 m x n 的矩阵,如果一个元素为 0,则将其所在行和列的所有元素都设为 0。请使用原地算法。

示例

 
matrix = [[1,1,1],[1,0,1],[1,1,1]]
 
输出:[[1,0,1],[0,0,0],[1,0,1]]
 

思路

先讲个故事:教室里的告示板

教室里有一块告示板(矩阵),有人不小心在某个格子洒了墨水。值日生需要把这一整行和这一整列的格子都擦掉。

但值日生手头没有多余的白板来记录哪些行和列需要擦——只能用告示板本身来记

他想到一个办法:用第一行第一列当备忘录。如果第 i 行第 j 列的格子有墨水,就在第一行第 j 列画个圈,在第一列第 i 行画个圈。最后根据这些圈来擦。

但问题是——第一行和第一列本身也可能有墨水!所以要先记下它们原本有没有墨水,再开始做标记。


引导式推导:从 O(m+n) 到 O(1)

第 1 层:用集合存行列号

遍历一遍,遇到 0 就把行列号记下来。再遍历一遍,把记录的行列清零。

  • 时间 O(m×n)

  • 空间 O(m+n)


graph LR

    A["遍历矩阵"] --> B["遇到0 → 记下行号i和列号j"]

    B --> C["再遍历清零"]

第 2 层:用矩阵自身当哈希表

能不能把 O(m+n) 空间省掉?用第一行和第一列充当标记数组!


graph TD

    subgraph 五步法

        S1["① 记录第一行、第一列是否有0"]

        S2["② 遍历其余区域,用第一行/列做标记"]

        S3["③ 根据标记清零其余区域"]

        S4["④ 处理第一行"]

        S5["⑤ 处理第一列"]

    end

    S1 --> S2 --> S3 --> S4 --> S5

为什么必须先记录第一行/列的原始状态?

因为第二步要用第一行/列来存标记,这会覆盖它们原本的值。如果不提前备份,第三步清零后我们就不知道第一行/列本身是否需要清零了。


代码

 
def setZeroes(self, matrix):
 
    if not matrix or not matrix[0]:
 
        return
 
    m, n = len(matrix), len(matrix[0])
 
    # 第一步:记录第一行和第一列本身是否有0
 
    first_row_has_zero = any(matrix[0][j] == 0 for j in range(n))
 
    first_col_has_zero = any(matrix[i][0] == 0 for i in range(m))
 
    # 第二步:用第一行和第一列做标记
 
    for i in range(1, m):
 
        for j in range(1, n):
 
            if matrix[i][j] == 0:
 
                matrix[i][0] = 0   # 第 i 行需要清零
 
                matrix[0][j] = 0   # 第 j 列需要清零
 
    # 第三步:根据标记清零(除第一行/列)
 
    for i in range(1, m):
 
        for j in range(1, n):
 
            if matrix[i][0] == 0 or matrix[0][j] == 0:
 
                matrix[i][j] = 0
 
    # 第四步:处理第一行
 
    if first_row_has_zero:
 
        for j in range(n):
 
            matrix[0][j] = 0
 
    # 第五步:处理第一列
 
    if first_col_has_zero:
 
        for i in range(m):
 
            matrix[i][0] = 0
 

复杂度

指标解释
时间O(m×n)若干次线性扫描
空间O(1)仅两个布尔变量

实战考量

频率分析

出现在:字节/美团常考,核心考点不是算法本身(太简单),而是你能不能优化到 O(1) 空间

延伸思考

Q:为什么不能边遍历边清零?

A:如果你遇到一个 0 就立即把整行整列清零,后面的遍历会误以为那些被清零的位置原本就是 0,导致不该清零的也被清零了。比如矩阵 0,1,边遍历边清零会把 [0,1] 变成 [0,0]。

Q:除了用第一行/列做标记,还有什么方法?

A:可以用两个布尔变量替代集合,思路一样。或者用常数个额外变量的位运算技巧,但第一行/列法最直观。

Q:如果遇到稀疏矩阵(0 很少)呢?

A:可以用两个集合存行和列,空间 O(m+n) 但常数小。可以先讲集合法(直观),再优化到 O(1)。

Q:变形题:把每个 0 的上下左右相邻元素也置零(扩散)?

A:BFS/DFS 扩散,或者两遍扫描——先找到所有 0,再逐层扩散。

易错点

  • 必须先备份第一行/列的原始状态,再做标记

  • 标记和清零的顺序:先标记(除第一行/列)→ 再清零(除第一行/列)→ 最后处理第一行/列

  • 不要用 if matrix[0][j] == 0 替代 first_row_has_zero——标记过程会改变第一行


生活类比

告示板备忘录 → 原地标记

你把第一行和第一列当便利贴用——先看这俩便利贴本身脏不脏,记在心里。

然后用它们记录其他地方哪里需要擦。

擦完其他地方,再看心里记的那两处要不要擦。

把矩阵本身当纸用,省下额外的一张纸。


相关题目

题目关系
54螺旋矩阵矩阵遍历
48旋转图像矩阵原地操作

→ 返回题单:LeetCode学习路线图 > 一、数组与哈希(含双指针、矩阵)

速记卡(面试闪卡)

Q1:一句话讲清「73. 矩阵置零」到底是什么?

A:矩阵里一旦发现某个格子是0,就把它所在的整行整列全部清零,而且必须原地修改、不占额外数组。

Q2:为什么不能边扫边清零? —— 怎么理解?

A:像在白板上擦字,你一擦后面就分不清哪些格子本来就是0、哪些是被你误擦的,会把无辜格子也清零。这考的是原地修改(in-place modification,不借助额外数组、直接在原矩阵上改)。

Q3:第一行/列怎么当备忘录? —— 怎么理解?

A:把第一行和第一列当便利贴:先看它们本身脏不脏(有没有0)记在心里,再用它们去标记别处哪里要擦。这是用矩阵自身当哈希表的空间优化(space optimization via matrix-as-hashmap)。

Q4:五步法的顺序怎么记? —— 怎么理解?

A:像三明治层层来:①先拍照备份首行首列 → ②用首行首列做标记 → ③清零其余 → ④处理首行 → ⑤处理首列。顺序乱了就会误清。这是典型的两遍扫描(two-pass marking)。

Q5:复杂度和实战怎么理解? —— 怎么理解?

A:字节/美团常考,考点根本不是算法本身,而是你能不能把空间从 O(m+n) 压到 O(1)。时间 O(m×n),空间 O(1)(只两个布尔变量)。这就是常量级空间(constant space)的胜利。

Q6:核心速记主线有哪些?

  • 题目:0 所在的整行整列全清零,必须原地

  • 核心:用第一行/列当标记位,省掉额外数组

  • 顺序:先备份首行首列 → 标记 → 清零 → 最后处理首行首列

  • 实战:字节/美团爱问,考点在 O(1) 空间优化

口诀

A:矩阵遇零就归零,

首行首列当便签;

先存原本脏不脏,

原地清空最稳妥。

相关链接