LeetCode 73. 矩阵置零 - 完整解法详解
一、问题理解
问题描述
给定一个 m × n 的矩阵,如果一个元素为 0,则将其所在行和列的所有元素都设为 0。请使用原地算法。
示例
python
输入: matrix = [[1,1,1],[1,0,1],[1,1,1]]
输出: [[1,0,1],[0,0,0],[1,0,1]]
解释: 位置(1,1)的元素为0,因此第一行和第二列都被设为0
输入: 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]]
要求
-
使用原地算法
-
时间复杂度:O(m × n)
-
空间复杂度:尽可能低
二、核心思路:标记与置零
基本思想
我们可以分两步解决这个问题:
-
标记阶段:找出哪些行和列需要被置零
-
置零阶段:根据标记将对应的行和列置零
优化方法
为了降低空间复杂度,我们可以:
-
使用矩阵的第一行和第一列作为标记空间
-
使用两个额外变量标记第一行和第一列是否需要置零
三、代码逐行解析
方法一:使用两个标记数组(空间复杂度 O(m+n))
Python 解法
python
from typing import List
class Solution:
def setZeroes(self, matrix: List[List[int]]) -> None:
"""
不要返回任何内容,原地修改矩阵
"""
m, n = len(matrix), len(matrix[0])
# 1. 创建两个标记数组,记录哪些行和列需要置零
zero_rows = [False] * m # 记录哪些行需要置零
zero_cols = [False] * n # 记录哪些列需要置零
# 2. 第一次遍历:标记哪些行和列需要置零
for i in range(m):
for j in range(n):
if matrix[i][j] == 0:
zero_rows[i] = True
zero_cols[j] = True
# 3. 第二次遍历:根据标记置零
for i in range(m):
for j in range(n):
if zero_rows[i] or zero_cols[j]:
matrix[i][j] = 0
Java 解法
java
class Solution {
public void setZeroes(int[][] matrix) {
int m = matrix.length;
int n = matrix[0].length;
// 1. 创建两个标记数组
boolean[] zeroRows = new boolean[m];
boolean[] zeroCols = new boolean[n];
// 2. 标记需要置零的行和列
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == 0) {
zeroRows[i] = true;
zeroCols[j] = true;
}
}
}
// 3. 根据标记置零
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (zeroRows[i] || zeroCols[j]) {
matrix[i][j] = 0;
}
}
}
}
}
方法二:优化空间复杂度(O(1) 额外空间)
Python 解法
python
from typing import List
class Solution:
def setZeroes(self, matrix: List[List[int]]) -> None:
"""
使用第一行和第一列作为标记空间
"""
m, n = len(matrix), len(matrix[0])
# 1. 判断第一行和第一列是否需要置零
first_row_zero = any(matrix[0][j] == 0 for j in range(n))
first_col_zero = any(matrix[i][0] == 0 for i in range(m))
# 2. 使用第一行和第一列作为标记
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 # 标记这一列需要置零
# 3. 根据标记置零(除了第一行和第一列)
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
# 4. 处理第一行
if first_row_zero:
for j in range(n):
matrix[0][j] = 0
# 5. 处理第一列
if first_col_zero:
for i in range(m):
matrix[i][0] = 0
Java 解法
java
class Solution {
public void setZeroes(int[][] matrix) {
int m = matrix.length;
int n = matrix[0].length;
// 1. 判断第一行和第一列是否需要置零
boolean firstRowZero = false;
boolean firstColZero = false;
for (int j = 0; j < n; j++) {
if (matrix[0][j] == 0) {
firstRowZero = true;
break;
}
}
for (int i = 0; i < m; i++) {
if (matrix[i][0] == 0) {
firstColZero = true;
break;
}
}
// 2. 使用第一行和第一列作为标记
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; // 标记这一列
}
}
}
// 3. 根据标记置零(除了第一行和第一列)
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;
}
}
}
// 4. 处理第一行
if (firstRowZero) {
for (int j = 0; j < n; j++) {
matrix[0][j] = 0;
}
}
// 5. 处理第一列
if (firstColZero) {
for (int i = 0; i < m; i++) {
matrix[i][0] = 0;
}
}
}
}
方法三:一行代码解法(Python 简洁版)
Python 解法
python
from typing import List
class Solution:
def setZeroes(self, matrix: List[List[int]]) -> None:
"""
简洁但空间复杂度较高的解法
"""
row_has_zero = [0 in row for row in matrix] # 行是否包含 0
col_has_zero = [0 in col for col in zip(*matrix)] # 列是否包含 0
for i, row0 in enumerate(row_has_zero):
for j, col0 in enumerate(col_has_zero):
if row0 or col0: # i 行或 j 列有 0
matrix[i][j] = 0 # 题目要求原地修改,无返回值
四、Java 与 Python 语法对比
1. 矩阵/二维数组操作
| 操作 | Java | Python |
|---|---|---|
| 获取行数 | matrix.length |
len(matrix) |
| 获取列数 | matrix[0].length |
len(matrix[0]) |
| 遍历行 | for (int i=0; i<m; i++) |
for i in range(m): |
| 遍历列 | for (int j=0; j<n; j++) |
for j in range(n): |
| 转置矩阵 | 需要手动实现 | zip(*matrix) |
2. 布尔数组/列表
| 操作 | Java | Python |
|---|---|---|
| 创建布尔数组 | boolean[] arr = new boolean[n]; |
arr = [False] * n |
| 检查是否存在 | 需要遍历 | any(arr) |
| 标记为真 | arr[i] = true; |
arr[i] = True |
3. 原地修改
| 操作 | Java | Python |
|---|---|---|
| 修改元素 | matrix[i][j] = 0; |
matrix[i][j] = 0 |
| 方法返回值 | void |
None |
五、实例演示
示例:matrix = [[1,1,1],[1,0,1],[1,1,1]]
方法一(两个标记数组)步骤:
-
标记阶段:
-
遍历发现
matrix[1][1] = 0 -
标记
zero_rows[1] = True -
标记
zero_cols[1] = True
-
-
置零阶段:
-
遍历所有位置,如果行索引为1或列索引为1,则置零
-
结果:
[[1,0,1],[0,0,0],[1,0,1]]
-
方法二(优化空间)步骤:
-
判断第一行和第一列:
-
first_row_zero = False(第一行没有0) -
first_col_zero = False(第一列没有0)
-
-
标记阶段(使用第一行和第一列):
-
发现
matrix[1][1] = 0 -
设置
matrix[1][0] = 0(标记第1行) -
设置
matrix[0][1] = 0(标记第1列) -
此时矩阵:
[[1,0,1],[0,0,1],[1,1,1]]
-
-
置零阶段(除了第一行和第一列):
-
遍历 i=1..2, j=1..2
-
位置 (1,1):
matrix[1][0]==0 or matrix[0][1]==0→ 置零 -
位置 (1,2):
matrix[1][0]==0→ 置零 -
位置 (2,1):
matrix[0][1]==0→ 置零 -
位置 (2,2): 不满足条件
-
此时矩阵:
[[1,0,1],[0,0,0],[1,0,1]]
-
-
处理第一行和第一列:
-
first_row_zero = False,第一行不变 -
first_col_zero = False,第一列不变 -
最终结果:
[[1,0,1],[0,0,0],[1,0,1]]
-
方法三(一行代码版)步骤:
-
计算行标记:
-
row_has_zero = [False, True, False] -
因为第1行(索引1)包含0
-
-
计算列标记:
-
col_has_zero = [False, True, False] -
因为第1列(索引1)包含0
-
-
置零:
-
遍历所有位置,如果行或列标记为True,则置零
-
结果:
[[1,0,1],[0,0,0],[1,0,1]]
-
六、关键细节解析
1. 为什么要先判断第一行和第一列?
-
在优化空间的方法中,我们使用第一行和第一列作为标记空间
-
如果第一行或第一列原本就有0,我们需要记录下来,最后再处理
-
否则,我们的标记会覆盖第一行和第一列的原始信息
2. 为什么标记阶段要从 (1,1) 开始?
-
因为 (0,0) 这个位置同时属于第一行和第一列
-
如果从 (0,0) 开始,可能会错误地标记第一行和第一列
-
所以我们要先处理完第一行和第一列的判断,再从 (1,1) 开始标记
3. 如何处理边界情况?
-
空矩阵:直接返回
-
1×1 矩阵:如果元素为0,则已经是0;如果不为0,则不变
-
只有一行或一列:特殊处理,但算法仍然适用
4. 为什么方法三的空间复杂度较高?
-
方法三创建了两个长度为 m 和 n 的布尔列表
-
空间复杂度为 O(m+n),虽然比 O(m×n) 好,但不如 O(1) 的优化方法
5. 原地修改的含义是什么?
-
原地修改意味着不能创建新的矩阵来存储结果
-
所有修改都必须在原始矩阵上进行
-
这也是为什么题目要求函数返回
None(Python)或void(Java)
七、复杂度分析
方法一(两个标记数组)
-
时间复杂度:O(m × n),需要两次遍历矩阵
-
空间复杂度:O(m + n),需要两个标记数组
方法二(优化空间)
-
时间复杂度:O(m × n),需要多次遍历矩阵
-
空间复杂度:O(1),只使用了常数个额外变量
方法三(一行代码版)
-
时间复杂度:O(m × n),需要遍历矩阵多次
-
空间复杂度:O(m + n),需要两个标记列表
八、其他解法
解法一:使用集合记录零的位置(Python)
python
class Solution:
def setZeroes(self, matrix: List[List[int]]) -> None:
m, n = len(matrix), len(matrix[0])
zero_rows = set()
zero_cols = set()
# 记录零的位置
for i in range(m):
for j in range(n):
if matrix[i][j] == 0:
zero_rows.add(i)
zero_cols.add(j)
# 置零
for i in range(m):
for j in range(n):
if i in zero_rows or j in zero_cols:
matrix[i][j] = 0
解法二:分步置零法
python
class Solution:
def setZeroes(self, matrix: List[List[int]]) -> None:
m, n = len(matrix), len(matrix[0])
# 第一步:标记需要置零的行和列
rows_to_zero = set()
cols_to_zero = set()
for i in range(m):
for j in range(n):
if matrix[i][j] == 0:
rows_to_zero.add(i)
cols_to_zero.add(j)
# 第二步:行置零
for row in rows_to_zero:
for j in range(n):
matrix[row][j] = 0
# 第三步:列置零
for col in cols_to_zero:
for i in range(m):
matrix[i][col] = 0
九、常见问题与解答
Q1: 为什么不能先置零再遍历?
A1: 如果先置零再遍历,会创建新的0,导致连锁反应。例如:
text
原始矩阵: [[0,1,1],[1,1,1],[1,1,1]]
如果先置零第一行: [[0,0,0],[1,1,1],[1,1,1]]
再遍历时发现新的0,继续置零,最终所有元素都变成0
Q2: 如何处理大规模矩阵?
A2: 对于大规模矩阵:
-
优化空间的方法(O(1)空间复杂度)是最佳选择
-
避免创建与矩阵大小相关的额外数据结构
-
尽量减少遍历次数
Q3: 这个方法适用于非方阵吗?
A3: 是的,算法适用于任意 m × n 的矩阵,包括非方阵。
Q4: 如果矩阵中有负数怎么办?
A4: 算法只关心元素是否为0,与正负无关。负数可以正常处理。
Q5: 如何测试边界情况?
A5: 应该测试以下情况:
-
空矩阵
-
1×1矩阵(元素为0或非0)
-
只有一行的矩阵
-
只有一列的矩阵
-
所有元素都是0的矩阵
-
没有0的矩阵
十、相关题目
1. LeetCode 289. 生命游戏
python
class Solution:
def gameOfLife(self, board: List[List[int]]) -> None:
m, n = len(board), len(board[0])
# 八个方向的偏移量
directions = [(-1,-1), (-1,0), (-1,1),
(0,-1), (0,1),
(1,-1), (1,0), (1,1)]
# 遍历每个细胞
for i in range(m):
for j in range(n):
# 统计活邻居的数量
live_neighbors = 0
for dx, dy in directions:
ni, nj = i + dx, j + dy
if 0 <= ni < m and 0 <= nj < n and abs(board[ni][nj]) == 1:
live_neighbors += 1
# 应用规则
if board[i][j] == 1 and (live_neighbors < 2 or live_neighbors > 3):
board[i][j] = -1 # 活细胞死亡
elif board[i][j] == 0 and live_neighbors == 3:
board[i][j] = 2 # 死细胞复活
# 更新细胞状态
for i in range(m):
for j in range(n):
if board[i][j] > 0:
board[i][j] = 1
else:
board[i][j] = 0
2. LeetCode 48. 旋转图像
python
class Solution:
def rotate(self, matrix: List[List[int]]) -> None:
n = len(matrix)
# 转置矩阵
for i in range(n):
for j in range(i, n):
matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
# 反转每一行
for i in range(n):
matrix[i].reverse()
十一、总结
核心要点
-
问题分解:将矩阵置零问题分解为标记和置零两个阶段
-
空间优化:使用矩阵的第一行和第一列作为标记空间,实现 O(1) 空间复杂度
-
原地修改:所有操作都在原矩阵上进行,不创建新矩阵
算法步骤(优化空间版本)
-
判断第一行和第一列是否需要置零
-
使用第一行和第一列作为标记空间,记录其他行和列是否需要置零
-
根据标记将对应的行和列置零(除了第一行和第一列)
-
处理第一行和第一列
时间复杂度与空间复杂度
-
时间复杂度:O(m × n),需要遍历矩阵多次
-
空间复杂度:O(1),只使用常数个额外变量
适用场景
-
需要原地修改矩阵
-
空间复杂度要求严格
-
矩阵可能很大,不能创建额外的大数据结构
扩展思考
矩阵置零问题的核心思想是使用现有空间存储额外信息,这种思想可以应用于:
-
其他需要标记的矩阵问题
-
需要原地操作的数组/矩阵问题
-
空间受限环境下的算法设计
掌握矩阵置零的多种解法,不仅能够解决这个问题,还能理解如何在空间和时间之间做出权衡,是面试中重要的算法技巧。