🔄 题目链接: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) 空间)
这是最直观的想法:用两个布尔数组 row 和 col 来记录每一行、每一列是否出现过 0。
✅ 思路步骤:
- 第一次遍历:扫描整个矩阵,若
matrix[i][j] == 0,则设置row[i] = true,col[j] = true。 - 第二次遍历:根据
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: 第一行是否原本包含 0flag_col0: 第一列是否原本包含 0
✅ 解决流程:
- 先预处理
flag_row0和flag_col0 - 从
(1,1)开始遍历,把matrix[i][j] == 0的信息写入matrix[i][0]和matrix[0][j] - 再次遍历,用
matrix[i][0]和matrix[0][j]更新非首行/列的元素 - 最后用
flag_row0和flag_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] 同时被用于第一行和第一列的标记,所以我们要避免冲突。
👉 解决方案:从最后一行开始倒序遍历,这样我们在更新第一行时,不会影响到后面还未处理的列标记。
✅ 流程调整:
- 仍用
flag_col0记录第一列是否有0。 - 用
matrix[0][0]代替flag_row0。 - 从下往上、从右往左遍历,确保不会提前覆盖重要信息。
- 在最后一步一次性更新第一列(基于
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. 螺旋矩阵 —— 边界控制与模拟艺术 🌀