今天来拆解 LeetCode 经典中等题------73. 矩阵置零。这道题的核心考点是「原地算法」的实现,以及空间复杂度的优化,新手容易在"不额外开辟矩阵"和"避免标记覆盖"上踩坑,今天咱们从基础实现到优化思路,一步步讲明白。
一、题目核心要求
题目很简洁:给定一个 m x n 的二维矩阵,如果某个元素的值为 0,就将该元素所在的整行 和整列的所有元素都设为 0。关键约束是「原地修改」,也就是不能额外创建一个和原矩阵大小一致的新矩阵,只能在原矩阵上操作。
举个简单例子帮助理解:
输入矩阵:
javascript
[
[1, 1, 1],
[1, 0, 1],
[1, 1, 1]
]
输出矩阵(原地修改后):
javascript
[
[1, 0, 1],
[0, 0, 0],
[1, 0, 1]
]
二、原始实现代码详解(基础版)
先看咱们手上的这份代码,这是最直观、最容易想到的基础实现,核心思路是「先标记,后置零」,咱们逐行拆解,搞懂每一步的作用。
typescript
/**
Do not return anything, modify matrix in-place instead.
*/
function setZeroes(matrix: number[][]): void {
// 1. 边界条件判断(防御性编程)
if (!matrix || !matrix.length || !matrix[0] || !matrix[0].length) return;
// 2. 定义变量,获取矩阵行数和列数
const row = matrix.length;
const col = matrix[0].length;
// 3. 创建两个辅助数组,用于标记需要置零的行和列
const zeroRow = new Array(row);
const zeroCol = new Array(col);
// 4. 第一次遍历:标记所有包含0的行和列
for (let m = 0; m < row; m++) {
for (let n = 0; n < col; n++) {
if (matrix[m][n] === 0) {
zeroRow[m] = 0; // 标记第m行需要置零
zeroCol[n] = 0; // 标记第n列需要置零
}
}
}
// 5. 第二次遍历:根据标记,将对应行和列置零
for (let m = 0; m < row; m++) {
for (let n = 0; n < col; n++) {
if (zeroRow[m] === 0 || zeroCol[n] === 0) {
matrix[m][n] = 0;
}
}
}
};
关键步骤拆解
1. 边界条件判断
if (!matrix || !matrix.length || !matrix[0] || !matrix[0].length) return;
这一步是刷题必备的「防御性编程」,避免空矩阵导致后续遍历报错。主要处理4种异常情况:
-
matrix 本身是 null/undefined(比如传入空值);
-
matrix 是一个空数组(没有任何行,比如 []);
-
matrix 的第一行是 null/undefined(比如 [null, [1,2]]);
-
matrix 有行,但每行都是空数组(比如 [[], [], []])。
只要满足任一情况,直接返回,不执行后续逻辑。
2. 辅助数组的作用(核心亮点)
为什么要创建 zeroRow 和 zeroCol 两个辅助数组?
如果直接遍历矩阵,遇到 0 就立刻将所在行和列置零,会导致「后续判断出错」------ 你无法区分哪些 0 是原始矩阵的,哪些是你刚刚置的 0,比如:
假设矩阵中有一个 0 在 (1,1),你立刻将第1行、第1列置零,后续遍历到 (1,2) 时,发现它是 0(你刚置的),就会错误地将第2列也置零,导致结果出错。
所以,辅助数组的核心作用是「暂存标记」:先遍历一遍,把所有需要置零的行和列记下来,再统一置零,避免覆盖原始 0 的信息。
3. 两次遍历的逻辑
第一次遍历(标记):遍历矩阵每一个元素,只要是 0,就把对应行的 zeroRow 设为 0,对应列的 zeroCol 设为 0(这里不修改原矩阵);
第二次遍历(置零):再次遍历矩阵,只要当前元素所在的行被标记(zeroRow[m] === 0),或者所在的列被标记(zeroCol[n] === 0),就将该元素置为 0。
基础版代码评价
✅ 优点:逻辑简单、易懂,不容易出错,适合新手入门;
❌ 缺点:空间复杂度较高 ------ 两个辅助数组的大小分别是 m 和 n,所以空间复杂度是 O(m + n),没有充分利用「原地算法」的约束,还有优化空间。
三、进阶优化:空间复杂度 O(1)(原地标记)
题目要求「原地修改」,核心优化方向就是「去掉辅助数组」,用原矩阵本身的空间来做标记。最巧妙的思路是:利用矩阵的第一行和第一列,充当辅助数组。
为什么可以用第一行和第一列?因为第一行和第一列的元素,本身也是矩阵的一部分,我们可以用它们来标记"对应列"和"对应行"是否需要置零,唯一需要注意的是:先记录第一行和第一列本身是否有 0,避免后续标记覆盖原始信息。
优化思路拆解
-
先单独记录「第一行是否有 0」和「第一列是否有 0」(用两个布尔变量);
-
利用第一行标记所有列是否需要置零,利用第一列标记所有行是否需要置零(从索引 1 开始遍历,避免覆盖第一步记录的信息);
-
根据第一行和第一列的标记,先将矩阵除第一行、第一列以外的元素置零;
-
最后,根据第一步记录的布尔变量,处理第一行和第一列(如果原本有 0,就全部置零)。
优化后的完整代码(TypeScript)
typescript
/**
Do not return anything, modify matrix in-place instead.
*/
function setZeroes(matrix: number[][]): void {
if (!matrix || !matrix.length || !matrix[0] || !matrix[0].length) return;
const row = matrix.length;
const col = matrix[0].length;
// 步骤1:记录第一行、第一列是否原本有0(关键:避免后续标记覆盖)
let firstRowHasZero = false;
let firstColHasZero = false;
// 检查第一行是否有0
for (let n = 0; n < col; n++) {
if (matrix[0][n] === 0) {
firstRowHasZero = true;
break; // 找到一个0即可,无需继续遍历
}
}
// 检查第一列是否有0
for (let m = 0; m < row; m++) {
if (matrix[m][0] === 0) {
firstColHasZero = true;
break;
}
}
// 步骤2:利用第一行、第一列标记其他行/列(从索引1开始)
for (let m = 1; m < row; m++) {
for (let n = 1; n < col; n++) {
if (matrix[m][n] === 0) {
matrix[m][0] = 0; // 用第一列标记第m行需要置零
matrix[0][n] = 0; // 用第一行标记第n列需要置零
}
}
}
// 步骤3:根据第一列的标记,置零对应行(除第一行)
for (let m = 1; m < row; m++) {
if (matrix[m][0] === 0) {
for (let n = 1; n < col; n++) {
matrix[m][n] = 0;
}
}
}
// 步骤4:根据第一行的标记,置零对应列(除第一列)
for (let n = 1; n < col; n++) {
if (matrix[0][n] === 0) {
for (let m = 1; m < row; m++) {
matrix[m][n] = 0;
}
}
}
// 步骤5:处理第一行和第一列(根据最开始的标记)
if (firstRowHasZero) {
for (let n = 0; n < col; n++) {
matrix[0][n] = 0;
}
}
if (firstColHasZero) {
for (let m = 0; m < row; m++) {
matrix[m][0] = 0;
}
}
};
四、两种方案对比(清晰版)
| 实现方案 | 时间复杂度 | 空间复杂度 | 核心特点 |
|---|---|---|---|
| 基础版(辅助数组) | O(m × n) | O(m + n) | 逻辑简单,易理解,适合新手,额外占用内存 |
| 优化版(原地标记) | O(m × n) | O(1) | 空间最优,贴合题目原地要求,逻辑稍复杂,需注意处理顺序 |
补充说明:两种方案的时间复杂度都是 O(m × n),这是最优时间复杂度------ 因为我们必须遍历矩阵中所有元素至少一次(既要找到所有原始 0,也要修改所有需要置零的元素),无法再进一步优化。
五、刷题易错点总结(避坑指南)
这道题看似简单,但新手很容易踩以下3个坑,一定要注意:
-
忘记边界条件判断:比如传入空矩阵、只有一行/一列的矩阵,导致代码报错;
-
直接遍历置零,不做标记:导致覆盖原始 0,后续判断出错,比如误将自己置的 0 当作原始 0;
-
优化版中,忘记先记录第一行/列的原始 0:直接用第一行/列做标记,覆盖了本身的 0,导致最后第一行/列无法正确置零。
六、总结
LeetCode 73. 矩阵置零的核心是「原地算法」和「标记逻辑」:
-
基础版适合新手入门,用辅助数组记录标记,逻辑清晰,代价是额外的内存空间;
-
优化版是面试中的优选,利用矩阵自身的第一行/列做标记,将空间复杂度降至 O(1),核心是「先记录、再标记、最后处理边界」;
-
时间复杂度无法进一步优化,最优就是 O(m × n),因为必须遍历所有元素。