🧩 LeetCode 73. 矩阵置零:从暴力 Set 到 O(1) 原地算法(附完整解析)
题目要求 :给定一个
m x n的整数矩阵,若某个元素为0,则将其所在整行和整列 全部置为0。
关键限制 :必须 原地修改(in-place) ,不能返回新数组。
在刷这道题时,我一开始写了暴力解法,后来尝试优化空间,却踩了几个经典坑。今天就用我自己的代码 ,带大家一步步理解如何从 O(m+n) 空间优化到 O(1),以及为什么第一行和第一列要特殊处理。

🔥 第一步:暴力解法(清晰但非最优)
最直观的想法是:
- 遍历矩阵,记录所有含
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 不只是刷题,更是思维训练。
共勉!