资讯详情

资讯详情

力扣Hot 100矩阵题详解:掌握坐标与边界,攻克螺旋矩阵到最大正方形

Hot 100 里的矩阵题是我刷题过程中最纠结的一组。说它难吧每一道的核心算法其实在数组题里都见过说它简单吧螺旋矩阵、旋转图像、矩阵置零这种题明明思路写了一屏一跑就错。后来我把这十几道题单独整理了一遍发现它们完全值得作为一个专题反复刷因为矩阵题考的不是算法复杂度而是你对二维空间的理解、对边界的敏感度以及把多维问题降维成熟悉套路的能力。这篇文章围绕 Hot 100 中的矩阵类题目把我自己的刷题顺序、核心套路和踩坑记录完整写出来。适合刚开始准备算法面试、或者已经刷了一轮数组题但对二维数组还不熟的读者如果你已经刷完一遍也可以对照我后面的常见翻车现场看看自己有没有踩过同样的坑。网上其实已经有不少 hot100 题解 PDF但矩阵这类题我建议不要只看答案一定要自己在编辑器里跑一遍因为很多细节是文字描述根本表达不出来的。1. 为什么把矩阵题单独拎出来练1.1 Hot 100 里的矩阵题到底有多少先盘一下力扣 Hot 100 中涉及矩阵的经典题目54 螺旋矩阵、48 旋转图像、73 矩阵置零、240 搜索二维矩阵 II、79 单词搜索、200 岛屿数量、221 最大正方形还有带一点矩阵背景的 238 除自身以外数组的乘积这个通常归类到前缀积以及 994 腐烂的橘子这类 BFS 网格题。加起来大概十道出头在 Hot 100 里占比不算低而且出镜率很高。这些题在面试中出现的频率非常高。我周围好几个朋友面后端、面客户端、面算法岗都遇到过矩阵置零和岛屿数量这两道题尤其是岛屿数量几乎成了 BFS/DFS 的招牌题。所以如果你时间有限矩阵专题里的这些题优先级应该排在很前面性价比非常高。1.2 矩阵题的通用共性矩阵在题目里本质就是二维数组但一旦把它当作“矩阵”来看题目就多了几何层面的约束行、列、对角线、上下左右相邻、旋转翻转。这些约束让不少同学写代码时懵掉不是因为算法不会而是因为二维坐标的概念没有建立起来。我自己总结矩阵题逃脱不了这三个要素坐标表示、遍历方向、状态记录。坐标表示决定了你怎样描述一个格子遍历方向决定了你走路的顺序比如螺旋、锯齿、按层状态记录决定了你如何避免重复访问或污染原数据。大部分矩阵题就是把这三件事想明白了代码也就顺理成章写出来了。所以我做题的时候第一件事永远是拿笔在草稿纸上画出坐标轴标好i和j的方向再开始想状态转移而不是直接打开 IDE。1.3 一个最容易忽视的坐标系问题矩阵题最常见的一个低级错误是把行和列搞反。有些同学喜欢用x和y来命名二维数组下标然后写着写着就变成matrix[y][x]一旦题目的描述里用的是“第 i 行第 j 列”代码就会彻底乱掉。我的建议是在矩阵题里统一用row和col或者i和j而且心里始终明确i是行的索引往下是增加j是列的索引往右是增加。上下左右四个方向分别对应(i - 1, j)、(i 1, j)、(i, j - 1)、(i, j 1)。这个基本盘稳了后面的所有题目才有讨论的基础。2. 矩阵题的六大核心套路2.1 遍历与模拟螺旋矩阵、旋转图像的两大代表矩阵题里最“直白”的一类就是模拟遍历。它不考你什么高深算法纯粹考你按照题目规则走的每一步是否准确以及边界条件是否完备。螺旋矩阵要求你从外到内按顺时针打印每一个元素。这道题我用的是“四个方向 边界收缩”的思路维护top、bottom、left、right四个边界按照向右、向下、向左、向上的顺序循环每走完一条边就把对应的边界向内收缩一格直到边界错位。旋转图像原地旋转 90 度则是另一个极端。我试过先转置再逐行翻转这是最简单的做法但如果要求原地O(1)额外空间就必须用四个角的循环交换。下一次我在第 3 部分会详细拆一下这个四步替换法因为它是面试官很爱深入追问的点。2.2 原地状态标记矩阵置零的高频考点矩阵置零这道题有一个非常经典的要求如果某个元素为 0则将它所在的行和列全部置为 0且要求就地完成不用额外的二维数组。很容易想到用两个集合分别记录“哪几行要清零”和“哪几列要清零”但这样做额外空间是O(m n)。进阶版要求常数额外空间这时候就需要用矩阵的第一行和第一列本身来充当标记位。核心思路是先扫描一遍矩阵把“某行要清零”的信息记在该行第一个元素把“某列要清零”的信息记在该列第一个元素然后用两个额外变量单独记住第一行和第一列自身是否需要清零。这个“用自身数据结构做标记”的思路在很多原地算法里都很有用。2.3 二分思想在二维矩阵中的应用搜索二维矩阵Hot 100 里的搜索二维矩阵 II 是一道非常妙的题。矩阵的每一行从左到右递增每一列从上到下递增不是简单的“拉平成一维数组再二分”因为拉平之后整个序列并不保证有序。正确的打开方式是每次和右上角的元素比较如果目标值小于当前元素就排除当前列如果目标值大于当前元素就排除当前行。这个过程每一步都会排除一整行或一整列所以最坏情况时间复杂度是O(m n)。我第一次见到这个解法时觉得它很像二分查找的几何化表达核心还是“有序数据 缩小搜索空间”的思想。2.4 深度优先搜索与洪泛填充岛屿类问题网格里的搜索题是矩阵题里的一大分支其中岛屿数量绝对是最经典的。这类题本质上是把二维网格当作一个图每个格子是一个节点相邻的上下左右格子是边然后统计连通分量个数。用 DFS 实现时每遇到一个未访问过的“1”就把它所在的整个岛屿通过递归访问一遍同时把访问过的格子改成“0”或者用visited数组记录。在实际面试中我更推荐用 BFS 实现因为递归深度可能很大极端情况下会爆栈。BFS 配合方向数组[(1, 0), (-1, 0), (0, 1), (0, -1)]和队列写起来非常稳定也容易解释给面试官听。2.5 动态规划在矩阵中的经典形态最大正方形矩阵题和动态规划结合的典型是最大正方形。题目要求在一个由0和1组成的二维矩阵中找到只包含1的最大正方形面积。这题的 DP 状态定义非常经典dp[i][j]表示以第i行第j列为右下角的最大正方形边长。转移方程是著名的最小值加一dp[i][j] min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) 1。为什么取三者最小值因为要形成更大的正方形左边、上边、左上三个方向必须同时满足条件任何一个方向缺一块都会导致边角缺失。这个“短板效应”的直觉非常重要很多类似题都是从它延伸出来的。2.6 图论视角把矩阵当成图前面的岛屿问题已经体现了矩阵的图论意义。进一步看很多看似复杂的矩阵题目一旦你把每个格子当成节点、把相邻关系当成边就可以自然地套用 BFS、DFS、并查集等图论工具。比如腐烂的橘子它就是典型的多源 BFS一开始有多个坏橘子每分钟向四周扩散一次问多少分钟后所有橘子都变坏或者是否存在永远无法变坏的橘子。这种情况下你会发现在矩阵里做 BFS 和在图里做 BFS 逻辑完全一样唯一的区别是邻接关系由四个方向隐式给出不需要显式建图。这个视角一旦建立你再看类似的题目就会觉得都是套路。3. 六道高频题的完整拆解与实现3.1 54. 螺旋矩阵最考验边界的模拟题螺旋矩阵的代码不长但极容易出错。我先给出一个我反复使用的版本这个版本在边界处理上非常直观def spiralOrder(matrix): if not matrix or not matrix[0]: return [] top, bottom 0, len(matrix) - 1 left, right 0, len(matrix[0]) - 1 res [] while top bottom and left right: # 从左到右 for j in range(left, right 1): res.append(matrix[top][j]) top 1 # 从上到下 for i in range(top, bottom 1): res.append(matrix[i][right]) right - 1 # 从右到左需要确认上下边界是否还有效 if top bottom: for j in range(right, left - 1, -1): res.append(matrix[bottom][j]) bottom - 1 # 从下到上需要确认左右边界是否还有效 if left right: for i in range(bottom, top - 1, -1): res.append(matrix[i][left]) left 1 return res这里最关键的细节是后两个方向的循环必须先判断top bottom和left right。如果不判断在最后一层只剩一行或只剩一列时会出现重复读取的情况。3.2 48. 旋转图像原地旋转的四步替换法旋转 90 度听起来不是一个难题但要求原地完成时很多人会绕晕。我的做法分两层思考外层是逐层处理内层是把每一层的四个角看成一组完成一次四元素循环交换。def rotate(matrix): n len(matrix) for i in range(n // 2): for j in range(i, n - 1 - i): tmp matrix[i][j] matrix[i][j] matrix[n - 1 - j][i] matrix[n - 1 - j][i] matrix[n - 1 - i][n - 1 - j] matrix[n - 1 - i][n - 1 - j] matrix[j][n - 1 - i] matrix[j][n - 1 - i] tmp理解这段代码的关键是抓住坐标旋转公式顺时针旋转 90 度后原来在(i, j)位置的元素会到(j, n - 1 - i)。所以四个位置刚好构成一个环(i, j) - (j, n-1-i) - (n-1-i, n-1-j) - (n-1-j, i) - (i, j)。沿着这个环交换即可。如果不想背这个公式还有一个保底方案先按主对角线翻转转置再左右翻转每一行。这个方案容易记但需要额外空间或两步操作面试时如果说清楚时间复杂度和空间复杂度通常也能接受。3.3 73. 矩阵置零用首行首列做标记矩阵置零的进阶解法确实需要一点巧思。思路分四步走。第一步先确定第一行和第一列自身是否包含 0用row0和col0两个布尔变量记录下来。第二步遍历除第一行和第一列之外的矩阵区域如果遇到matrix[i][j] 0就把matrix[i][0]和matrix[0][j]置为 0用第一行和第一列当“便签”。第三步根据这些“便签”把对应行和对应列清零。第四步最后根据row0和col0决定是否把第一行和第一列整个清零。这里有一个容易忽略的点必须把第一行和第一列的自身状态先保存下来再开始扫描否则你后面往第一行和第一列写标记的时候会把原来的 0 信息污染掉。我第一次做的时候就是顺序写反了测试用例全挂在包含 0 的边界行上。用第一行第一列当标记的代码我写在下面。def setZeroes(matrix): m, n len(matrix), len(matrix[0]) row0 any(matrix[0][j] 0 for j in range(n)) col0 any(matrix[i][0] 0 for i in range(m)) 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 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 if row0: for j in range(n): matrix[0][j] 0 if col0: for i in range(m): matrix[i][0] 0这道题我实际面试时被追问过“为什么不能直接边扫描边清零”原因很好理解你扫描的时候如果遇到一个 0 就把整行整列改成 0那么后面遍历到这些被改出来的 0 时会把原本不需要清的行列也清了状态直接污染。3.4 240. 搜索二维矩阵 II从右上角开始的二分搜索这道题的矩阵是“每一行左小右大每一列上小下大”抓住右上角元素matrix[0][n-1]的特性它左边都比它小下边都比它大。每次和目标值比较如果目标值比它小说明这一列都可以排除了如果目标值比它大说明这一行都可以排除了。def searchMatrix(matrix, target): if not matrix or not matrix[0]: return False m, n len(matrix), len(matrix[0]) i, j 0, n - 1 while i m and j 0: if matrix[i][j] target: return True elif matrix[i][j] target: j - 1 else: i 1 return False这个解法的精妙之处在于每次比较都能排除一整行或一整列最坏情况走m n步。注意不是从左上角开始左上角是最小值你没法判断该往右还是往下只有右上角或左下角能提供明确的方向指引。3.5 79. 单词搜索DFS回溯的模板题单词搜索要求在一个字符矩阵中找出某个单词是否存在。每个格子只能用一次且路径必须相邻。做法是枚举每个格子作为起点DFS 匹配单词。DFS 里维护当前坐标和单词匹配到的位置每次向四个方向搜索搜索前先把当前格子标记为已访问搜索结束后再恢复。这个恢复动作就是回溯的精髓要保证不同路径之间互不干扰。def exist(board, word): m, n len(board), len(board[0]) dirs [(1, 0), (-1, 0), (0, 1), (0, -1)] def dfs(i, j, k): if k len(word): return True if i 0 or i m or j 0 or j n or board[i][j] ! word[k]: return False board[i][j] # for di, dj in dirs: if dfs(i di, j dj, k 1): return True board[i][j] word[k] return False for i in range(m): for j in range(n): if dfs(i, j, 0): return True return False我用#代替临时 visited 数组省去了额外空间。但这招只在同一个格子不能重复用的题里适用如果题目允许重复访问就不能这样标记了。另外DFS 的递归深度最长等于单词长度不会超过矩阵总格数所以一般不用担心爆栈但如果你遇到超长的单词改成 BFS 或显式栈也可以。3.6 221. 最大正方形DP思路的推导最大正方形的代码非常短但推导过程值得展开。def maximalSquare(matrix): if not matrix: return 0 m, n len(matrix), len(matrix[0]) dp [[0] * n for _ in range(m)] max_side 0 for i in range(m): for j in range(n): if matrix[i][j] 1: if i 0 or j 0: dp[i][j] 1 else: dp[i][j] min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) 1 max_side max(max_side, dp[i][j]) return max_side * max_side理解min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) 1的方式我建议直接画一个 3x3 的全 1 矩阵手工推一遍 dp 值第一行全是 1第一列全是 1到了中间格子左边是 1、上边是 1、左上也是 1三者最小值 1 加 1 得到 2代表以它为右下角能形成 2x2 的正方形。如果三格中有任何一个值是 0说明左侧、上方或左上区域里有一个格子不是 1这个位置就不可能构成更大的正方形。4. 常见的翻车现场与排查技巧4.1 行列坐标搞反越界提示让你怀疑人生矩阵题的报错常有IndexError: list index out of range。我排查这一类错误时第一反应不是去看循环是不是没写对而是去看我到底是先取行还是先取列。比如matrix[i][j]i必须小于mj必须小于n。如果你写成matrix[j][i]而m和n不相等那就一定越界。解决办法很简单写代码之前先注释一行# i: row, j: col并且强制自己在所有矩阵题里保持一致不要在螺旋矩阵用i表示列、搜索二维矩阵又用i表示行。4.2 边界条件少写一个等号螺旋矩阵多走一个数螺旋矩阵的经典报错是输出结果比预期多一个元素。我印象最深刻的一次是在for j in range(left, right 1)里少写了 1结果每一层少了最后一个元素一单测就原形毕露。另一个高频错误是在从右到左和从下到上的遍历里忘了加top bottom和left right的判断。在矩阵行数不等于列数或者层数为奇数时边界会提前交错如果你还用旧的边界继续遍历就会重复读取已经访问过的格子。推荐每次写完螺旋矩阵后用三个用例测试一行、一列、3x4 这种非方形矩阵。4.3 原地修改导致状态污染矩阵置零的连环雷矩阵置零最常见的翻车点是没用标记直接边遍历边置零然后碰到自己新写入的 0 又触发新的置零最后整个矩阵全变成 0。还有一个进阶雷区当使用第一行和第一列做标记时如果第一行本身有 0而你在标记阶段又把matrix[0][j]改了那就会丢失第一行原本的状态。所以我在代码里保留row0和col0两个变量专门存第一行和第一列的原始状态这个习惯帮我避了很多次雷。4.4 Python 版本特有的坑浅拷贝与负数下标用 Python 做矩阵题时新的dp数组要用列表推导式生成比如[[0] * n for _ in range(m)]千万不要写成[[0] * n] * m。后者每一行都是同一个对象的引用改一个元素整列跟着变。这是 Python 新手在矩阵 DP 题里最容易踩的坑没有之一。负数下标同样需要注意。matrix[i][j - 1]在j 0时并不会报错因为 Python 会把-1当成最后一个元素但这个“灵活”恰恰会掩盖边界错误让程序在你完全没察觉的情况下输出错误答案。所以循环里涉及j - 1的逻辑一定要检查j 0的情况。4.5 调试矩阵题的实用技巧我调试矩阵题的固定流程是先写一个打印矩阵的工具函数把每一步处理后的矩阵打出来肉眼观察变化是否符合预期。比如螺旋矩阵我在每个方向的遍历结束后打印一次top、bottom、left、right四个边界值错误马上就能定位。如果是动态规划题我会把整个dp表打印出来逐行检查状态是怎么转移的。另外我强烈建议在 IDE 里用断点而不是只靠print排查复杂 DFS。因为 DFS 的调用栈层级很深print只能看到进入顺序断点可以看到递归返回时每一步的现场。特别是 79 单词搜索这种回溯题现场调试比事后分析高效太多。5. 从刷题到实际场景的延伸认知5.1 混淆矩阵与模型评估很多人刷完矩阵题后来又在机器学习里碰到另一个带“矩阵”的东西——混淆矩阵。它其实就是二维表格行是真实类别列是预测类别四个格子分别对应 TP、FP、FN、TN精确率和召回率都是从这张表里算出来的。我第一次写 Python 多分类混淆矩阵代码时发现 sklearn 里的confusion_matrix函数返回的就是一个二维数组底层同样离不开索引、遍历和统计。其实很多所谓的“矩阵”应用本质上都没有脱离二维数组的操作范畴这也是为什么刷算法题里的矩阵题对后续学任何领域的矩阵应用都有帮助。5.2 单应性矩阵与图像变换图像处理里的单应性矩阵是另一个热门概念。图像上的坐标变换、透视矫正、全景拼接很多地方都会用到 3x3 的单应性矩阵。刷题时学的旋转图像只是 90 度整数倍旋转而真实图像变换里是任意角度的线性变换底层需要矩阵乘法、矩阵求逆等线性代数工具。这就引出一个感受Hot 100 的矩阵题只是开了个门。如果你后续做图形学、做自动驾驶、做音视频处理会遇到更多围绕矩阵的复杂话题比如旋转矩阵的多次叠加、矩阵特征值分解做 PCA 降维。但基础的二维坐标直觉一定是从遍地是坑的矩阵模拟题里练出来的。5.3 分块矩阵求逆与数值计算的底层逻辑刷题之外矩阵论里还有一个逃不开的话题是分块矩阵求逆。把大矩阵分成若干小矩阵块利用块状结构简化求逆运算在原位更新和数值计算中很常见。听起来很高大上但底层仍然是在二维数组上做行列索引和状态运算。我个人的体会是如果你计划做科学计算、图形学、机器学习系统这类方向刷题时矩阵题的遍历和边界处理能力就是一种很踏实的铺垫。不要觉得这些题太基础没用很多高级库的底层实现就是要把这些细节抠到极致不然算一次大矩阵乘法就可能偏离正确答案。5.4 矩阵键盘与嵌入式里的异曲同工再把视角拉远一点嵌入式和硬件方向同样有矩阵。单片机里的矩阵键盘、LED 扫描本质是拿有限的 IO 口驱动更多的按键或灯珠行列交叉扫描每一行检测每一列的电压变化。二维数组的索引和遍历在这里变成了硬件的行列扫描逻辑。我没怎么写过单片机代码但看过相关的实现后发现它的核心就是“行索引 列索引 遍历方向”那套思路。所以矩阵这类题目刷明白 54 和 200 以后再去看这些行业应用至少不会觉得陌生。收尾的几句实话Hot 100 里的矩阵题确实不难但没有哪一类题像它一样能把坐标系意识、边界判断、原地操作、图遍历和 DP 状态设计全部揉在一起。我刷完这一组之后最大的变化是拿到任何带坐标的题目都会先花十秒确认行列方向写循环时习惯性检查边界处理二维数组初始化也永远不会再用[[0] * n] * m。如果你现在刚开始接触这类题别急我建议按这样的顺序来先做 54 螺旋矩阵和 48 旋转图像把坐标操作练熟再做 73 矩阵置零感受原地标记的技巧然后花时间啃 79 单词搜索和 200 岛屿数量把 DFS/BFS 的模板背下来最后尝试 221 最大正方形把 DP 的状态定义理清楚。这个过程不需要一天完成但每做完一道都可以顺手在草稿纸上把坐标图和状态转移图重画一遍。画着画着你会发现矩阵题翻来覆去就是那几板斧真正难的从来不是矩阵而是你愿不愿意在动手前先把问题想清楚。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →