【力扣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 生命游戏 原地算法,用位操作标记
相关推荐
wzdark10 小时前
稀疏矩阵存储结构及其运算复杂度研究4
线性代数·矩阵
Phil32310 小时前
多智能体不是越多越好:Google《Towards a Science of Scaling Agent Systems》论文深度解读
算法
6Hzlia10 小时前
【Classic 150 刷题计划】 LeetCode 26. 删除有序数组中的重复项 | C++ 快慢双指针经典模板
c++·算法·leetcode
huang57914711 小时前
基于滑动窗口的流式数据算法优化思路3
算法
Ulyanov11 小时前
AudioVision Pro:基于 PySide6 + sounddevice 的实时音频可视化播放器设计
python·算法·音视频
土司大王11 小时前
LeetCode hot100——74.搜索二维矩阵:Java 二分模板
java·算法·leetcode
tyler_download11 小时前
揉扁搓圆transformer架构:NAG优化器算法详解
深度学习·算法·transformer
weixin1997010801611 小时前
[特殊字符]《京东POP二手品类对接:B2C订单模型 vs 二手C2C属性的字段转换难题》(附Python源码)
前端·python·算法
Jared_devin12 小时前
单头、多头 Self-Attention可视化
人工智能·pytorch·深度学习·算法·机器学习·pycharm
wuyk55512 小时前
18.Kruskal 算法:用最短的边连成一张网
开发语言·算法·图论