【LeetCode Hot100 刷题日记(18/100)】73. 矩阵置零 ——数组、原地算法、空间优化、矩阵操作 🔄

42 阅读9分钟

🔄 题目链接:leetcode.cn/problems/se…
🔍 难度:中等 | 🏷️ 标签:数组、原地算法、空间优化、矩阵操作
⏱️ 目标时间复杂度:O(mn)
💾 空间复杂度:O(1)(最优解)


🌟 本期我们深入解析 LeetCode 第73题《矩阵置零》,这道题看似简单,实则蕴含了 “如何在不使用额外空间的情况下进行信息记录” 的核心思想。它不仅是面试中的常客,更是考察你对 原地算法(in-place algorithm)理解深度 的绝佳题目。

✅ 本篇将从 暴力解法 → O(m+n) 空间优化 → O(1) 常数空间终极优化 逐步展开,带你掌握 标记技巧、双指针思维、矩阵遍历策略 等关键能力,助你在面试中轻松应对类似问题!


📊 题目分析

给定一个 m x n 的二维整数矩阵 matrix,如果某个元素为 0,则将其所在行和列的所有元素都设为 0

📌 要求:使用原地算法(in-place),即不能创建新的矩阵来存储结果。

✅ 示例回顾:

输入:matrix = [[1,1,1],[1,0,1],[1,1,1]]
输出:[[1,0,1],[0,0,0],[1,0,1]]
输入: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]]

❗ 关键点:

  • 不能用 vector<vector<int>> res 这类额外结构。
  • 一旦某个位置是 0,会影响其整行整列。
  • 直接修改会破坏原始信息,导致后续判断出错(比如第 i 行有 0,但我们先改了它,再看其他列就找不到原始的 0 了)。

🔥 所以我们需要一种机制:先记录哪些行/列需要置零,再统一处理


🔍 核心算法及代码讲解

本题的核心在于:如何在有限的空间内高效地记录“哪些行和列需要被清零”?

我们从三种方案递进式讲解:


🧩 方法一:使用标记数组(O(m+n) 空间)

这是最直观的想法:用两个布尔数组 rowcol 来记录每一行、每一列是否出现过 0

✅ 思路步骤:

  1. 第一次遍历:扫描整个矩阵,若 matrix[i][j] == 0,则设置 row[i] = true, col[j] = true
  2. 第二次遍历:根据 row[i]col[j] 是否为真,将对应位置设为 0

🧠 优点:

  • 思路清晰,易于实现。
  • 时间复杂度优秀。

🧠 缺点:

  • 使用了额外的 O(m + n) 空间,不符合“常数空间”的要求。

💡 代码(保留原内容并添加注释):

class Solution {
public:
    void setZeroes(vector<vector<int>>& matrix) {
        int m = matrix.size();           // 行数
        int n = matrix[0].size();        // 列数
        
        vector<int> row(m, false);       // 记录每行是否有0
        vector<int> col(n, false);       // 记录每列是否有0
        
        // 第一次遍历:标记所有含0的行和列
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (matrix[i][j] == 0) {   // 发现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(mn)
✅ 空间复杂度:O(m + n)


🧩 方法二:使用两个标记变量(O(1) 空间)

👉 我们可以利用 矩阵的第一行和第一列 作为“标记数组”,但要小心:它们本身可能原本就有 0,会被误改!

所以我们引入两个额外变量:

  • flag_row0: 第一行是否原本包含 0
  • flag_col0: 第一列是否原本包含 0

✅ 解决流程:

  1. 先预处理 flag_row0flag_col0
  2. (1,1) 开始遍历,把 matrix[i][j] == 0 的信息写入 matrix[i][0]matrix[0][j]
  3. 再次遍历,用 matrix[i][0]matrix[0][j] 更新非首行/列的元素
  4. 最后用 flag_row0flag_col0 更新第一行和第一列

💡 代码(保留原内容并添加详细注释):

class Solution {
public:
    void setZeroes(vector<vector<int>>& matrix) {
        int m = matrix.size();
        int n = matrix[0].size();
        
        bool flag_col0 = false;  // 第一列是否原本有0
        bool flag_row0 = false;  // 第一行是否原本有0
        
        // 预处理:检查第一列和第一行是否有0
        for (int i = 0; i < m; i++) {
            if (matrix[i][0] == 0) {
                flag_col0 = true;
            }
        }
        for (int j = 0; j < n; j++) {
            if (matrix[0][j] == 0) {
                flag_row0 = true;
            }
        }
        
        // 从(1,1)开始,把0的信息写入第一行和第一列
        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列需要清零
                }
            }
        }
        
        // 从(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;
                }
            }
        }
        
        // 最后处理第一行和第一列
        if (flag_col0) {
            for (int i = 0; i < m; i++) {
                matrix[i][0] = 0;
            }
        }
        if (flag_row0) {
            for (int j = 0; j < n; j++) {
                matrix[0][j] = 0;
            }
        }
    }
};

✅ 时间复杂度:O(mn)
✅ 空间复杂度:O(1)(仅用了两个布尔变量)


🧩 方法三:使用一个标记变量(极致优化)

进一步优化:我们可以只用一个变量 flag_col0,因为 第一行的第一个元素 matrix[0][0] 可以同时表示第一行是否有0

但是注意:matrix[0][0] 同时被用于第一行和第一列的标记,所以我们要避免冲突。

👉 解决方案:从最后一行开始倒序遍历,这样我们在更新第一行时,不会影响到后面还未处理的列标记。

✅ 流程调整:

  1. 仍用 flag_col0 记录第一列是否有0。
  2. matrix[0][0] 代替 flag_row0
  3. 从下往上、从右往左遍历,确保不会提前覆盖重要信息。
  4. 在最后一步一次性更新第一列(基于 flag_col0),第一行(基于 matrix[0][0])。

💡 代码(保留原内容并添加注释):

class Solution {
public:
    void setZeroes(vector<vector<int>>& matrix) {
        int m = matrix.size();
        int n = matrix[0].size();
        
        bool flag_col0 = false;  // 第一列是否原本有0
        
        // 检查第一列是否有0
        for (int i = 0; i < m; i++) {
            if (matrix[i][0] == 0) {
                flag_col0 = true;
            }
        }
        
        // 从第1行开始,用第1列和第1行做标记
        // 注意:这里我们不再单独维护flag_row0,而是用matrix[0][0]表示
        for (int i = 0; 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 = 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 (flag_col0) {
                matrix[i][0] = 0;
            }
        }
    }
};

✅ 时间复杂度:O(mn)
✅ 空间复杂度:O(1)
✅ 更加紧凑,适合面试展示高阶思维!


🧠 解题思路(分步详解)

我们总结一下解决此类“矩阵置零 / 区域扩散”问题的一般性思考路径:

🔹 步骤 1:识别问题本质

  • 一个元素为 0 → 影响整行整列
  • 顺序操作会导致信息丢失 → 必须先记录再执行

🔹 步骤 2:选择信息记录方式

方案空间优点缺点
额外数组O(m+n)清晰易懂不满足常数空间
第一行/列作标记 + 两个变量O(1)平衡性好需要额外状态管理
单个变量 + 倒序遍历O(1)极致优化容易出错,需谨慎

🔹 步骤 3:设计遍历顺序

  • 通常先 收集信息(如遍历找0)
  • 应用规则(如置零)
  • 若共享空间,避免覆盖 是关键 → 倒序遍历是常用技巧!

🔹 步骤 4:边界处理

  • 第一行和第一列是特殊区域,必须单独处理
  • 使用辅助变量保存原始状态,防止误判

📈 算法分析

指标方法一方法二方法三
时间复杂度O(mn)O(mn)O(mn)
空间复杂度O(m+n)O(1)O(1)
实现难度⭐⭐⭐⭐⭐⭐⭐⭐⭐
面试推荐度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

面试建议

  • 如果时间紧张,方法一即可,表达清楚逻辑。
  • 如果追求优雅,方法二更佳,体现工程思维。
  • 如果想展现“极致优化”,方法三是加分项,但要注意解释清楚为什么倒序。

💻 代码(完整测试版)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

// 方法三:使用一个标记变量(最优解)
class Solution {
public:
    void setZeroes(vector<vector<int>>& matrix) {
        int m = matrix.size();
        int n = matrix[0].size();
        
        bool flag_col0 = false;  // 第一列是否原本有0
        
        // 检查第一列是否有0
        for (int i = 0; i < m; i++) {
            if (matrix[i][0] == 0) {
                flag_col0 = true;
            }
        }
        
        // 从第1行开始,用第1列和第1行做标记
        for (int i = 0; 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 = 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 (flag_col0) {
                matrix[i][0] = 0;
            }
        }
    }
};

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    // 测试用例1
    vector<vector<int>> matrix1 = {{1,1,1},{1,0,1},{1,1,1}};
    Solution().setZeroes(matrix1);
    cout << "Test 1:\n";
    for (auto& row : matrix1) {
        for (int x : row) cout << x << " ";
        cout << "\n";
    }

    // 测试用例2
    vector<vector<int>> matrix2 = {{0,1,2,0},{3,4,5,2},{1,3,1,5}};
    Solution().setZeroes(matrix2);
    cout << "Test 2:\n";
    for (auto& row : matrix2) {
        for (int x : row) cout << x << " ";
        cout << "\n";
    }

    return 0;
}

✅ 输出应为:

Test 1:
1 0 1 
0 0 0 
1 0 1 
Test 2:
0 0 0 0 
0 4 5 0 
0 3 1 0 

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪


📣 下一期预告:LeetCode 热题 100 第54题 —— 螺旋矩阵(中等)

🔹 题目:给定一个 m x n 的矩阵,按顺时针螺旋顺序返回所有元素。

🔹 核心思路:模拟螺旋过程,使用四个边界(上、下、左、右)控制方向,逐层剥开。

🔹 考点:模拟、边界控制、循环逻辑、二维数组遍历。

🔹 难度:中等,常见于大厂面试,尤其是腾讯、字节、阿里等。

🔹 提示:不要用递归,容易越界;要用“边界收缩法”!

💡 提示:想象你在一个矩形里画圈,一圈一圈缩小范围,直到中心。


📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!

🧠 拓展思考

  • 如果是逆时针螺旋呢?
  • 如果是之字形遍历呢?
  • 如何扩展到三维矩阵?

这些都能帮助你在面试中脱颖而出!


🔁 系列持续更新中……
👉 下一篇:【LeetCode Hot100 刷题日记(54/100)】54. 螺旋矩阵 —— 边界控制与模拟艺术 🌀