【LeetCode 热题 100】73. 矩阵置零——(解法二)空间复杂度 O(1)
Problem: 73. 矩阵置零
题目:给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。
【LeetCode 热题 100】73. 矩阵置零——(解法一)空间复杂度 O(M + N)
文章目录
- 整体思路
- 完整代码
- 时空复杂度
- 时间复杂度:O(M * N)
- 空间复杂度:O(1)
整体思路
这段代码旨在以 O(1) 的额外空间复杂度 解决 “矩阵置零” 问题。这是一个非常经典且巧妙的算法,它通过 复用矩阵自身的第一行和第一列 作为标记区域,来代替使用额外的 Set
或 List
。
算法的整体思路可以分解为以下四个关键阶段:
-
阶段一:预先检查并标记第一行和第一列的状态
- 由于第一行和第一列将被用作标记区域,它们自身是否需要被置零的状态需要被特殊处理,否则信息会丢失。
- 算法使用两个布尔变量
rowBase0
和colBase0
:colBase0
:通过一个循环检查第一列自身是否含有 0。如果含有,就将colBase0
设为true
。rowBase0
:通过另一个循环检查第一行自身是否含有 0。如果含有,就将rowBase0
设为true
。
- 这两个标志位保存了第一行和第一列最终是否需要被整体置零的“命运”,为后续的标记操作解除了后顾之忧。
-
阶段二:使用第一行和第一列作为标记区域
- 算法遍历矩阵的内部区域(即从
i=1, j=1
开始)。 - 当在内部区域发现一个零
matrix[i][j] == 0
时,它并不直接修改整行整列,而是将这个信息记录在对应的第一行和第一列上:matrix[i][0] = 0;
// 标记第i
行需要被置零。matrix[0][j] = 0;
// 标记第j
列需要被置零。
- 这样,第一行和第一列就变成了存储标记的“哨兵”或“备忘录”。
- 算法遍历矩阵的内部区域(即从
-
阶段三:根据标记置零内部矩阵
- 在完成标记后,算法再次遍历矩阵的内部区域(
i=1, j=1
)。 - 对于每个元素
matrix[i][j]
,它会检查其对应的行标记和列标记:if (matrix[i][0] == 0 || matrix[0][j] == 0)
- 如果第
i
行的标记(matrix[i][0]
)为 0,或者第j
列的标记(matrix[0][j]
)为 0,就说明matrix[i][j]
位于一个需要被置零的行或列,因此将其值设为 0。
- 在完成标记后,算法再次遍历矩阵的内部区域(
-
阶段四:根据初始标志位处理第一行和第一列
- 到目前为止,内部矩阵已经被正确处理。最后,轮到处理第一行和第一列自身了。
- 这一步必须是最后一步,因为在此之前,第一行和第一列还承担着标记的角色。
- 根据阶段一保存的
rowBase0
和colBase0
的值:- 如果
rowBase0
为true
,则将整个第一行置零。 - 如果
colBase0
为true
,则将整个第一列置零。
- 如果
通过这个精巧的四步流程,算法在不使用额外数组或集合的情况下,完成了矩阵置零的任务。
完整代码
class Solution {/*** 将矩阵中包含 0 的元素的整行和整列都置为 0。* (空间复杂度 O(1) 的最优解)* @param matrix 一个 M x N 的整数矩阵*/public void setZeroes(int[][] matrix) {int m = matrix.length;int n = matrix[0].length;// rowBase0: 标记第一行本身是否需要被置零boolean rowBase0 = false;// colBase0: 标记第一列本身是否需要被置零boolean colBase0 = false;// 步骤 1: 检查第一列是否含有 0for (int i = 0; i < m; i++) {if (matrix[i][0] == 0) {colBase0 = true;break; // 找到一个即可}}// 步骤 1: 检查第一行是否含有 0for (int j = 0; j < n; j++) {if (matrix[0][j] == 0) {rowBase0 = true;break; // 找到一个即可}}// 步骤 2: 使用第一行和第一列作为标记区域,记录内部矩阵 (从[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 列需要置零}}}// 步骤 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 (rowBase0) {for (int j = 0; j < n; j++) {matrix[0][j] = 0;}}if (colBase0) {for (int i = 0; i < m; i++) {matrix[i][0] = 0;}}}
}
时空复杂度
时间复杂度:O(M * N)
- 检查第一行/列:两个独立的
for
循环,分别执行最多M
次和N
次。时间复杂度为 O(M + N)。 - 标记内部矩阵:一个双层
for
循环,遍历(M-1) * (N-1)
个元素。时间复杂度为 O(M * N)。 - 置零内部矩阵:另一个双层
for
循环,同样遍历(M-1) * (N-1)
个元素。时间复杂度为 O(M * N)。 - 处理第一行/列:两个独立的
for
循环,分别执行N
次和M
次。时间复杂度为 O(M + N)。
综合分析:
算法的总时间复杂度由几个部分组成,其中最高阶项是 O(M * N)
。因此,最终的时间复杂度是 O(M * N)。
空间复杂度:O(1)
- 主要存储开销:该算法没有创建任何与输入规模
M
或N
成比例的新的数据结构。 - 辅助变量:只使用了
m
,n
,rowBase0
,colBase0
,i
,j
等几个常数数量的变量。
综合分析:
算法所需的额外辅助空间是常数级别的,不随矩阵大小变化。因此,其空间复杂度为 O(1),这是该问题的最优空间解法。