LeetCode HOT 100 —— 矩阵置零(多种解法详解)

题目描述

LeetCode 73. 矩阵置零

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

示例 1:

text

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

示例 2:

text

复制代码
输入: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(mn) 的额外空间,但这并不是一个好的解决方案。

  • 一个简单的改进方案是使用 O(m + n) 的额外空间,但这仍然不是最好的解决方案。

  • 你能想出一个仅使用常量空间的解决方案吗?


解题思路

这道题最大的坑点在于:如果在遍历过程中直接修改矩阵,新赋值的0会影响后续判断,导致整个矩阵全变成0。因此,我们需要采取"先标记,后修改"的策略。

下面介绍三种解法,从最容易想到到最节省空间,逐步优化。


解法一:标记数组(O(m+n) 空间)

思路

这是最直观的解法。我们创建两个布尔数组:rowcol,分别记录每一行和每一列是否需要被置零。

  1. 第一次遍历矩阵,如果 matrix[i][j] == 0,则将 row[i]col[j] 都标记为 true

  2. 第二次遍历矩阵,如果 row[i]col[j]true,则将当前位置置为 0。

代码实现

java

复制代码
class Solution {
    public void setZeroes(int[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        boolean[] row = new boolean[m];
        boolean[] col = new boolean[n];
        
        // 第一次遍历:记录哪些行和列需要置零
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (matrix[i][j] == 0) {
                    row[i] = true;
                    col[j] = true;
                }
            }
        }
        
        // 第二次遍历:根据标记置零
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (row[i] || col[j]) {
                    matrix[i][j] = 0;
                }
            }
        }
    }
}

复杂度分析

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

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


解法二:两个标记变量(O(1) 空间,优化版)

思路

能不能不使用额外数组?我们可以利用矩阵的第一行和第一列来存储标记信息。

具体做法:

  1. 用两个布尔变量 rowZerocolZero 记录第一行和第一列本身是否原本包含 0。

  2. (1,1) 位置开始遍历,如果 matrix[i][j] == 0,则将 matrix[i][0]matrix[0][j] 标记为 0。

  3. 第二次遍历(从 (1,1) 开始),如果 matrix[i][0] == 0matrix[0][j] == 0,则将当前位置置为 0。

  4. 最后根据 rowZerocolZero 决定是否将第一行和第一列置为 0。

为什么要单独记录第一行和第一列?

因为第一行和第一列同时承担了"存储标记"的角色,它们原本的状态会被覆盖。如果不提前记录,我们就不知道第一行和第一列本身是否需要被置零。

代码实现

java

复制代码
class Solution {
    public void setZeroes(int[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        boolean rowZero = false, colZero = false;
        
        // 1. 记录第一行和第一列是否原本包含 0
        for (int i = 0; i < m; i++) {
            if (matrix[i][0] == 0) colZero = true;
        }
        for (int j = 0; j < n; j++) {
            if (matrix[0][j] == 0) rowZero = true;
        }
        
        // 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. 根据标记置零(从 (1,1) 开始)
        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 (rowZero) {
            for (int j = 0; j < n; j++) matrix[0][j] = 0;
        }
        if (colZero) {
            for (int i = 0; i < m; i++) matrix[i][0] = 0;
        }
    }
}

复杂度分析

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

  • 空间复杂度:O(1)


解法三:一个标记变量(极简版)

思路

能否进一步优化,只用一个标记变量?答案是肯定的。

我们只用 colZero 记录第一列是否包含 0,而第一行是否包含 0 的信息可以存储在 matrix[0][0] 中。

但这里有一个关键细节 :置零操作需要从最后一行开始倒序进行 。因为 matrix[0][0] 既代表第一行的状态,也代表第一列的状态,如果正序处理,第一行的状态可能会被提前覆盖。

代码实现

java

复制代码
class Solution {
    public void setZeroes(int[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        boolean colZero = false;
        
        for (int i = 0; i < m; i++) {
            // 检查第一列是否有 0
            if (matrix[i][0] == 0) colZero = true;
            // 检查其他列,并将标记存储在第一行和第一列
            for (int j = 1; j < n; j++) {
                if (matrix[i][j] == 0) {
                    matrix[i][0] = 0;
                    matrix[0][j] = 0;
                }
            }
        }
        
        // 倒序遍历,避免标记被覆盖
        for (int i = m - 1; i >= 0; i--) {
            for (int j = 1; j < n; j++) {
                if (matrix[i][0] == 0 || matrix[0][j] == 0) {
                    matrix[i][j] = 0;
                }
            }
            if (colZero) {
                matrix[i][0] = 0;
            }
        }
    }
}

复杂度分析

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

  • 空间复杂度:O(1)


三种解法对比

解法 空间复杂度 优点 缺点
标记数组 O(m+n) 简单直观,不易出错 需要额外空间
两个标记变量 O(1) 空间最优,思路清晰 需要单独记录第一行/列
一个标记变量 O(1) 最节省空间 倒序遍历容易写错,可读性略差

推荐:面试时建议先给出解法一,然后主动优化到解法二,这样能展示你的优化思维。解法三可以作为扩展了解,实际工程中更推荐解法二,因为可读性更好。


踩坑提醒

  1. 不要边遍历边置零:这是最常见的错误。如果遇到 0 就立即将整行整列置零,后面遍历到的 0 可能是刚被赋值的,会导致错误地多置零。

  2. 注意第一行和第一列的特殊性:当使用原地标记法时,一定要先记录第一行和第一列的原始状态,否则它们的状态会丢失。

  3. 考虑边界情况:如果矩阵只有一行或一列,确保代码仍然能正确运行。


总结

这道题的核心思想是 "标记后置" ------ 先记录哪些行/列需要置零,再进行修改。从 O(mn) 到 O(m+n) 再到 O(1) 的空间优化过程,是一道非常经典的考察原地算法的题目。

掌握这三种解法,不仅能帮你通过这道题,更能让你对矩阵操作类的题目有更深的理解。


题目链接 & 题解链接


如果这篇文章对你有帮助,欢迎点赞、收藏、关注!更多 LeetCode HOT 100 题解持续更新中~

相关推荐
葫三生2 小时前
《论三生原理》系列:文化自信、知识范式重构与科技自主创新的思想运动源头?
大数据·人工智能·科技·深度学习·算法·重构·transformer
YuanDaima20482 小时前
矩阵基础原理与题目说明
人工智能·笔记·python·学习·线性代数·矩阵
Q741_1472 小时前
每日一题 力扣 3761. 镜像对之间最小绝对距离 哈希表 数组 C++ 题解
c++·算法·leetcode·哈希算法·散列表
John.Lewis2 小时前
C++加餐课-哈希:扩展学习(2)布隆过滤器
c++·算法·哈希算法
我真不是小鱼2 小时前
cpp刷题打卡记录29——矩阵置零 & 旋转图像 & 除了自身以外数组的乘积
数据结构·c++·算法·leetcode·矩阵
澈2072 小时前
快速排序与希尔排序实战解析
数据结构·算法·排序算法
帅小伙―苏2 小时前
力扣128.最长连续序列
算法·leetcode
tankeven2 小时前
HJ180 游游的最长稳定子数组
c++·算法
良木生香3 小时前
【C++初阶】:泛型编程的代表作---C++初阶模板
c语言·开发语言·数据结构·c++·算法