LeetCode 第73题:矩阵置零
题目描述
给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。
难度
中等
题目链接
示例
示例 1:
输入:matrix = [[1,1,1],[1,0,1],[1,1,1]]
输出:[[1,0,1],[0,0,0],[1,0,1]]
示例 2:
输入: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]]
提示
m == matrix.lengthn == matrix[0].length1 <= m, n <= 200-2^31 <= matrix[i][j] <= 2^31 - 1
解题思路
使用标记数组
最直观的解法是使用两个标记数组,分别记录需要置零的行和列。
关键点:
- 使用两个标记数组
rowZero和colZero记录哪些行和列需要置零 - 遍历矩阵,标记含有0的行和列
- 再次遍历矩阵,根据标记数组置零
原地标记法
为了实现 O(1) 的空间复杂度,我们可以使用矩阵的第一行和第一列作为标记数组。
关键点:
- 使用两个变量
firstRowZero和firstColZero记录第一行和第一列是否需要置零 - 使用矩阵的第一行和第一列作为标记数组
- 从第二行第二列开始遍历矩阵,如果元素为0,则将对应的第一行和第一列的元素置为0
- 根据第一行和第一列的标记,将对应的行和列置零
- 最后根据
firstRowZero和firstColZero处理第一行和第一列
图解思路
算法步骤分析表
以 matrix = [[1,1,1],[1,0,1],[1,1,1]] 为例:
| 步骤 | 操作 | 矩阵状态 | 说明 |
|---|---|---|---|
| 初始 | 输入 | [[1,1,1],[1,0,1],[1,1,1]] | 原始矩阵 |
| 检查 | 第一行列 | firstRowZero=false, firstColZero=false | 第一行和第一列没有0 |
| 标记 | (1,1)=0 | matrix[0][1]=0, matrix[1][0]=0 | 在第一行第一列标记 |
| 置零 | 根据标记 | [[1,0,1],[0,0,0],[1,0,1]] | 根据第一行和第一列的标记置零 |
状态/情况分析表
| 情况 | 输入 | 输出 | 说明 |
|---|---|---|---|
| 无零矩阵 | [[1,1],[1,1]] | [[1,1],[1,1]] | 矩阵不变 |
| 第一行有零 | [[0,1],[1,1]] | [[0,0],[0,1]] | 第一行和第一列置零 |
| 中间有零 | [[1,1],[1,0]] | [[1,0],[0,0]] | 对应行列置零 |
| 全零矩阵 | [[0,0],[0,0]] | [[0,0],[0,0]] | 矩阵不变 |
代码实现
C# 实现
public class Solution {
public void SetZeroes(int[][] matrix) {
int m = matrix.Length;
int n = matrix[0].Length;
// 记录第一行和第一列是否有0
bool firstRowZero = false;
bool firstColZero = false;
// 检查第一行是否有0
for (int j = 0; j < n; j++) {
if (matrix[0][j] == 0) {
firstRowZero = true;
break;
}
}
// 检查第一列是否有0
for (int i = 0; i < m; i++) {
if (matrix[i][0] == 0) {
firstColZero = true;
break;
}
}
// 使用第一行和第一列作为标记
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (matrix[i][j] == 0) {
matrix[i][0] = 0;
matrix[0][j] = 0;
}
}
}
// 根据第一行和第一列的标记,将对应的行和列置零
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 (firstRowZero) {
for (int j = 0; j < n; j++) {
matrix[0][j] = 0;
}
}
// 处理第一列
if (firstColZero) {
for (int i = 0; i < m; i++) {
matrix[i][0] = 0;
}
}
}
}
Python 实现
class Solution:
def setZeroes(self, matrix: List[List[int]]) -> None:
"""
Do not return anything, modify matrix in-place instead.
"""
m, n = len(matrix), len(matrix[0])
# 记录第一行和第一列是否有0
first_row_zero = False
first_col_zero = False
# 检查第一行是否有0
for j in range(n):
if matrix[0][j] == 0:
first_row_zero = True
break
# 检查第一列是否有0
for i in range(m):
if matrix[i][0] == 0:
first_col_zero = True
break
# 使用第一行和第一列作为标记
for i in range(1, m):
for j in range(1, n):
if matrix[i][j] == 0:
matrix[i][0] = 0
matrix[0][j] = 0
# 根据第一行和第一列的标记,将对应的行和列置零
for i in range(1, m):
for j in range(1, n):
if matrix[i][0] == 0 or matrix[0][j] == 0:
matrix[i][j] = 0
# 处理第一行
if first_row_zero:
for j in range(n):
matrix[0][j] = 0
# 处理第一列
if first_col_zero:
for i in range(m):
matrix[i][0] = 0
C++ 实现
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
int m = matrix.size();
int n = matrix[0].size();
// 记录第一行和第一列是否有0
bool firstRowZero = false;
bool firstColZero = false;
// 检查第一行是否有0
for (int j = 0; j < n; j++) {
if (matrix[0][j] == 0) {
firstRowZero = true;
break;
}
}
// 检查第一列是否有0
for (int i = 0; i < m; i++) {
if (matrix[i][0] == 0) {
firstColZero = true;
break;
}
}
// 使用第一行和第一列作为标记
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (matrix[i][j] == 0) {
matrix[i][0] = 0;
matrix[0][j] = 0;
}
}
}
// 根据第一行和第一列的标记,将对应的行和列置零
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 (firstRowZero) {
for (int j = 0; j < n; j++) {
matrix[0][j] = 0;
}
}
// 处理第一列
if (firstColZero) {
for (int i = 0; i < m; i++) {
matrix[i][0] = 0;
}
}
}
};
执行结果
- C# 执行用时:160 ms
- C# 内存消耗:44.2 MB
- Python 执行用时:44 ms
- Python 内存消耗:15.2 MB
- C++ 执行用时:12 ms
- C++ 内存消耗:13.2 MB
代码亮点
- 🎯 使用原地算法,空间复杂度为 O(1)
- 💡 巧妙利用矩阵的第一行和第一列作为标记数组
- 🔍 分步处理,逻辑清晰
- 🎨 处理边界情况(第一行和第一列)
常见错误分析
- 🚫 没有单独处理第一行和第一列
- 🚫 在标记过程中就开始置零,导致错误传播
- 🚫 忘记检查第一行和第一列是否有0
- 🚫 使用额外空间而不是原地算法
解法对比
| 解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 使用标记数组 | O(m*n) | O(m+n) | 直观易懂 | 需要额外空间 |
| 原地标记法 | O(m*n) | O(1) | 空间优化 | 实现复杂 |
| 使用特殊值标记 | O(m*n) | O(1) | 无需额外变量 | 依赖于数据范围 |
相关题目
- LeetCode 第54题:螺旋矩阵 - 中等
- LeetCode 第289题:生命游戏 - 中等
- LeetCode 第867题:转置矩阵 - 简单