本文概览:本文以LeetCode经典题目"矩阵置零"为例,从最容易踩的坑入手,逐步优化空间复杂度,从 O(mn) 到 O(m+n) 再到 O(1) 原地变换,系统讲解如何用第一行和第一列作为标记数组实现原地置零
一、题目

二、题目分析
给定一个 m×n 的矩阵,如果一个元素为 0,则将其所在行和列的所有元素都设为 0,要求原地修改
目标:将所有包含 0 的元素所在的行和列全部置零
最容易踩的坑:不能边遍历边置零
这是本题最容易忽略的地方。如果在遍历矩阵的时候顺便置零,会导致后续遍历时无法区分某个 0 是原本就有的 还是置零操作产生的。比如矩阵中只有一个原本的 0,但置零后它所在的行和列都变成了 0,后续遍历到这些新的 0 时又会把更多的行和列置零,最终整个矩阵都变成了 0,算法完全错误
所以正确的做法是:先标记,后置零。第一遍遍历只记录哪些位置有 0,第二遍遍历根据标记来置零
思路概览
Java实现代码如下
Java
public void setZeroes(int[][] matrix) {
// 行长度
int m = matrix.length;
// 列长度
int n = matrix[0].length;
// 记录第一列或者第一行是否有0
boolean row0 = false;
boolean col0 = false;
// 记录第0列是否有0
for (int i = 0; i < m; i++) {
if (matrix[i][0] == 0)
col0 = true;
}
// 记录第0行是否有0
for (int j = 0; j < n; j++) {
if (matrix[0][j] == 0)
row0 = true;
}
// 用第一行和第一列作为标记
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;
}
}
}
// 置0
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (matrix[0][j] == 0 || matrix[i][0] == 0)
matrix[i][j] = 0;
}
}
// 最后处理第一行/第一列
if (row0) for (int j = 0; j < n; j++) matrix[0][j] = 0;
if (col0) for (int i = 0; i < m; i++) matrix[i][0] = 0;
}
思路简要说明
-
记录第一行和第一列的原始状态:用两个布尔变量记录第一行和第一列本身是否含有 0
-
用第一行和第一列作为标记:遍历内部矩阵,遇到 0 就把对应的第一行和第一列位置标记为 0
-
根据标记置零:再次遍历内部矩阵,根据第一行和第一列的标记来置零
-
最后处理第一行和第一列:根据布尔变量决定是否将第一行或第一列全部置零
三、思路详解
思路一:O(mn) 空间
最直接的想法是:创建一个和原矩阵同样大小的 m×n 标记数组,遍历原矩阵,把所有 0 的位置在标记数组中标记出来。然后再次遍历原矩阵,去标记数组中查询对应位置是否为 0,是就置零
- 时间复杂度:O(mn)
- 空间复杂度:O(mn),需要额外一个 m×n 的标记数组
- 核心瓶颈:标记数组和原矩阵一样大,空间开销太大
- 关键思考:我们真的需要记录每个位置是否为 0 吗?
思路二:O(m+n) 空间
仔细想想,我们不需要记录"哪个位置是 0",只需要记录"哪一行和哪一列需要置零"。因为只要某一行有一个 0,整行都要置零;某一列有一个 0,整列都要置零
所以可以用两个一维数组:一个长度为 m 的数组标记哪些行需要置零,一个长度为 n 的数组标记哪些列需要置零。遍历矩阵时,遇到 matrix[i][j] == 0,就把行标记数组第 i 位和列标记数组第 j 位设为 true
- 时间复杂度:O(mn)
- 空间复杂度:O(m+n),两个一维数组
- 核心瓶颈:虽然比 O(mn) 好很多,但仍然有额外空间开销
- 关键思考:能否不创建任何额外数组,原地完成标记?
思路三:O(1) 空间------原地变换
核心思想
既然我们需要标记"哪些行和哪些列需要置零",而行标记和列标记本质上就是两个一维数组。那我们能不能用矩阵本身的第一行和第一列来充当这两个标记数组?
- 第一行充当列标记数组:
matrix[0][j] == 0表示第 j 列需要置零 - 第一列充当行标记数组:
matrix[i][0] == 0表示第 i 行需要置零
这样就不需要创建额外的数组了,空间复杂度降为 O(1)
为什么用第一行和第一列作为标记数组是可行的?
这是整个解法最关键的地方。我们用一个具体的矩阵来逐步说明
假设原始矩阵如下,其中 matrix[2][3] = 0:
列0 列1 列2 列3 列4
行0 [ 1 , 1 , 1 , 1 , 1 ] ← 第一行:充当列标记
行1 [ 1 , 1 , 1 , 1 , 1 ]
行2 [ 1 , 1 , 1 , 0 , 1 ] ← 这里有0!
行3 [ 1 , 1 , 1 , 1 , 1 ]
↑
第一列:充当行标记
标记操作 :遇到 matrix[2][3] = 0,把对应的第一行和第一列位置标记为 0
列0 列1 列2 列3 列4
行0 [ 1 , 1 , 1 , 0 , 1 ] ← matrix[0][3]被标记为0(第3列需要置零)
行1 [ 1 , 1 , 1 , 1 , 1 ]
行2 [ 0 , 1 , 1 , 0 , 1 ] ← matrix[2][0]被标记为0(第2行需要置零)
行3 [ 1 , 1 , 1 , 1 , 1 ]
↑
第2行标记位为0
置零操作:遍历内部矩阵,看第一行和第一列的标记
列0 列1 列2 列3 列4
行0 [ 1 , 1 , 1 , 0 , 1 ] ← 列标记:第3列需要置零
行1 [ 1 , 1 , 1 , 0 , 1 ] ← matrix[0][3]==0,所以matrix[1][3]置零
行2 [ 0 , 0 , 0 , 0 , 0 ] ← matrix[2][0]==0,整行置零
行3 [ 1 , 1 , 1 , 0 , 1 ] ← matrix[0][3]==0,所以matrix[3][3]置零
↑
行标记:第2行需要置零
可以看到:标记位为 0 后,遍历内部矩阵时对应的整行或整列都会被置零,不需要额外的行遍历或列遍历
那如果第一行或第一列本身就有 0 呢?比如 matrix[0][2] 原本就是 0:
列0 列1 列2 列3 列4
行0 [ 1 , 1 , 0 , 1 , 1 ] ← 原本就有0
行1 [ 1 , 1 , 1 , 1 , 1 ]
行2 [ 1 , 1 , 1 , 0 , 1 ] ← 内部也有0
行3 [ 1 , 1 , 1 , 1 , 1 ]
标记后:
列0 列1 列2 列3 列4
行0 [ 1 , 1 , 0 , 0 , 1 ] ← matrix[0][3]被标记为0
行1 [ 1 , 1 , 1 , 1 , 1 ]
行2 [ 0 , 1 , 1 , 0 , 1 ] ← matrix[2][0]被标记为0
行3 [ 1 , 1 , 1 , 1 , 1 ]
此时 matrix[0][2] 是原本的 0,matrix[0][3] 是标记产生的 0,我们无法区分。但这没关系------第一行本身有 0,意味着第一行最终本来就要全部置零,标记操作把某个位置设为 0,和"原本就有 0"的最终效果是一样的。所以标记和原始数据之间不会产生矛盾
我们只需要用两个布尔变量 row0 和 col0 提前记录第一行和第一列是否原本含有 0,最后根据这两个变量决定是否将第一行或第一列全部置零即可
具体步骤
第一步:记录第一行和第一列的原始状态
遍历第一行,如果有 0 则 row0 = true;遍历第一列,如果有 0 则 col0 = true
第二步:用第一行和第一列作为标记
遍历内部矩阵(i 从 1 到 m-1,j 从 1 到 n-1),如果 matrix[i][j] == 0,就把 matrix[i][0] 和 matrix[0][j] 标记为 0
这一步完全没有冲突:因为内部矩阵的 0 只会影响第一行和第一列的标记,而第一行和第一列的置零操作我们留到最后单独处理
第三步:根据标记置零内部矩阵
再次遍历内部矩阵,如果 matrix[i][0] == 0 或 matrix[0][j] == 0,就把 matrix[i][j] 置零
第四步:处理第一行和第一列
根据第一步记录的布尔变量,如果 row0 为 true,就把第一行全部置零;如果 col0 为 true,就把第一列全部置零
为什么第一行和第一列要最后处理?
因为第一行和第一列在第二步和第三步中充当标记数组,如果提前把它们置零了,标记信息就丢失了,后续无法正确判断哪些行和列需要置零
举例说明
以 matrix = [[1,1,1],[1,0,1],[1,1,1]] 为例
第一步:记录第一行和第一列状态
第一行无 0,row0 = false;第一列无 0,col0 = false
第二步:标记
遍历到 matrix[1][1] = 0:
标记 matrix[1][0] = 0,matrix[0][1] = 0
矩阵变为:
[1, 0, 1]
[0, 0, 1]
[1, 1, 1]
第三步:置零内部矩阵
matrix[1][1]:matrix[1][0]==0,置零
matrix[1][2]:matrix[1][0]==0,置零
matrix[2][1]:matrix[0][1]==0,置零
矩阵变为:
[1, 0, 1]
[0, 0, 0]
[1, 0, 1]
第四步:处理第一行和第一列
row0 = false,第一行不动;col0 = false,第一列不动
最终结果为 [[1,0,1],[0,0,0],[1,0,1]],正确
- 时间复杂度:O(mn),遍历矩阵若干次
- 空间复杂度:O(1),只用了两个布尔变量