题目简介
给定一个 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-231 <= matrix[i][j] <= 231 - 1
进阶:
- 一个直观的解决方案是使用
O(m n)的额外空间,但这并不是一个好的解决方案。 - 一个简单的改进方案是使用
O(m + n)的额外空间,但这仍然不是最好的解决方案。 - 你能想出一个仅使用常量空间的解决方案吗?
解题思路
这道题有几种解法,从最简单的思路开始,逐步优化到常数空间复杂度。
基本思路分析
题目的难点在于:如果我们直接修改矩阵,那么后续扫描时无法分辨哪些是原始 0 哪些是被设置的 0,所以需要先记录原始 0 的位置,再统一修改。
优化到常数空间复杂度的方法
最优解法使用矩阵的第一行和第一列作为标记,以记录哪些行列需要设置为 0,额外使用两个变量记录第一行和第一列本身是否需要置为 0.
具体步骤如下:
- 使用两个布尔变量记录第一行和第一列是否包含 0
- 使用矩阵的第一行和第一列作为标记数组
- 遍历矩阵,标记需要设置为 0 的行和列
- 根据标记,将相应的行和列设置为 0
- 最后处理第一行和第一列
Golang 代码实现
func setZeroes(matrix [][]int) {
if len(matrix) == 0 || len(matrix[0]) == 0 {
return
}
m, n := len(matrix), len(matrix[0])
firstRowHasZero, firstColHasZero := false, false
for j := 0; j < n; j++ {
if matrix[0][j] == 0 {
firstRowHasZero = true
break
}
}
for i := 0; i < m; i++ {
if matrix[i][0] == 0 {
firstColHasZero = true
break
}
}
// 使用第一行和第一列作为标记
for i := 1; i < m; i++ {
for j := 1; j < n; j++ {
if matrix[i][j] == 0 {
matrix[i][0] = 0
matrix[0][j] = 0
}
}
}
for i := 1; i < m; i++ {
if matrix[i][0] == 0 {
for j := 1; j < n; j++ {
matrix[i][j] = 0
}
}
}
for j := 1; j < n; j++ {
if matrix[0][j] == 0 {
for i := 1; i < m; i++ {
matrix[i][j] = 0
}
}
}
if firstRowHasZero {
for j := 0; j < n; j++ {
matrix[0][j] = 0
}
}
if firstColHasZero {
for i := 0; i < m; i++ {
matrix[i][0] = 0
}
}
}
算法处理时序
sequenceDiagram
participant M as 主程序
participant F1 as 检查第一行
participant F2 as 检查第一列
participant S as 标记阶段
participant R1 as 处理行
participant R2 as 处理列
participant FR as 处理第一行
participant FC as 处理第一列
M->>F1: 检查第一行是否有0
F1-->>M: 返回firstRowHasZero
M->>F2: 检查第一列是否有0
F2-->>M: 返回firstColHasZero
M->>S: 使用第一行和第一列作为标记
S-->>M: 完成标记
M->>R1: 根据第一列标记处理行
R1-->>M: 完成行处理
M->>R2: 根据第一行标记处理列
R2-->>M: 完成列处理
alt 第一行有0
M->>FR: 将第一行全部置为0
FR-->>M: 完成
end
alt 第一列有0
M->>FC: 将第一列全部置为0
FC-->>M: 完成
end
关键数据变化图
示例 1
matrix = [[1,1,1],[1,0,1],[1,1,1]]
初始状态:
+---+---+---+
| 1 | 1 | 1 |
+---+---+---+
| 1 | 0 | 1 |
+---+---+---+
| 1 | 1 | 1 |
+---+---+---+
检查第一行和第一列:
+---+---+---+ firstRowHasZero = false
| 1 | 1 | 1 | firstColHasZero = false
+---+---+---+
| 1 | 0 | 1 |
+---+---+---+
| 1 | 1 | 1 |
+---+---+---+
标记阶段(发现matrix[1][1]=0):
+---+---+---+
| 1 | 0 | 1 | <- 标记第1列(索引为1)需要置0
+---+---+---+
| 0 | 0 | 1 | <- 标记第1行(索引为1)需要置0
+---+---+---+
| 1 | 1 | 1 |
+---+---+---+
根据标记处理行(第1行):
+---+---+---+
| 1 | 0 | 1 |
+---+---+---+
| 0 | 0 | 0 | <- 根据第一列标记将第1行全部置0
+---+---+---+
| 1 | 1 | 1 |
+---+---+---+
根据标记处理列(第1列):
+---+---+---+
| 1 | 0 | 1 |
+---+---+---+
| 0 | 0 | 0 |
+---+---+---+
| 1 | 0 | 1 | <- 根据第一行标记将第1列全部置0
+---+---+---+
最终结果:
+---+---+---+
| 1 | 0 | 1 |
+---+---+---+
| 0 | 0 | 0 |
+---+---+---+
| 1 | 0 | 1 |
+---+---+---+
示例 2
matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]]
初始状态:
+---+---+---+---+
| 0 | 1 | 2 | 0 |
+---+---+---+---+
| 3 | 4 | 5 | 2 |
+---+---+---+---+
| 1 | 3 | 1 | 5 |
+---+---+---+---+
检查第一行和第一列:
+---+---+---+---+ firstRowHasZero = true
| 0 | 1 | 2 | 0 | firstColHasZero = true
+---+---+---+---+
| 3 | 4 | 5 | 2 |
+---+---+---+---+
| 1 | 3 | 1 | 5 |
+---+---+---+---+
标记阶段(处理内部元素):
+---+---+---+---+
| 0 | 1 | 2 | 0 | 第一行和第一列保留
+---+---+---+---+
| 3 | 4 | 5 | 0 | matrix[1][3]=0,标记第1行和第3列
+---+---+---+---+
| 1 | 3 | 1 | 5 |
+---+---+---+---+
根据标记处理行和列:
+---+---+---+---+
| 0 | 1 | 2 | 0 |
+---+---+---+---+
| 3 | 4 | 5 | 0 | 没有内部行列需要置0
+---+---+---+---+
| 1 | 3 | 1 | 0 | <- 处理第3列(因为matrix[0][3]=0)
+---+---+---+---+
处理第一行和第一列:
+---+---+---+---+
| 0 | 0 | 0 | 0 | <- 第一行全部置0(因为firstRowHasZero=true)
+---+---+---+---+
| 0 | 4 | 5 | 0 |
+---+---+---+---+ <- 第一列全部置0(因为firstColHasZero=true)
| 0 | 3 | 1 | 0 |
+---+---+---+---+
最终结果:
+---+---+---+---+
| 0 | 0 | 0 | 0 |
+---+---+---+---+
| 0 | 4 | 5 | 0 |
+---+---+---+---+
| 0 | 3 | 1 | 0 |
+---+---+---+---+
这个算法的巧妙之处在于利用矩阵的第一行和第一列作为标记空间,将空间复杂度从O(m+n)优化到了O(1),同时通过两个布尔变量记录第一行和第一列的原始状态,避免了信息丢失。整个过程只需要几次线性扫描,时间复杂度为O(mn)。
考查知识点
-
空间复杂度优化:从直观的O(mn)优化到O(m+n),最终达到O(1)的常数空间复杂度
-
就地算法(In-place Algorithm) :不使用额外空间,直接在原数据结构上操作
-
矩阵操作:对二维数组的行列进行操作的能力
-
标记技术:使用现有数据结构的部分作为标记,避免额外空间
-
边界条件处理:对第一行和第一列的特殊处理
-
编程技巧:使用变量存储状态而不是额外数组