【Hot100|17-LeetCode 73. 矩阵置零 - 完整解法详解】

LeetCode 73. 矩阵置零 - 完整解法详解

一、问题理解

问题描述

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

示例

python

复制代码
输入: matrix = [[1,1,1],[1,0,1],[1,1,1]]
输出: [[1,0,1],[0,0,0],[1,0,1]]
解释: 位置(1,1)的元素为0,因此第一行和第二列都被设为0

输入: 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]]

要求

  • 使用原地算法

  • 时间复杂度:O(m × n)

  • 空间复杂度:尽可能低

二、核心思路:标记与置零

基本思想

我们可以分两步解决这个问题:

  1. 标记阶段:找出哪些行和列需要被置零

  2. 置零阶段:根据标记将对应的行和列置零

优化方法

为了降低空间复杂度,我们可以:

  1. 使用矩阵的第一行和第一列作为标记空间

  2. 使用两个额外变量标记第一行和第一列是否需要置零

三、代码逐行解析

方法一:使用两个标记数组(空间复杂度 O(m+n))

Python 解法

python

复制代码
from typing import List

class Solution:
    def setZeroes(self, matrix: List[List[int]]) -> None:
        """
        不要返回任何内容,原地修改矩阵
        """
        m, n = len(matrix), len(matrix[0])
        
        # 1. 创建两个标记数组,记录哪些行和列需要置零
        zero_rows = [False] * m  # 记录哪些行需要置零
        zero_cols = [False] * n  # 记录哪些列需要置零
        
        # 2. 第一次遍历:标记哪些行和列需要置零
        for i in range(m):
            for j in range(n):
                if matrix[i][j] == 0:
                    zero_rows[i] = True
                    zero_cols[j] = True
        
        # 3. 第二次遍历:根据标记置零
        for i in range(m):
            for j in range(n):
                if zero_rows[i] or zero_cols[j]:
                    matrix[i][j] = 0
Java 解法

java

复制代码
class Solution {
    public void setZeroes(int[][] matrix) {
        int m = matrix.length;
        int n = matrix[0].length;
        
        // 1. 创建两个标记数组
        boolean[] zeroRows = new boolean[m];
        boolean[] zeroCols = new boolean[n];
        
        // 2. 标记需要置零的行和列
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (matrix[i][j] == 0) {
                    zeroRows[i] = true;
                    zeroCols[j] = true;
                }
            }
        }
        
        // 3. 根据标记置零
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (zeroRows[i] || zeroCols[j]) {
                    matrix[i][j] = 0;
                }
            }
        }
    }
}

方法二:优化空间复杂度(O(1) 额外空间)

Python 解法

python

复制代码
from typing import List

class Solution:
    def setZeroes(self, matrix: List[List[int]]) -> None:
        """
        使用第一行和第一列作为标记空间
        """
        m, n = len(matrix), len(matrix[0])
        
        # 1. 判断第一行和第一列是否需要置零
        first_row_zero = any(matrix[0][j] == 0 for j in range(n))
        first_col_zero = any(matrix[i][0] == 0 for i in range(m))
        
        # 2. 使用第一行和第一列作为标记
        for i in range(1, m):
            for j in range(1, n):
                if matrix[i][j] == 0:
                    matrix[i][0] = 0  # 标记这一行需要置零
                    matrix[0][j] = 0  # 标记这一列需要置零
        
        # 3. 根据标记置零(除了第一行和第一列)
        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
        
        # 4. 处理第一行
        if first_row_zero:
            for j in range(n):
                matrix[0][j] = 0
        
        # 5. 处理第一列
        if first_col_zero:
            for i in range(m):
                matrix[i][0] = 0
Java 解法

java

复制代码
class Solution {
    public void setZeroes(int[][] matrix) {
        int m = matrix.length;
        int n = matrix[0].length;
        
        // 1. 判断第一行和第一列是否需要置零
        boolean firstRowZero = false;
        boolean 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;
            }
        }
        
        // 2. 使用第一行和第一列作为标记
        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;  // 标记这一列
                }
            }
        }
        
        // 3. 根据标记置零(除了第一行和第一列)
        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;
                }
            }
        }
        
        // 4. 处理第一行
        if (firstRowZero) {
            for (int j = 0; j < n; j++) {
                matrix[0][j] = 0;
            }
        }
        
        // 5. 处理第一列
        if (firstColZero) {
            for (int i = 0; i < m; i++) {
                matrix[i][0] = 0;
            }
        }
    }
}

方法三:一行代码解法(Python 简洁版)

Python 解法

python

复制代码
from typing import List

class Solution:
    def setZeroes(self, matrix: List[List[int]]) -> None:
        """
        简洁但空间复杂度较高的解法
        """
        row_has_zero = [0 in row for row in matrix]  # 行是否包含 0
        col_has_zero = [0 in col for col in zip(*matrix)]  # 列是否包含 0

        for i, row0 in enumerate(row_has_zero):
            for j, col0 in enumerate(col_has_zero):
                if row0 or col0:  # i 行或 j 列有 0
                    matrix[i][j] = 0  # 题目要求原地修改,无返回值

四、Java 与 Python 语法对比

1. 矩阵/二维数组操作

操作 Java Python
获取行数 matrix.length len(matrix)
获取列数 matrix[0].length len(matrix[0])
遍历行 for (int i=0; i<m; i++) for i in range(m):
遍历列 for (int j=0; j<n; j++) for j in range(n):
转置矩阵 需要手动实现 zip(*matrix)

2. 布尔数组/列表

操作 Java Python
创建布尔数组 boolean[] arr = new boolean[n]; arr = [False] * n
检查是否存在 需要遍历 any(arr)
标记为真 arr[i] = true; arr[i] = True

3. 原地修改

操作 Java Python
修改元素 matrix[i][j] = 0; matrix[i][j] = 0
方法返回值 void None

五、实例演示

示例:matrix = [[1,1,1],[1,0,1],[1,1,1]]

方法一(两个标记数组)步骤:
  1. 标记阶段

    • 遍历发现 matrix[1][1] = 0

    • 标记 zero_rows[1] = True

    • 标记 zero_cols[1] = True

  2. 置零阶段

    • 遍历所有位置,如果行索引为1或列索引为1,则置零

    • 结果:[[1,0,1],[0,0,0],[1,0,1]]

方法二(优化空间)步骤:
  1. 判断第一行和第一列

    • first_row_zero = False(第一行没有0)

    • first_col_zero = False(第一列没有0)

  2. 标记阶段(使用第一行和第一列):

    • 发现 matrix[1][1] = 0

    • 设置 matrix[1][0] = 0(标记第1行)

    • 设置 matrix[0][1] = 0(标记第1列)

    • 此时矩阵:[[1,0,1],[0,0,1],[1,1,1]]

  3. 置零阶段(除了第一行和第一列):

    • 遍历 i=1..2, j=1..2

    • 位置 (1,1): matrix[1][0]==0 or matrix[0][1]==0 → 置零

    • 位置 (1,2): matrix[1][0]==0 → 置零

    • 位置 (2,1): matrix[0][1]==0 → 置零

    • 位置 (2,2): 不满足条件

    • 此时矩阵:[[1,0,1],[0,0,0],[1,0,1]]

  4. 处理第一行和第一列

    • first_row_zero = False,第一行不变

    • first_col_zero = False,第一列不变

    • 最终结果:[[1,0,1],[0,0,0],[1,0,1]]

方法三(一行代码版)步骤:
  1. 计算行标记

    • row_has_zero = [False, True, False]

    • 因为第1行(索引1)包含0

  2. 计算列标记

    • col_has_zero = [False, True, False]

    • 因为第1列(索引1)包含0

  3. 置零

    • 遍历所有位置,如果行或列标记为True,则置零

    • 结果:[[1,0,1],[0,0,0],[1,0,1]]

六、关键细节解析

1. 为什么要先判断第一行和第一列?

  • 在优化空间的方法中,我们使用第一行和第一列作为标记空间

  • 如果第一行或第一列原本就有0,我们需要记录下来,最后再处理

  • 否则,我们的标记会覆盖第一行和第一列的原始信息

2. 为什么标记阶段要从 (1,1) 开始?

  • 因为 (0,0) 这个位置同时属于第一行和第一列

  • 如果从 (0,0) 开始,可能会错误地标记第一行和第一列

  • 所以我们要先处理完第一行和第一列的判断,再从 (1,1) 开始标记

3. 如何处理边界情况?

  • 空矩阵:直接返回

  • 1×1 矩阵:如果元素为0,则已经是0;如果不为0,则不变

  • 只有一行或一列:特殊处理,但算法仍然适用

4. 为什么方法三的空间复杂度较高?

  • 方法三创建了两个长度为 m 和 n 的布尔列表

  • 空间复杂度为 O(m+n),虽然比 O(m×n) 好,但不如 O(1) 的优化方法

5. 原地修改的含义是什么?

  • 原地修改意味着不能创建新的矩阵来存储结果

  • 所有修改都必须在原始矩阵上进行

  • 这也是为什么题目要求函数返回 None(Python)或 void(Java)

七、复杂度分析

方法一(两个标记数组)

  • 时间复杂度:O(m × n),需要两次遍历矩阵

  • 空间复杂度:O(m + n),需要两个标记数组

方法二(优化空间)

  • 时间复杂度:O(m × n),需要多次遍历矩阵

  • 空间复杂度:O(1),只使用了常数个额外变量

方法三(一行代码版)

  • 时间复杂度:O(m × n),需要遍历矩阵多次

  • 空间复杂度:O(m + n),需要两个标记列表

八、其他解法

解法一:使用集合记录零的位置(Python)

python

复制代码
class Solution:
    def setZeroes(self, matrix: List[List[int]]) -> None:
        m, n = len(matrix), len(matrix[0])
        zero_rows = set()
        zero_cols = set()
        
        # 记录零的位置
        for i in range(m):
            for j in range(n):
                if matrix[i][j] == 0:
                    zero_rows.add(i)
                    zero_cols.add(j)
        
        # 置零
        for i in range(m):
            for j in range(n):
                if i in zero_rows or j in zero_cols:
                    matrix[i][j] = 0

解法二:分步置零法

python

复制代码
class Solution:
    def setZeroes(self, matrix: List[List[int]]) -> None:
        m, n = len(matrix), len(matrix[0])
        
        # 第一步:标记需要置零的行和列
        rows_to_zero = set()
        cols_to_zero = set()
        
        for i in range(m):
            for j in range(n):
                if matrix[i][j] == 0:
                    rows_to_zero.add(i)
                    cols_to_zero.add(j)
        
        # 第二步:行置零
        for row in rows_to_zero:
            for j in range(n):
                matrix[row][j] = 0
        
        # 第三步:列置零
        for col in cols_to_zero:
            for i in range(m):
                matrix[i][col] = 0

九、常见问题与解答

Q1: 为什么不能先置零再遍历?

A1: 如果先置零再遍历,会创建新的0,导致连锁反应。例如:

text

复制代码
原始矩阵: [[0,1,1],[1,1,1],[1,1,1]]
如果先置零第一行: [[0,0,0],[1,1,1],[1,1,1]]
再遍历时发现新的0,继续置零,最终所有元素都变成0

Q2: 如何处理大规模矩阵?

A2: 对于大规模矩阵:

  • 优化空间的方法(O(1)空间复杂度)是最佳选择

  • 避免创建与矩阵大小相关的额外数据结构

  • 尽量减少遍历次数

Q3: 这个方法适用于非方阵吗?

A3: 是的,算法适用于任意 m × n 的矩阵,包括非方阵。

Q4: 如果矩阵中有负数怎么办?

A4: 算法只关心元素是否为0,与正负无关。负数可以正常处理。

Q5: 如何测试边界情况?

A5: 应该测试以下情况:

  • 空矩阵

  • 1×1矩阵(元素为0或非0)

  • 只有一行的矩阵

  • 只有一列的矩阵

  • 所有元素都是0的矩阵

  • 没有0的矩阵

十、相关题目

1. LeetCode 289. 生命游戏

python

复制代码
class Solution:
    def gameOfLife(self, board: List[List[int]]) -> None:
        m, n = len(board), len(board[0])
        
        # 八个方向的偏移量
        directions = [(-1,-1), (-1,0), (-1,1),
                     (0,-1),         (0,1),
                     (1,-1),  (1,0),  (1,1)]
        
        # 遍历每个细胞
        for i in range(m):
            for j in range(n):
                # 统计活邻居的数量
                live_neighbors = 0
                for dx, dy in directions:
                    ni, nj = i + dx, j + dy
                    if 0 <= ni < m and 0 <= nj < n and abs(board[ni][nj]) == 1:
                        live_neighbors += 1
                
                # 应用规则
                if board[i][j] == 1 and (live_neighbors < 2 or live_neighbors > 3):
                    board[i][j] = -1  # 活细胞死亡
                elif board[i][j] == 0 and live_neighbors == 3:
                    board[i][j] = 2   # 死细胞复活
        
        # 更新细胞状态
        for i in range(m):
            for j in range(n):
                if board[i][j] > 0:
                    board[i][j] = 1
                else:
                    board[i][j] = 0

2. LeetCode 48. 旋转图像

python

复制代码
class Solution:
    def rotate(self, matrix: List[List[int]]) -> None:
        n = len(matrix)
        
        # 转置矩阵
        for i in range(n):
            for j in range(i, n):
                matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
        
        # 反转每一行
        for i in range(n):
            matrix[i].reverse()

十一、总结

核心要点

  1. 问题分解:将矩阵置零问题分解为标记和置零两个阶段

  2. 空间优化:使用矩阵的第一行和第一列作为标记空间,实现 O(1) 空间复杂度

  3. 原地修改:所有操作都在原矩阵上进行,不创建新矩阵

算法步骤(优化空间版本)

  1. 判断第一行和第一列是否需要置零

  2. 使用第一行和第一列作为标记空间,记录其他行和列是否需要置零

  3. 根据标记将对应的行和列置零(除了第一行和第一列)

  4. 处理第一行和第一列

时间复杂度与空间复杂度

  • 时间复杂度:O(m × n),需要遍历矩阵多次

  • 空间复杂度:O(1),只使用常数个额外变量

适用场景

  • 需要原地修改矩阵

  • 空间复杂度要求严格

  • 矩阵可能很大,不能创建额外的大数据结构

扩展思考

矩阵置零问题的核心思想是使用现有空间存储额外信息,这种思想可以应用于:

  • 其他需要标记的矩阵问题

  • 需要原地操作的数组/矩阵问题

  • 空间受限环境下的算法设计

掌握矩阵置零的多种解法,不仅能够解决这个问题,还能理解如何在空间和时间之间做出权衡,是面试中重要的算法技巧。

相关推荐
ArturiaZ20 小时前
【day29】
数据结构·c++·算法
MoonOutCloudBack21 小时前
VeRL 框架下 RL 微调 DeepSeek-7B,比较 PPO / GRPO 脚本的参数差异
人工智能·深度学习·算法·语言模型·自然语言处理
_F_y21 小时前
二叉树中的深搜
算法
锅包一切21 小时前
PART17 一维动态规划
c++·学习·算法·leetcode·动态规划·力扣·刷题
Polaris北21 小时前
第二十六天打卡
c++·算法·动态规划
罗湖老棍子1 天前
【例 2】选课(信息学奥赛一本通- P1576)
算法·树上背包·树型动态规划
每天要多喝水1 天前
动态规划Day33:编辑距离
算法·动态规划
每天要多喝水1 天前
动态规划Day34:回文
算法·动态规划
weixin_477271691 天前
马王堆帛书《周易》系统性解读(《函谷门》原创)
算法·图搜索算法
AomanHao1 天前
【ISP】基于暗通道先验改进的红外图像透雾
图像处理·人工智能·算法·计算机视觉·图像增强·红外图像