Hot 100 --- 矩阵置零

本文概览:本文以LeetCode经典题目"矩阵置零"为例,从最容易踩的坑入手,逐步优化空间复杂度,从 O(mn) 到 O(m+n) 再到 O(1) 原地变换,系统讲解如何用第一行和第一列作为标记数组实现原地置零


一、题目

二、题目分析

给定一个 m×n 的矩阵,如果一个元素为 0,则将其所在行和列的所有元素都设为 0,要求原地修改

目标:将所有包含 0 的元素所在的行和列全部置零

最容易踩的坑:不能边遍历边置零

这是本题最容易忽略的地方。如果在遍历矩阵的时候顺便置零,会导致后续遍历时无法区分某个 0 是原本就有的 还是置零操作产生的。比如矩阵中只有一个原本的 0,但置零后它所在的行和列都变成了 0,后续遍历到这些新的 0 时又会把更多的行和列置零,最终整个矩阵都变成了 0,算法完全错误

所以正确的做法是:先标记,后置零。第一遍遍历只记录哪些位置有 0,第二遍遍历根据标记来置零

思路概览

Java实现代码如下

Java 复制代码
public void setZeroes(int[][] matrix) {
    // 行长度
    int m = matrix.length;
    // 列长度
    int n = matrix[0].length;

    // 记录第一列或者第一行是否有0
    boolean row0 = false;
    boolean col0 = false;
    // 记录第0列是否有0
    for (int i = 0; i < m; i++) {
        if (matrix[i][0] == 0)
            col0 = true;
    }
    // 记录第0行是否有0
    for (int j = 0; j < n; j++) {
        if (matrix[0][j] == 0)
            row0 = true;
    }

    // 用第一行和第一列作为标记
    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;
            }
        }
    }

    // 置0
    for (int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            if (matrix[0][j] == 0 || matrix[i][0] == 0)
                matrix[i][j] = 0;
        }
    }

    // 最后处理第一行/第一列
    if (row0) for (int j = 0; j < n; j++) matrix[0][j] = 0;
    if (col0) for (int i = 0; i < m; i++) matrix[i][0] = 0;
}

思路简要说明

  1. 记录第一行和第一列的原始状态:用两个布尔变量记录第一行和第一列本身是否含有 0

  2. 用第一行和第一列作为标记:遍历内部矩阵,遇到 0 就把对应的第一行和第一列位置标记为 0

  3. 根据标记置零:再次遍历内部矩阵,根据第一行和第一列的标记来置零

  4. 最后处理第一行和第一列:根据布尔变量决定是否将第一行或第一列全部置零

三、思路详解

思路一:O(mn) 空间

最直接的想法是:创建一个和原矩阵同样大小的 m×n 标记数组,遍历原矩阵,把所有 0 的位置在标记数组中标记出来。然后再次遍历原矩阵,去标记数组中查询对应位置是否为 0,是就置零

  • 时间复杂度:O(mn)
  • 空间复杂度:O(mn),需要额外一个 m×n 的标记数组
  • 核心瓶颈:标记数组和原矩阵一样大,空间开销太大
  • 关键思考:我们真的需要记录每个位置是否为 0 吗?

思路二:O(m+n) 空间

仔细想想,我们不需要记录"哪个位置是 0",只需要记录"哪一行和哪一列需要置零"。因为只要某一行有一个 0,整行都要置零;某一列有一个 0,整列都要置零

所以可以用两个一维数组:一个长度为 m 的数组标记哪些行需要置零,一个长度为 n 的数组标记哪些列需要置零。遍历矩阵时,遇到 matrix[i][j] == 0,就把行标记数组第 i 位和列标记数组第 j 位设为 true

  • 时间复杂度:O(mn)
  • 空间复杂度:O(m+n),两个一维数组
  • 核心瓶颈:虽然比 O(mn) 好很多,但仍然有额外空间开销
  • 关键思考:能否不创建任何额外数组,原地完成标记?

思路三:O(1) 空间------原地变换

核心思想

既然我们需要标记"哪些行和哪些列需要置零",而行标记和列标记本质上就是两个一维数组。那我们能不能用矩阵本身的第一行和第一列来充当这两个标记数组

  • 第一行充当列标记数组:matrix[0][j] == 0 表示第 j 列需要置零
  • 第一列充当行标记数组:matrix[i][0] == 0 表示第 i 行需要置零

这样就不需要创建额外的数组了,空间复杂度降为 O(1)

为什么用第一行和第一列作为标记数组是可行的?

这是整个解法最关键的地方。我们用一个具体的矩阵来逐步说明

假设原始矩阵如下,其中 matrix[2][3] = 0

复制代码
        列0  列1  列2  列3  列4
行0  [  1 ,  1 ,  1 ,  1 ,  1 ]  ← 第一行:充当列标记
行1  [  1 ,  1 ,  1 ,  1 ,  1 ]
行2  [  1 ,  1 ,  1 ,  0 ,  1 ]  ← 这里有0!
行3  [  1 ,  1 ,  1 ,  1 ,  1 ]
         ↑
    第一列:充当行标记

标记操作 :遇到 matrix[2][3] = 0,把对应的第一行和第一列位置标记为 0

复制代码
        列0  列1  列2  列3  列4
行0  [  1 ,  1 ,  1 ,  0 ,  1 ]  ← matrix[0][3]被标记为0(第3列需要置零)
行1  [  1 ,  1 ,  1 ,  1 ,  1 ]
行2  [  0 ,  1 ,  1 ,  0 ,  1 ]  ← matrix[2][0]被标记为0(第2行需要置零)
行3  [  1 ,  1 ,  1 ,  1 ,  1 ]
         ↑
    第2行标记位为0

置零操作:遍历内部矩阵,看第一行和第一列的标记

复制代码
        列0  列1  列2  列3  列4
行0  [  1 ,  1 ,  1 ,  0 ,  1 ]  ← 列标记:第3列需要置零
行1  [  1 ,  1 ,  1 ,  0 ,  1 ]  ← matrix[0][3]==0,所以matrix[1][3]置零
行2  [  0 ,  0 ,  0 ,  0 ,  0 ]  ← matrix[2][0]==0,整行置零
行3  [  1 ,  1 ,  1 ,  0 ,  1 ]  ← matrix[0][3]==0,所以matrix[3][3]置零
         ↑
    行标记:第2行需要置零

可以看到:标记位为 0 后,遍历内部矩阵时对应的整行或整列都会被置零,不需要额外的行遍历或列遍历

那如果第一行或第一列本身就有 0 呢?比如 matrix[0][2] 原本就是 0:

复制代码
        列0  列1  列2  列3  列4
行0  [  1 ,  1 ,  0 ,  1 ,  1 ]  ← 原本就有0
行1  [  1 ,  1 ,  1 ,  1 ,  1 ]
行2  [  1 ,  1 ,  1 ,  0 ,  1 ]  ← 内部也有0
行3  [  1 ,  1 ,  1 ,  1 ,  1 ]

标记后:

复制代码
        列0  列1  列2  列3  列4
行0  [  1 ,  1 ,  0 ,  0 ,  1 ]  ← matrix[0][3]被标记为0
行1  [  1 ,  1 ,  1 ,  1 ,  1 ]
行2  [  0 ,  1 ,  1 ,  0 ,  1 ]  ← matrix[2][0]被标记为0
行3  [  1 ,  1 ,  1 ,  1 ,  1 ]

此时 matrix[0][2] 是原本的 0,matrix[0][3] 是标记产生的 0,我们无法区分。但这没关系------第一行本身有 0,意味着第一行最终本来就要全部置零,标记操作把某个位置设为 0,和"原本就有 0"的最终效果是一样的。所以标记和原始数据之间不会产生矛盾

我们只需要用两个布尔变量 row0col0 提前记录第一行和第一列是否原本含有 0,最后根据这两个变量决定是否将第一行或第一列全部置零即可

具体步骤

第一步:记录第一行和第一列的原始状态

遍历第一行,如果有 0 则 row0 = true;遍历第一列,如果有 0 则 col0 = true

第二步:用第一行和第一列作为标记

遍历内部矩阵(i 从 1 到 m-1,j 从 1 到 n-1),如果 matrix[i][j] == 0,就把 matrix[i][0]matrix[0][j] 标记为 0

这一步完全没有冲突:因为内部矩阵的 0 只会影响第一行和第一列的标记,而第一行和第一列的置零操作我们留到最后单独处理

第三步:根据标记置零内部矩阵

再次遍历内部矩阵,如果 matrix[i][0] == 0matrix[0][j] == 0,就把 matrix[i][j] 置零

第四步:处理第一行和第一列

根据第一步记录的布尔变量,如果 row0 为 true,就把第一行全部置零;如果 col0 为 true,就把第一列全部置零

为什么第一行和第一列要最后处理?

因为第一行和第一列在第二步和第三步中充当标记数组,如果提前把它们置零了,标记信息就丢失了,后续无法正确判断哪些行和列需要置零

举例说明

matrix = [[1,1,1],[1,0,1],[1,1,1]] 为例

第一步:记录第一行和第一列状态

第一行无 0,row0 = false;第一列无 0,col0 = false

第二步:标记

复制代码
遍历到 matrix[1][1] = 0:
标记 matrix[1][0] = 0,matrix[0][1] = 0

矩阵变为:
[1, 0, 1]
[0, 0, 1]
[1, 1, 1]

第三步:置零内部矩阵

复制代码
matrix[1][1]:matrix[1][0]==0,置零
matrix[1][2]:matrix[1][0]==0,置零
matrix[2][1]:matrix[0][1]==0,置零

矩阵变为:
[1, 0, 1]
[0, 0, 0]
[1, 0, 1]

第四步:处理第一行和第一列

row0 = false,第一行不动;col0 = false,第一列不动

最终结果为 [[1,0,1],[0,0,0],[1,0,1]],正确

  • 时间复杂度:O(mn),遍历矩阵若干次
  • 空间复杂度:O(1),只用了两个布尔变量
相关推荐
生成论实验室1 小时前
自动驾驶:一个自主运动的系统
人工智能·算法·机器学习·语言模型·机器人·自动驾驶·安全架构
sheeta19981 小时前
LeetCode 每日一题笔记 日期:2026.06.16 题目:3612. 字符串特殊符号处理
笔记·算法·leetcode
CoderYanger1 小时前
A.每日一题:2095. 删除链表的中间节点
java·数据结构·程序人生·leetcode·链表·面试·职场和发展
Jasmine_llq1 小时前
《B4264 [GESP202503 四级] 二阶矩阵》
线性代数·算法·矩阵·二维矩阵遍历枚举所有2×2矩阵·交叉乘积等式条件判断·输入输出快读加速·长整型防溢出计数统计
不知名的老吴2 小时前
面经经验分享|算法和数据结构考察
数据结构·经验分享·算法
snow@li2 小时前
前端:构建工具(Vite / Webpack)的 文件指纹(File Hash) 机制 / 浏览器缓存控制
前端·webpack·哈希算法
叫我:松哥2 小时前
基于Python flask的中学可控智能命题系统设计与实现,整合遗传算法、DeepSeek 大模型及数据库技术构建一体化应用
数据库·人工智能·python·算法·机器学习·flask·遗传算法
CoderYanger2 小时前
A.每日一题:3612. 用特殊操作处理字符串 I
java·程序人生·leetcode·面试·职场和发展·学习方法·改行学it
黎阳之光2 小时前
黎阳之光透明大楼:实景孪生重构智慧建筑全新范式
人工智能·物联网·算法·安全·数字孪生