【力扣100题】22. 矩阵置零

一、题目描述

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

示例 1:

复制代码
输入:matrix = [[1,1,1],[1,0,1],[1,1,1]]
输出:[[1,0,1],[0,0,0],[1,0,1]]

示例 2:

复制代码
输入:matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]]
输出:[[0,0,0,0],[0,4,5,0],[0,3,1,0]]

提示:

  • m == matrix.length
  • n == matrix0.length
  • 1 <= m, n <= 200
  • -2^31 <= matrixij <= 2^31 - 1

进阶:

  • 一个直观的解决方案是使用 O(mn) 的额外空间,但这并不是一个好的解决方案
  • 一个简单的改进方案是使用 O(m + n) 的额外空间,但这仍然不是最好的解决方案
  • 你能想出一个仅使用常量空间的解决方案吗

二、解题思路总览

核心问题: 如何记录哪些行和列需要置零,同时不破坏原矩阵?

解决方案:两次遍历 + 额外数组记录

遍历 操作
第一次遍历 扫描整个矩阵,遇到 0 就记录该行该列需要置零
第二次遍历 根据记录,将对应行和列的所有元素置零
方案 时间复杂度 空间复杂度
暴力解(每个0都遍历行列) O(mn * (m+n)) O(1)
额外数组记录(本题) O(mn) O(m+n)
原地算法(进阶) O(mn) O(1)

三、完整代码

cpp 复制代码
class Solution {
public:
    void setZeroes(vector<vector<int>>& matrix) {
        int m = matrix.size();
        int n = matrix[0].size();

        vector<int> row(m, 0);  // 记录哪些行需要置零
        vector<int> col(n, 0);  // 记录哪些列需要置零

        // 第一次遍历:扫描矩阵,记录需要置零的行和列
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (matrix[i][j] == 0) {
                    row[i] = 1;
                    col[j] = 1;
                }
            }
        }

        // 第二次遍历:根据记录,将对应行和列置零
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (row[i] || col[j]) {
                    matrix[i][j] = 0;
                }
            }
        }
    }
};

四、算法流程图

4.1 整体流程

复制代码
输入:matrix(m x n 矩阵)

[Step 1] 获取矩阵尺寸
         m = matrix.size()
         n = matrix[0].size()
         |
         v
[Step 2] 创建辅助数组
         row = vector<int>(m, 0)
         col = vector<int>(n, 0)
         |
         v
[Step 3] 第一次遍历:扫描并标记
         |
         v
    for i = 0 to m-1:
        for j = 0 to n-1:
            matrix[i][j] == 0 ?
              |是                  |否
              v                   v
          row[i] = 1          继续
          col[j] = 1              |
              |                   v
              +<---- 继续内层循环 --+
              |
              v
         内层循环结束
              |
              v
         回到外层循环或下一步
              |
              v
[Step 4] 第二次遍历:置零
              |
              v
    for i = 0 to m-1:
        for j = 0 to n-1:
            row[i] || col[j] ?
              |是                  |否
              v                   v
          matrix[i][j] = 0     继续(不变)
              |                   |
              v                   v
         【返回】            【返回】

4.2 第一次遍历(标记)详细流程

复制代码
外层循环:i = 0 to m-1
  内层循环:j = 0 to n-1
    |
    v
  matrix[i][j] == 0 ?
    |否
    v
  继续 j++

    |是
    v
  row[i] = 1
  col[j] = 1
  |
  v
  继续 j++

内层循环 j 结束后
  |
  v
i++ 继续外层循环

所有遍历完成后:
row 数组标记了所有含 0 的行
col 数组标记了所有含 0 的列

4.3 第二次遍历(置零)详细流程

复制代码
外层循环:i = 0 to m-1
  内层循环:j = 0 to n-1
    |
    v
  row[i] == 1 或 col[j] == 1 ?
    |是
    v
  matrix[i][j] = 0
    |
    v
  继续 j++

    |否(该位置不变)
    |
    v
  继续 j++

内层循环 j 结束后
  |
  v
i++ 继续外层循环

4.4 具体示例执行流程

复制代码
输入矩阵:
[[1, 1, 1],
 [1, 0, 1],
 [1, 1, 1]]

m = 3, n = 3
row = [0, 0, 0]
col = [0, 0, 0]

第一次遍历(标记):
  i=0, j=0: matrix[0][0]=1 != 0 → 跳过
  i=0, j=1: matrix[0][1]=1 != 0 → 跳过
  i=0, j=2: matrix[0][2]=1 != 0 → 跳过
  i=1, j=0: matrix[1][0]=1 != 0 → 跳过
  i=1, j=1: matrix[1][1]=0 == 0 → row[1]=1, col[1]=1
  i=1, j=2: matrix[1][2]=1 != 0 → 跳过
  i=2, j=0: matrix[2][0]=1 != 0 → 跳过
  i=2, j=1: matrix[2][1]=1 != 0 → 跳过
  i=2, j=2: matrix[2][2]=1 != 0 → 跳过

标记结果:
row = [0, 1, 0]
col = [0, 1, 0]

第二次遍历(置零):
  i=0, j=0: row[0]=0 且 col[0]=0 → 保持 1
  i=0, j=1: row[0]=0 但 col[1]=1 → 置零
  i=0, j=2: row[0]=0 且 col[2]=0 → 保持 1
  i=1, j=0: row[1]=1 → 置零
  i=1, j=1: row[1]=1 且 col[1]=1 → 置零
  i=1, j=2: row[1]=1 → 置零
  i=2, j=0: row[2]=0 且 col[0]=0 → 保持 1
  i=2, j=1: row[2]=0 但 col[1]=1 → 置零
  i=2, j=2: row[2]=0 且 col[2]=0 → 保持 1

输出矩阵:
[[1, 0, 1],
 [0, 0, 0],
 [1, 0, 1]]

五、逐行解析

5.1 创建辅助数组

cpp 复制代码
vector<int> row(m, 0);  // 记录哪些行需要置零,初始化全为 0
vector<int> col(n, 0);  // 记录哪些列需要置零,初始化全为 0

原理: 用两个一维数组分别记录需要置零的行和列。数组下标对应行号或列号,值为 1 表示需要置零。

空间复杂度: O(m + n)


5.2 第一次遍历:标记

cpp 复制代码
for (int i = 0; i < m; i++) {
    for (int j = 0; j < n; j++) {
        if (matrix[i][j] == 0) {
            row[i] = 1;
            col[j] = 1;
        }
    }
}

原理: 遍历整个矩阵,找到所有值为 0 的元素,将其所在行和列标记。

时间复杂度: O(m * n)


5.3 第二次遍历:置零

cpp 复制代码
for (int i = 0; i < m; i++) {
    for (int j = 0; j < n; j++) {
        if (row[i] || col[j]) {
            matrix[i][j] = 0;
        }
    }
}

原理: 再次遍历矩阵,如果当前元素所在行或列被标记过(rowi || colj 为真),则将该元素置零。

逻辑: 只要行被标记 OR 列被标记,该元素就需要置零。


六、进阶:原地算法 O(1) 空间

6.1 核心思想

用矩阵的第一行和第一列作为标记数组,代替 row 和 col 的作用。

问题: 如何区分「原本就是 0」和「被置零后变成 0」?

解决方案: 在遍历前,先判断第一行和第一列是否需要置零,然后用它们作为标记。

6.2 进阶代码

cpp 复制代码
class Solution {
public:
    void setZeroes(vector<vector<int>>& matrix) {
        int m = matrix.size();
        int n = matrix[0].size();

        // 判断第一行和第一列是否需要置零
        bool firstRowZero = false;
        bool firstColZero = false;

        for (int j = 0; j < n; j++) {
            if (matrix[0][j] == 0) {
                firstRowZero = true;
                break;
            }
        }

        for (int i = 0; i < m; i++) {
            if (matrix[i][0] == 0) {
                firstColZero = true;
                break;
            }
        }

        // 用第一行和第一列作为标记
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                if (matrix[i][j] == 0) {
                    matrix[i][0] = 0;  // 标记第 i 行
                    matrix[0][j] = 0;  // 标记第 j 列
                }
            }
        }

        // 根据标记置零(跳过第一行和第一列)
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                if (matrix[i][0] == 0 || matrix[0][j] == 0) {
                    matrix[i][j] = 0;
                }
            }
        }

        // 处理第一行和第一列
        if (firstRowZero) {
            for (int j = 0; j < n; j++) {
                matrix[0][j] = 0;
            }
        }

        if (firstColZero) {
            for (int i = 0; i < m; i++) {
                matrix[i][0] = 0;
            }
        }
    }
};

6.3 进阶流程图

复制代码
[Step 1] 判断第一行是否含 0
         firstRowZero = matrix[0][j] == 0 ?
         |
         v
[Step 2] 判断第一列是否含 0
         firstColZero = matrix[i][0] == 0 ?
         |
         v
[Step 3] 用第一行和第一列作为标记数组
         for i = 1 to m-1:
             for j = 1 to n-1:
                 matrix[i][j] == 0 ?
                   |是
                   v
               matrix[i][0] = 0  // 标记行
               matrix[0][j] = 0  // 标记列
         |
         v
[Step 4] 根据标记置零(排除第一行第一列)
         for i = 1 to m-1:
             for j = 1 to n-1:
                 matrix[i][0] == 0 或 matrix[0][j] == 0 ?
                   |是
                   v
               matrix[i][j] = 0
         |
         v
[Step 5] 处理第一行
         firstRowZero ?
           |是
           v
         matrix[0][j] = 0 for all j
         |
         v
[Step 6] 处理第一列
         firstColZero ?
           |是
           v
         matrix[i][0] = 0 for all i

6.4 三种方案对比

方案 时间复杂度 空间复杂度 特点
暴力解 O(mn * (m+n)) O(1) 每个0都遍历行列,时间太慢
额外数组(本题) O(mn) O(m+n) 两遍遍历,空间稍大
原地算法(进阶) O(mn) O(1) 用第一行/列作为标记,最优

七、复杂度分析

7.1 本题解法

指标 复杂度 说明
时间复杂度 O(m * n) 两次遍历矩阵
空间复杂度 O(m + n) row 数组 m 个,col 数组 n 个

7.2 进阶原地算法

指标 复杂度 说明
时间复杂度 O(m * n) 三次遍历矩阵
空间复杂度 O(1) 只用几个布尔变量

八、面试追问

问题 回答要点
为什么需要两次遍历? 第一次标记(记录哪些行/列要置零),第二次执行置零。如果边标记边置零,会导致后续判断出错
rowi
空间复杂度能进一步优化吗? 可以,用矩阵第一行和第一列作为标记数组,实现 O(1) 空间
原地算法中 firstRowZero 的作用? 记录第一行本身是否需要置零(因为第一行会被用作标记,不能直接置零)
为什么原地算法要跳过第一行第一列? 第一行和第一列被用作标记数组,如果对它们执行置零操作,会丢失标记信息
原地算法如何恢复第一行第一列? 最后根据 firstRowZero 和 firstColZero 单独处理第一行和第一列
这个题有没有其他解法? 还有一种方案是设置一个 sentinel 值(如 INFINITY)来标记,但会改变矩阵范围外的状态,不推荐

九、相关题目

题号 题目 关键点
73 矩阵置零 本题
48 旋转图像 矩阵旋转
54 螺旋矩阵 矩阵遍历
59 螺旋矩阵 II 矩阵生成
289 生命游戏 原地算法,用位操作标记
相关推荐
luj_17682 小时前
星火科技助力边远地区防病攻坚
c语言·开发语言·c++·经验分享·算法
geats人山人海5 小时前
数据结构第六章c语言 树的存储下二叉树的概念和存储
c语言·数据结构·算法
漂流瓶jz6 小时前
UVA-12627 奇怪的气球膨胀 题解答案代码 算法竞赛入门经典第二版
算法·图论·递归·aoapc·算法竞赛入门经典·uva·12627
jarvisuni6 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
kobesdu6 小时前
流形上的优化:SO(3)与SE(3)的广义加减法在FAST-LIO中如何简化状态估计
人工智能·算法·fastlio
白狐_7986 小时前
408 数据结构算法题 01:线性表暴力求解保分指南
java·数据结构·算法
乐观勇敢坚强的老彭7 小时前
C++信奥:开关门、开关灯问题
开发语言·c++·算法
冻柠檬飞冰走茶7 小时前
PTA基础编程题目集 7-31 字符串循环左移(C语言实现)
c语言·开发语言·数据结构·算法
Hi李耶7 小时前
【LeetCode】4-寻找两个正序数组的中位数
算法·leetcode·职场和发展
图灵机z7 小时前
【算法提高课】AcWing 1027. 方格取数 长期攻克提高课-day3(3/219)
算法