不用 Set,只用两个布尔值:如何用标志位将矩阵置零的空间复杂度压到 O(1)


🧩 LeetCode 73. 矩阵置零:从暴力 Set 到 O(1) 原地算法(附完整解析)

题目要求 :给定一个 m x n 的整数矩阵,若某个元素为 0,则将其所在整行和整列 全部置为 0
关键限制 :必须 原地修改(in-place) ,不能返回新数组。

在刷这道题时,我一开始写了暴力解法,后来尝试优化空间,却踩了几个经典坑。今天就用我自己的代码 ,带大家一步步理解如何从 O(m+n) 空间优化到 O(1),以及为什么第一行和第一列要特殊处理

先看题目:73. 矩阵置零 - 力扣(LeetCode)


🔥 第一步:暴力解法(清晰但非最优)

最直观的想法是:

  • 遍历矩阵,记录所有含 0行号列号
  • 再遍历一次,把对应行列置零
js 复制代码
// 暴力法
let xZero = new Set();
let yZero = new Set();
for (let i = 0; i < x; i++) {
    for (let j = 0; j < y; j++) {
        if (matrix[i][j] === 0) {
            xZero.add(i);
            yZero.add(j);
        }
    }
}
for (let i = 0; i < x; i++) {
    for (let j = 0; j < y; j++) {
        if (xZero.has(i) || yZero.has(j))
            matrix[i][j] = 0;
    }
}

优点 :逻辑简单,一次 AC

缺点 :用了两个 Set,空间复杂度 O(m + n),不符合"极致原地"的要求

面试官可能会问:"能不能不用额外空间?"

于是,我开始思考:能不能把标记信息存在矩阵自己身上?


🚀 第二步:空间优化 ------ 利用第一行和第一列做标记

💡 核心思想

  • matrix[i][0] == 0 表示第 i 行需要置零
  • matrix[0][j] == 0 表示第 j 列需要置零

这样,我们就把"标记位"复用到了矩阵的第一列和第一行上,省下了 O(m+n) 的空间

但问题来了:

如果第一行或第一列本来就有 0,我们怎么知道这个 0 是"原始数据"还是"后来设置的标记"?

举个例子:

csharp 复制代码
[
  [1, 0, 1],
  [1, 1, 1],
  [1, 1, 1]
]
  • 这里的 matrix[0][1] = 0 是原始数据,意味着第 0 行和第 1 列都要清零
  • 但如果我们在后续过程中把它当作"标记",可能会漏掉对第 0 行的清零!

所以,必须提前记录第一行和第一列是否原本就有 0


✅ 我的优化代码-标记法

js 复制代码
/**
 * @param {number[][]} matrix
 * @return {void} Do not return anything, modify matrix in-place instead.
 */
var setZeroes = function(matrix) {
    let x = matrix.length;
    let y = matrix[0].length;

    // 用两个布尔值记录第一行/列是否原本有0
    let xZero = false;  // 实际表示:第一行是否有0
    let yZero = false;  // 实际表示:第一列是否有0

    // 检查第一列(所有 matrix[i][0])
    for (let i = 0; i < x; i++) {
        if (matrix[i][0] === 0) {
            yZero = true;
            break;
        }
    }

    // 检查第一行(所有 matrix[0][i])
    for (let i = 0; i < y; i++) {
        if (matrix[0][i] === 0) {
            xZero = true;
            break;
        }
    }

    // 用第一行/列作为标记区(从 [1][1] 开始)
    for (let i = 1; i < x; i++) {
        for (let j = 1; j < y; j++) {
            if (matrix[i][j] === 0) {
                matrix[i][0] = 0; // 标记该行
                matrix[0][j] = 0; // 标记该列
                // ⚠️ 注意:这里不能加 break!否则会漏掉同一行的多个0
            }
        }
    }

    // 根据标记置零(从 [1][1] 开始)
    for (let i = 1; i < x; i++) {
        for (let j = 1; j < y; j++) {
            if (matrix[0][j] === 0 || matrix[i][0] === 0) {
                matrix[i][j] = 0;
            }
        }
    }

    // 单独处理第一行
    if (xZero) {
        for (let i = 0; i < y; i++) {
            matrix[0][i] = 0;
        }
    }

    // 单独处理第一列
    if (yZero) {
        for (let i = 0; i < x; i++) {
            matrix[i][0] = 0;
        }
    }

    // 注意:题目要求 void,不要 return matrix(虽然JS不报错)
};

📌 说明 :虽然我用 xZero 表示"第一行是否有0"、yZero 表示"第一列是否有0",命名稍有反直觉,但逻辑是正确的。建议实际开发中改用 firstRowHasZero / firstColHasZero 提高可读性。


❗ 我踩过的关键 bug

在标记循环中加了 break

错误写法

ini 复制代码
if(matrix[i][j]===0) {
    matrix[0][j]=0;
    matrix[i][0]=0;
    break; // ← 错!
}

后果:一行中有多个 0 时,只标记第一个,后面的列不会被置零!

修复删除 break,让内层循环完整遍历。


🤔 为什么必须单独处理第一行和第一列?

这是本题的灵魂所在!

  • 我们借用第一行和第一列来存储"其他行列是否要清零"的信息。

  • 但它们自己也可能是"受害者"(原本就有 0)。

  • 如果不提前记录,最后无法判断:

    "这个 0 是用来标记别人的,还是自己需要被清零?"

因此,先扫描、再标记、最后统一处理,是唯一安全的做法。

就像借朋友的笔记本做笔记前,先拍照保存他原来写的内容,避免覆盖。


✅ 复杂度对比

方法 时间复杂度 空间复杂度 是否原地
暴力 Set O(mn) O(m + n)
原地标记法 O(mn) O(1)

💬 结语

通过这道题,我深刻体会到:

  • 原地算法的核心:巧妙复用已有空间,同时避免信息污染
  • 边界处理的重要性:第一行/列既是"工具"又是"数据",必须特殊对待
  • 细节决定成败 :一个多余的 break,就能让代码全盘皆错

希望我的踩坑经历能帮你少走弯路!如果你也有类似经历,欢迎在评论区分享~

LeetCode 不只是刷题,更是思维训练。

共勉!


相关推荐
有意义2 小时前
斐波那契数列:从递归到优化的完整指南
javascript·算法·面试
Mr.Jessy3 小时前
JavaScript高级:深入对象与内置构造函数
开发语言·前端·javascript·ecmascript
温宇飞3 小时前
深入理解 JavaScript 模块系统:CJS 与 ESM 的实现原理
javascript
charlie1145141913 小时前
编写INI Parser 测试完整指南 - 从零开始
开发语言·c++·笔记·学习·算法·单元测试·测试
mmz12073 小时前
前缀和问题2(c++)
c++·算法
TL滕3 小时前
从0开始学算法——第十六天(双指针算法)
数据结构·笔记·学习·算法
幸运小圣3 小时前
深入理解ref、reactive【Vue3工程级指南】
前端·javascript·vue.js
用户47949283569153 小时前
面试官最爱挖的坑:用户 Token 到底该存哪?
前端·javascript·面试
Heo3 小时前
Vue3.4中diff算法核心梳理
前端·javascript·面试