力扣Hoot100矩阵置零:O(1)空间复杂度深度解析与优化实践

在算法面试与日常刷题中,LeetCode 73题「矩阵置零」是一道检验空间优化能力的经典题目。它要求在不使用额外数组的情况下,原地修改矩阵,这不仅是算法思维的挑战,也体现了在资源受限场景下(如嵌入式系统或大规模数据处理)的优化思想。本文将深入剖析如何利用矩阵自身的第一行和第一列作为标记,实现O(1)空间复杂度的优雅解法,并探讨其背后的逻辑与实现细节。

一、问题定义与直观解法的局限

题目要求非常明确:给定一个 m x n 的矩阵,如果某个元素为0,则将其所在的行和列的所有元素都设置为0。要求必须原地修改矩阵,并尽可能降低空间复杂度。进阶要求是实现O(1)的空间复杂度。

我们先来看题目给出的示例,以直观理解问题:

示例输入1:

matrix = [
  [1,1,1],
  [1,0,1],
  [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]
]

对应的输出2:

[
  [0,0,0,0],
  [0,4,5,0],
  [0,3,1,0]
]

最直接的思路是使用两个额外的布尔数组来记录需要置零的行和列。遍历矩阵,遇到0时,在对应的行标记数组和列标记数组中做记录。然后再次遍历矩阵,根据标记数组将元素置零。这种方法的时间复杂度是O(m*n),空间复杂度是O(m+n)。虽然简单易懂,但不符合O(1)空间复杂度的进阶要求。这促使我们去寻找一种能复用矩阵自身空间的巧妙方法。这种从“使用额外空间”到“复用已有空间”的思维转变,在优化算法时非常关键。

二、核心优化策略:将矩阵第一行与第一列变为标记位

为了实现O(1)的空间复杂度,核心思想是利用矩阵自身的第一行(row 0)和第一列(column 0)作为我们需要的标记数组。具体来说:

  • matrix[i][0]来标记第i行是否需要整体置零。
  • matrix[0][j]来标记第j列是否需要整体置零。

然而,这里存在一个关键的冲突:第一行和第一列本身也是矩阵的一部分,它们内部也可能包含0。如果我们直接用它们做标记,就会覆盖掉这些原始的0,导致后续无法判断第一行和第一列自身是否需要被置零。

解决方案:引入两个独立的变量来解决这个冲突。

  • 用一个变量(例如firstRowHasZero)来专门记录第一行原本是否存在0。
  • matrix[0][0]这个位置来记录第一列原本是否存在0。因为第一行和第一列的交汇点matrix[0][0]具有双重身份,我们选择让它优先代表第一列的标记状态。

通过这种方式,我们成功地将标记信息“编码”到了矩阵内部,完全省去了额外的数组,这正是实现O(1)空间复杂度的精髓所在。这种思路在深度学习的模型压缩中也有体现,即如何用更少的参数或计算量来存储和传递关键信息。

[AFFILIATE_SLOT_1]

三、算法步骤的详细拆解与实现

理解了核心思想后,我们可以将整个算法分解为五个清晰的步骤。以下步骤紧密结合了代码逻辑,并指出了实现中需要特别注意的“修复点”。

步骤1:初始化
首先获取矩阵的行数m和列数n。然后初始化标记变量firstRowHasZero,用于记录第一行是否有0。注意,我们通常用False0表示“有0”,用True1表示“无0”,具体取决于实现习惯。

步骤2:标记阶段(第一次遍历)
遍历整个矩阵(从i=0m-1j=0n-1)。对于每个元素matrix[i][j]

  • 如果它为0:
    • 立即标记其所在的列:将matrix[0][j]设为0。
    • 如果i == 0(即它在第一行),则将firstRowHasZero标记为True(表示第一行需要置零)。
    • 如果i != 0(即它不在第一行),则标记其所在的行:将matrix[i][0]设为0。

⚠️ 注意:标记行时跳过了第一行,因为第一行的状态由firstRowHasZero单独管理;标记列时则包含了第一列,其状态最终由matrix[0][0]管理。

步骤3:根据标记置零(处理内部矩阵)
现在,第一行和第一列已经存储了完整的列标记和行标记(除第一行外)。我们从第二行第二列(i=1, j=1)开始遍历矩阵。对于matrix[i][j],只要其对应的行标记(matrix[i][0] == 0)或列标记(matrix[0][j] == 0)有一个为真,就将其置为0。这一步避开了第一行和第一列,防止我们用于存储标记的原始数据被提前覆盖。

步骤4:关键修复点一:处理第一列
现在需要根据matrix[0][0]的值来判断第一列是否需要置零。如果matrix[0][0] == 0,则意味着第一列原本包含0或因为其他行的0而被标记。我们需要将第一列中除第一行外的所有元素(从i=1m-1)置零。✅ 必须优先处理第一列,再处理第一行,否则如果先处理第一行,会把matrix[0][0]这个关键标记位覆盖掉。

步骤5:关键修复点二:处理第一行
最后,根据firstRowHasZero变量的值来判断第一行是否需要置零。如果为真,则将第一行(j=0n-1)的所有元素置零。这里一个常见的错误是循环边界误用行数m,实际上应该使用列数n来遍历第一行的所有列。

这个过程类似于神经网络中的前向传播与反向传播,需要精心设计信息流动的路径和顺序,避免信息丢失或冲突。

四、完整代码实现与复杂度分析

将上述步骤整合起来,就得到了如下完整且健壮的代码实现。代码中清晰地体现了五个步骤,并妥善处理了两个关键的边界情况。

class Solution {
    public void setZeroes(int[][] matrix) {
        // 1. 初始化变量:获取矩阵行列数,标记第一行是否有0
        int rowLength = matrix.length;    // 矩阵总行数
        int colLength = matrix[0].length; // 矩阵总列数
        int zero = 1;                     // 标记第一行是否有0,1=无0,0=有0
        // 2. 标记阶段:遍历矩阵,用第一行/第一列记录需要置零的行/列
        for (int i = 0; i < rowLength; i++) {
            for (int m = 0; m < colLength; m++) {
                if (matrix[i][m] == 0) {
                    matrix[0][m] = 0; // 标记第m列需要置零
                    if (i == 0) {
                        zero = 0;      // 第一行有0,更新标记变量
                    } else {
                        matrix[i][0] = 0; // 标记第i行需要置零
                    }
                }
            }
        }
        // 3. 置零阶段:处理第二行第二列及以后的元素(避开第一行第一列)
        for (int i = 1; i < rowLength; i++) {
            for (int m = 1; m < colLength; m++) {
                if (matrix[i][0] == 0 || matrix[0][m] == 0) {
                    matrix[i][m] = 0;
                }
            }
        }
        // 4. 修复点1:先处理第一列(避免第一行置零覆盖matrix[0][0])
        if (matrix[0][0] == 0) {
            for (int i = 1; i < rowLength; i++) {
                matrix[i][0] = 0;
            }
        }
        // 5. 修复点2:处理第一行(修正循环边界:用列数colLength而非行数)
        if (zero == 0) {
            for (int m = 0; m < colLength; m++) { // 修复:循环到列数末尾
                matrix[0][m] = 0;
            }
        }
    }
}

复杂度分析:

  • 时间复杂度:O(m * n)。我们进行了两次完整的矩阵遍历(标记阶段和置零阶段),以及两次对边界的线性遍历(处理第一列和第一行)。常数操作,总体仍是O(m*n)。
  • 空间复杂度:O(1)。我们只使用了常数个额外变量(m, n, firstRowHasZero,以及循环变量),没有使用任何与矩阵尺寸相关的额外数据结构。

这个解法完美满足了题目的进阶要求。它展示了如何通过巧妙的信息编码,将问题所需的辅助空间压缩到极致。这种优化思想在机器学习AI系统开发中尤为重要,例如在移动端或边缘设备上部署模型时,对内存的极致利用。

[AFFILIATE_SLOT_2]

五、总结与思维延伸

LeetCode 73「矩阵置零」的O(1)空间解法是一道经典的“空间换时间”思维逆向题。它教会我们的不仅是具体的算法技巧,更是一种资源复用状态压缩的思维方式。回顾关键点:

  1. 识别标记载体:找到矩阵中可以被“牺牲”或“复用”的部分(第一行和第一列)来存储状态信息。
  2. 解决状态冲突:通过引入额外变量,分离重叠的状态信息(第一行和第一列自身的0),这是算法的核心难点。
  3. 严格规定操作顺序:先处理内部矩阵,再处理作为标记载体的边界,并且注意边界的处理顺序(先列后行),以避免覆盖标记。

掌握这种解法,不仅能帮助你在面试中脱颖而出,更能深化你对算法优化空间效率的理解。这种追求极致效率的思维,正是驱动人工智能自然语言处理等领域不断突破计算瓶颈的重要动力之一。希望本文的详细拆解能帮助你彻底吃透这道题,并将其背后的思想应用到更广泛的编程与优化场景中去。

posted on 2026-04-07 19:14  ljbguanli  阅读(8)  评论(0)    收藏  举报