LeetCode 热题 HOT 100(矩阵)73. 矩阵置零

76 阅读4分钟

题目简介

给定一个 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 == matrix[0].length
  • 1 <= m, n <= 200
  • -231 <= matrix[i][j] <= 231 - 1

 

进阶:

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

解题思路

这道题有几种解法,从最简单的思路开始,逐步优化到常数空间复杂度。

基本思路分析

题目的难点在于:如果我们直接修改矩阵,那么后续扫描时无法分辨哪些是原始 0 哪些是被设置的 0,所以需要先记录原始 0 的位置,再统一修改。

优化到常数空间复杂度的方法

最优解法使用矩阵的第一行和第一列作为标记,以记录哪些行列需要设置为 0,额外使用两个变量记录第一行和第一列本身是否需要置为 0.

具体步骤如下:

  1. 使用两个布尔变量记录第一行和第一列是否包含 0
  2. 使用矩阵的第一行和第一列作为标记数组
  3. 遍历矩阵,标记需要设置为 0 的行和列
  4. 根据标记,将相应的行和列设置为 0
  5. 最后处理第一行和第一列

Golang 代码实现

func setZeroes(matrix [][]int) {
    if len(matrix) == 0 || len(matrix[0]) == 0 {
        return
    }
    
    m, n := len(matrix), len(matrix[0])
    firstRowHasZero, firstColHasZero := false, false
    
    for j := 0; j < n; j++ {
        if matrix[0][j] == 0 {
            firstRowHasZero = true
            break
        }
    }
    for i := 0; i < m; i++ {
        if matrix[i][0] == 0 {
            firstColHasZero = true
            break
        }
    }
    
    // 使用第一行和第一列作为标记
    for i := 1; i < m; i++ {
        for j := 1; j < n; j++ {
            if matrix[i][j] == 0 {
                matrix[i][0] = 0
                matrix[0][j] = 0
            }
        }
    }
    
    for i := 1; i < m; i++ {
        if matrix[i][0] == 0 {
            for j := 1; j < n; j++ {
                matrix[i][j] = 0
            }
        }
    }
    
    for j := 1; j < n; j++ {
        if matrix[0][j] == 0 {
            for i := 1; i < m; i++ {
                matrix[i][j] = 0
            }
        }
    }
    
    if firstRowHasZero {
        for j := 0; j < n; j++ {
            matrix[0][j] = 0
        }
    }
    
    if firstColHasZero {
        for i := 0; i < m; i++ {
            matrix[i][0] = 0
        }
    }
}

算法处理时序

sequenceDiagram
    participant M as 主程序
    participant F1 as 检查第一行
    participant F2 as 检查第一列
    participant S as 标记阶段
    participant R1 as 处理行
    participant R2 as 处理列
    participant FR as 处理第一行
    participant FC as 处理第一列
    
    M->>F1: 检查第一行是否有0
    F1-->>M: 返回firstRowHasZero
    M->>F2: 检查第一列是否有0
    F2-->>M: 返回firstColHasZero
    M->>S: 使用第一行和第一列作为标记
    S-->>M: 完成标记
    M->>R1: 根据第一列标记处理行
    R1-->>M: 完成行处理
    M->>R2: 根据第一行标记处理列
    R2-->>M: 完成列处理
    
    alt 第一行有0
        M->>FR: 将第一行全部置为0
        FR-->>M: 完成
    end
    
    alt 第一列有0
        M->>FC: 将第一列全部置为0
        FC-->>M: 完成
    end

关键数据变化图

示例 1

matrix = [[1,1,1],[1,0,1],[1,1,1]]

初始状态:

+---+---+---+
| 1 | 1 | 1 |
+---+---+---+
| 1 | 0 | 1 |
+---+---+---+
| 1 | 1 | 1 |
+---+---+---+

检查第一行和第一列:

+---+---+---+  firstRowHasZero = false
| 1 | 1 | 1 |  firstColHasZero = false
+---+---+---+
| 1 | 0 | 1 |
+---+---+---+
| 1 | 1 | 1 |
+---+---+---+

标记阶段(发现matrix[1][1]=0):

+---+---+---+
| 1 | 0 | 1 |  <- 标记第1列(索引为1)需要置0
+---+---+---+
| 0 | 0 | 1 |  <- 标记第1行(索引为1)需要置0
+---+---+---+
| 1 | 1 | 1 |
+---+---+---+

根据标记处理行(第1行):

+---+---+---+
| 1 | 0 | 1 |
+---+---+---+
| 0 | 0 | 0 |  <- 根据第一列标记将第1行全部置0
+---+---+---+
| 1 | 1 | 1 |
+---+---+---+

根据标记处理列(第1列):

+---+---+---+
| 1 | 0 | 1 |
+---+---+---+
| 0 | 0 | 0 |
+---+---+---+
| 1 | 0 | 1 |  <- 根据第一行标记将第1列全部置0
+---+---+---+

最终结果:

+---+---+---+
| 1 | 0 | 1 |
+---+---+---+
| 0 | 0 | 0 |
+---+---+---+
| 1 | 0 | 1 |
+---+---+---+

示例 2

matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]]

初始状态:

+---+---+---+---+
| 0 | 1 | 2 | 0 |
+---+---+---+---+
| 3 | 4 | 5 | 2 |
+---+---+---+---+
| 1 | 3 | 1 | 5 |
+---+---+---+---+

检查第一行和第一列:

+---+---+---+---+  firstRowHasZero = true
| 0 | 1 | 2 | 0 |  firstColHasZero = true
+---+---+---+---+
| 3 | 4 | 5 | 2 |
+---+---+---+---+
| 1 | 3 | 1 | 5 |
+---+---+---+---+

标记阶段(处理内部元素):

+---+---+---+---+
| 0 | 1 | 2 | 0 |  第一行和第一列保留
+---+---+---+---+
| 3 | 4 | 5 | 0 |  matrix[1][3]=0,标记第1行和第3列
+---+---+---+---+
| 1 | 3 | 1 | 5 |
+---+---+---+---+

根据标记处理行和列:

+---+---+---+---+
| 0 | 1 | 2 | 0 |
+---+---+---+---+
| 3 | 4 | 5 | 0 |  没有内部行列需要置0
+---+---+---+---+
| 1 | 3 | 1 | 0 |  <- 处理第3列(因为matrix[0][3]=0)
+---+---+---+---+

处理第一行和第一列:

+---+---+---+---+
| 0 | 0 | 0 | 0 |  <- 第一行全部置0(因为firstRowHasZero=true)
+---+---+---+---+
| 0 | 4 | 5 | 0 |  
+---+---+---+---+  <- 第一列全部置0(因为firstColHasZero=true)
| 0 | 3 | 1 | 0 |
+---+---+---+---+

最终结果:

+---+---+---+---+
| 0 | 0 | 0 | 0 |
+---+---+---+---+
| 0 | 4 | 5 | 0 |
+---+---+---+---+
| 0 | 3 | 1 | 0 |
+---+---+---+---+

这个算法的巧妙之处在于利用矩阵的第一行和第一列作为标记空间,将空间复杂度从O(m+n)优化到了O(1),同时通过两个布尔变量记录第一行和第一列的原始状态,避免了信息丢失。整个过程只需要几次线性扫描,时间复杂度为O(mn)。

考查知识点

  • 空间复杂度优化:从直观的O(mn)优化到O(m+n),最终达到O(1)的常数空间复杂度

  • 就地算法(In-place Algorithm) :不使用额外空间,直接在原数据结构上操作

  • 矩阵操作:对二维数组的行列进行操作的能力

  • 标记技术:使用现有数据结构的部分作为标记,避免额外空间

  • 边界条件处理:对第一行和第一列的特殊处理

  • 编程技巧:使用变量存储状态而不是额外数组