LeetCode 73. 矩阵置零:原地算法实现与优化解析

今天来拆解 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,避免后续标记覆盖原始信息。

优化思路拆解

  1. 先单独记录「第一行是否有 0」和「第一列是否有 0」(用两个布尔变量);

  2. 利用第一行标记所有列是否需要置零,利用第一列标记所有行是否需要置零(从索引 1 开始遍历,避免覆盖第一步记录的信息);

  3. 根据第一行和第一列的标记,先将矩阵除第一行、第一列以外的元素置零;

  4. 最后,根据第一步记录的布尔变量,处理第一行和第一列(如果原本有 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个坑,一定要注意:

  1. 忘记边界条件判断:比如传入空矩阵、只有一行/一列的矩阵,导致代码报错;

  2. 直接遍历置零,不做标记:导致覆盖原始 0,后续判断出错,比如误将自己置的 0 当作原始 0;

  3. 优化版中,忘记先记录第一行/列的原始 0:直接用第一行/列做标记,覆盖了本身的 0,导致最后第一行/列无法正确置零。

六、总结

LeetCode 73. 矩阵置零的核心是「原地算法」和「标记逻辑」:

  • 基础版适合新手入门,用辅助数组记录标记,逻辑清晰,代价是额外的内存空间;

  • 优化版是面试中的优选,利用矩阵自身的第一行/列做标记,将空间复杂度降至 O(1),核心是「先记录、再标记、最后处理边界」;

  • 时间复杂度无法进一步优化,最优就是 O(m × n),因为必须遍历所有元素。

相关推荐
天赐学c语言2 小时前
2.1 - 反转字符串中的单词 && 每个进程的内存里包含什么
c++·算法·leecode
晚霞的不甘2 小时前
Flutter for OpenHarmony 实现动态天气与空气质量仪表盘:从 UI 到动画的完整解析
前端·flutter·ui·前端框架·交互
程序员泠零澪回家种桔子2 小时前
OpenManus开源自主规划智能体解析
人工智能·后端·算法
~小仙女~2 小时前
组件的二次封装
前端·javascript·vue.js
请注意这个女生叫小美2 小时前
C语言 实例20 25
c语言·开发语言·算法
好学且牛逼的马2 小时前
【Hot100|22-LeetCode 206. 反转链表 - 完整解法详解】
算法·leetcode·矩阵
hans汉斯2 小时前
国产生成式人工智能解决物理问题能力研究——以“智谱AI”、“讯飞星火认知大模型”、“天工”、“360智脑”、“文心一言”为例
大数据·人工智能·算法·aigc·文心一言·汉斯出版社·天工
这是个栗子2 小时前
AI辅助编程(一) - ChatGPT
前端·vue.js·人工智能·chatgpt
2501_944448002 小时前
Flutter for OpenHarmony衣橱管家App实战:预算管理实现
前端·javascript·flutter