☰
LeetCode矩阵题全攻略:从边界控制到降维思维
2026/10/10 4:20:31 网站建设 项目流程

1. 矩阵篇的整体思路与题型拆解

矩阵类题目在 LeetCode Hot 100 中占比不算高,但出镜率相当稳定,属于“考逻辑多于考算法”的一类题。说白了,矩阵就是一个二维数组,但正因为多了一个维度,很多一维数组里很自然的操作,放到二维场景里就变得容易出错:遍历方向搞乱、坐标越界、原地修改覆盖了后续需要的原值,这些都是高频翻车点。

我见过不少同学刷到这里会觉得很别扭,明明看题解能看懂,自己一写就废。核心原因不是代码能力差,而是脑子里没有建立一套“二维坐标系”的思维方式。矩阵题的本质是坐标系操作,你要随时知道当前在什么位置、下一步往哪个方向走、边界在哪里、哪些格子已经被处理过了。把这四件事想清楚,矩阵题一大半的难度就消失了。

从 Hot 100 的实际构成来看,矩阵篇覆盖的题型大致可以分成三类:第一类是遍历与顺序控制,典型代表是螺旋矩阵、旋转图像,这类题不考复杂算法,考的是你对边界条件的敏感度;第二类是状态标记与原地修改,典型代表是矩阵置零,核心难点在于如何在 O(1) 额外空间下完成标记,还不能丢失原有信息;第三类是利用矩阵自身性质进行搜索或转化,典型代表是搜索二维矩阵、矩阵中的最长递增路径,这类题往往会跟二分、DFS、动态规划结合,难度梯度也相对更大。

如果目光放长远一点,矩阵题在真实面试里往往不只是考一道题本身,考官更看重的是你面对一个陌生数据结构时,能不能快速建立模型、拆解子问题、控制边界。所以刷矩阵篇,我建议你不是背题,而是把每一道题背后那个“二维操作模板”抽出来,形成肌肉记忆。

我不想一上来就给一堆题号和题解,那样太像资料汇编了。我更想先带你建立一套处理矩阵问题的底层层逻辑:看到题目先判断该用什么“走法”,然后再谈具体实现。这套逻辑建立起来之后,上面提到的三类题型你基本都能快速找到下手点。

2. 四大高频题型的核心方法论与代码模板

矩阵篇最值得花时间的是这四道题——矩阵置零、螺旋矩阵、旋转图像、搜索二维矩阵。它们分别代表了四种最核心的二维操作模型:标记复用、边界收缩、坐标变换、坐标趋近。把这四个模型吃透,Hot 100 矩阵篇的主干就算拿下了。

2.1 矩阵置零:第一行第一列当标记位

题目要求很简单:如果矩阵里某个元素是 0,那么它所在的行和列全部置为 0。最容易想到的做法是开两个数组,分别记录哪些行、哪些列需要清零,但这样额外空间是 O(m+n)。进阶要求是 O(1) 额外空间,这时候就得动点脑筋了。

核心思路是“标记复用”——用矩阵的第一行和第一列来充当记录数组。具体做法分三步走:先扫描第一行和第一列,用两个布尔变量记住它们自身原本是否包含 0;然后从第二行第二列开始遍历剩下的区域,只要遇到matrix[i][j] == 0,就把matrix[i][0]和matrix[0][j]置为 0,相当于在边界上做标记;最后根据这些边界标记,把对应行和列整体清零,再回头处理第一行和第一列。

这里有一个特别容易踩的坑:必须先处理第一行第一列的原始状态,再去做标记。如果一上来就扫描全矩阵,第一行第一列的原始信息就被覆盖掉了。我第一次写的时候没有单独记录,结果第一行里原本有 0 的情况,处理完边界之后整行数据全丢了,排查了半天才发现是顺序问题。

代码模板如下:

def setZeroes(self, matrix: List[List[int]]) -> None: m, n = len(matrix), len(matrix[0]) first_row_zero = any(matrix[0][j] == 0 for j in range(n)) first_col_zero = 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 first_row_zero: for j in range(n): matrix[0][j] = 0 if first_col_zero: for i in range(m): matrix[i][0] = 0

这段代码里清零的过程,我建议从第二行第二列开始,不要动标记本身所在的第一行和第一列,否则会出现“清了标记导致后续判断失效”的连锁问题。这个问题在面试里经常被拿来追问,答上来就是加分项。

2.2 螺旋矩阵:四指针边界收缩法

螺旋矩阵这道题,与其说是算法题,不如说是“操作契约题”。你只需要按“右、下、左、上”的顺序一圈一圈往里走,每走完一条边就把对应的边界往里缩一格,直到所有元素都被访问过。

我自己的写法是用四个变量top, bottom, left, right维护当前未遍历区域的边界,然后在一个while循环里执行四次遍历。这里最关键的点是每次遍历完都要检查边界是否已经交错,一旦top > bottom或者left > right,立即退出循环,否则单行或单列的矩阵会重复读取元素。

比如上面一行往右走完,top += 1,此时如果top > bottom,说明矩阵已经全走完了,下面那几步就直接不用执行了。很多人的实现会在这里报IndexError,就是因为单行矩阵往下走的时候,边界检查做晚了。

def spiralOrder(self, matrix: List[List[int]]) -> List[int]: res = [] top, bottom = 0, len(matrix) - 1 left, right = 0, len(matrix[0]) - 1 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 or left > right: break # 向左遍历下边界 for j in range(right, left - 1, -1): res.append(matrix[bottom][j]) bottom -= 1 # 向上遍历左边界 for i in range(bottom, top - 1, -1): res.append(matrix[i][left]) left += 1 return res

这套四指针边界收缩的模板,本身就是一个非常通用的二维模型。后面遇到任何“按层处理矩阵”的题都可以直接复用。还有一个变体是逆时针螺旋,写法完全一样,只要把四个方向的顺序反过来就行。

2.3 旋转图像:两次轴对称替代一次旋转

旋转 90 度这道题,如果直接按坐标去搬元素,很容易把自己绕晕,因为一个元素移动之后会连锁影响四个位置。业界最常用的技巧是先转置再左右翻转,两步合起来就是顺时针旋转 90 度。从数学上讲,转置交换了行列坐标,左右翻转再反转列序,两者的复合效果恰好等价于旋转。

具体来说,第一步把matrix[i][j]和matrix[j][i]交换,遍历范围只需要上半三角;第二步把每一行的元素以中线为轴左右对调。两步操作都是按行独立进行的,逻辑简单而且不容易出错。

def rotate(self, matrix: List[List[int]]) -> None: n = len(matrix) # 第一步:转置 for i in range(n): for j in range(i + 1, n): matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j] # 第二步:每行左右翻转 for i in range(n): matrix[i].reverse()

为什么我不建议直接硬算旋转坐标?因为直接旋转的坐标映射是(i, j) -> (j, n-1-i),中间需要额外的临时变量维护,很容易做重或漏做。而转置加翻转的每步操作非常直观,现场写代码几乎不会出错,面试时也更容易讲清楚逻辑。顺带说一句,如果是逆时针旋转 90 度,做法是先上下翻转再转置,顺序别搞反了。

2.4 搜索二维矩阵:从右上角开始走

搜索二维矩阵在 Hot 100 里有两个版本:一个是完全有序的矩阵(每行每列均递增),另一个是每行有序、每行第一个元素大于上一行最后一个元素(本质退化成了一维有序数组的二分搜索)。后者用二分就能解决,重点说一下前者。

每行每列都递增这个性质非常特殊,它意味着从矩阵的右上角看出去,向左的所有元素都比当前值小,向下的所有元素都比当前值大。于是这里天然形成了一条“二分决策路径”:目标值比当前元素小就往左走,目标值比当前元素大就往下走。每一步都能排除一整行或一整列,最坏情况下总共走 m+n 步,复杂度 O(m+n)。

def searchMatrix(self, matrix: List[List[int]], target: int) -> bool: 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

这个“角点起步”的思路,实际上是一种贪心式的坐标趋近模型。顺着这个模型还能推导出很多变体,比如从左下角出发也可以,只是判断方向要反过来。面试时如果考官追问“还能怎么优化”,可以从右上角思路延伸到二分,但不要画蛇添足,先把基本解法讲清楚才有讨论空间。

3. 经典真题复盘与代码实现细节

前面四种模型属于矩阵篇的“基本功”,真正能拉开差距的,是把二维模型和更深的算法结合起来。这一节我挑两道覆盖面广、面试出现频率也比较高的题来完整复盘,一道考图的连通性,一道考降维思维。

3.1 被围绕的区域:从边界反向遍历

这道题的核心难点不在于 DFS 本身,而在于它的正向思维是陷阱。如果直接从内部的 O 出发去判断是否被 X 包围,你必须对每个 O 做一次全连通检查,然后还要回溯修改,非常复杂。但换个角度想:所有没有被 X 包围的 O,一定是从边界上的 O 出发能连通到的。换句话说,先找出边界相连的 O 并保护起来,剩下的 O 就必然是包围的,直接改掉就好。

我在落地时用了“染色标记法”:先从边界上的每一个 O 出发 DFS,把能连通到的 O 临时标记成#;全部标记完之后,再遍历全矩阵,遇到#就还原成O,遇到残留的O就替换成X。这个思路简洁可靠,而且只做一次全局遍历加若干次方向 DFS,时间上是最优的。

def solve(self, board: List[List[str]]) -> None: if not board or not board[0]: return m, n = len(board), len(board[0]) def dfs(i, j): if i < 0 or i >= m or j < 0 or j >= n or board[i][j] != 'O': return board[i][j] = '#' dfs(i - 1, j) dfs(i + 1, j) dfs(i, j - 1) dfs(i, j + 1) # 从边界出发标记所有可连通的 O for i in range(m): dfs(i, 0) dfs(i, n - 1) for j in range(n): dfs(0, j) dfs(m - 1, j) # 统一替换 for i in range(m): for j in range(n): if board[i][j] == '#': board[i][j] = 'O' elif board[i][j] == 'O': board[i][j] = 'X'

这道题如果矩阵规模很大,递归 DFS 有爆栈风险,商业产品里我建议改成显式栈的迭代 DFS。面试时写递归版本没问题,但如果面试官问“矩阵特别大怎么办”,能答出“递归转显式栈或 BFS”就是加分点。还有一个小细节:DFS 的递归里我先判断board[i][j] != 'O'而不是== 'O',这样可以省掉独立的 visited 数组,用就地标记解决了“哪些格子访问过”的问题。

3.2 最大矩形:矩阵降维成柱状图

最大矩形是矩阵篇里综合难度较高的一道题,但它本质上是一个经典的降维问题:逐行累积“当前格子往上连续有多少个 1”,然后就变成了“每一行的柱状图里找最大矩形面积”,也就是 LeetCode 84 题。84 题用单调栈求最大矩形面积是模板级解法,O(n),外层再套一层行遍历,总体 O(m * n)。

我第一次做这题的时候没有想通降维这件事,自己硬写了一个二维滑动窗口,边界判断多到崩溃。后来把“每一行的高度数组”打出来看,瞬间就明白了:上一行的高度如果当前行是 0 就要清零,否则高度加一。这个累积过程本身就是动态规划,只是它藏得比较浅。

单调栈部分的代码是核心:

def maximalRectangle(self, matrix: List[List[str]]) -> int: if not matrix or not matrix[0]: return 0 m, n = len(matrix), len(matrix[0]) heights = [0] * n max_area = 0 for i in range(m): for j in range(n): if matrix[i][j] == '1': heights[j] += 1 else: heights[j] = 0 stack = [] # 加入哨兵,简化收尾处理 for k in range(n + 1): cur = heights[k] if k < n else 0 while stack and heights[stack[-1]] > cur: h = heights[stack.pop()] left = stack[-1] if stack else -1 area = h * (k - left - 1) max_area = max(max_area, area) stack.append(k) return max_area

这里有一个实用技巧:在柱状图数组末尾加一个高度为 0 的“哨兵柱”,这样遍历结束后栈里剩余的元素能自动完成出栈结算,不需要再写一个 while 循环单独处理栈内残余。很多题解里没有这一步,导致代码里要多一段很丑的收尾逻辑。我强烈建议你把哨兵技巧记下来,因为它在很多栈相关的算法里都通用。

3.3 拓展:二维前缀和快速求子矩阵和

除了上面两道,矩阵篇还经常出现一类“求子矩阵和、子矩阵最大和”的问题,它们的通用预处理手段是二维前缀和。二维前缀和数组pre[i][j]表示从(0,0)到(i,j)围成矩形区域的所有元素之和,递推公式是pre[i][j] = pre[i-1][j] + pre[i][j-1] - pre[i-1][j-1] + matrix[i][j]。这里多减一次pre[i-1][j-1],是因为左上方那部分被加了两次,需要抵消。

有了前缀和数组,任意子矩阵(r1,c1)到(r2,c2)的和就能在 O(1) 时间内求出。虽然 Hot 100 矩阵篇里直接出前缀和的题目不算多,但很多后续的难题(比如动态规划优化、二维滑动窗口)都会默认你知道这个技巧。提前备好,后面刷题会轻松很多。

4. 矩阵题的刷题顺序与避坑指南

这个部分是我最想跟你分享的。刷题刷多了你会发现,矩阵题的套路其实非常有限,真正决定你能不能写出满分代码的,往往是一些容易被忽略的边界细节和代码习惯。

4.1 我认为最高效的刷题顺序

如果你打算集中刷矩阵篇,按下面这个顺序来效率最高:先刷矩阵置零和螺旋矩阵,这两道题能帮你建立“二维坐标感”和“边界控制感”;然后刷旋转图像,掌握坐标变换模型;接着刷搜索二维矩阵,理解如何利用矩阵有序性优化搜索;再刷被围绕的区域和岛屿数量,练习 DFS/BFS 与二维 visited 的各种标记方案;最后挑战最大矩形,体会降维思维在矩阵题中的威力。

这样安排的原因很简单:前几道题是模板,后面的题是模板的组合或者升级。基础没打牢就冲最大矩形,大概率会在“降维”这一步卡很久。我见过不少人是反过来刷的,先做最大矩形做不出来,心态崩了,回头才发现前面的基础题都没吃透。

4.2 高频报错点与边界条件速查表

根据我自己的刷题记录和帮别人 review 代码的经验,矩阵题的高频错误基本集中在下面几个位置,我整理成了一张表:

常见错误出现的题型根因分析解决方案解决方案
行和列的下标搞混所有矩阵题坐标轴意识不强,把matrix[i][j]当成行列都对每次循环前先确认 i 是行还是 j 是行,或直接改名row, col
单行矩阵读取越界螺旋矩阵遍历完一行后没有及时检查边界每次收缩边界后立刻判断top > bottom或left > right
遍历范围多算了半圈旋转图像/转置双层循环范围写成了全矩阵只遍历上半三角,即j从i+1开始
原地修改导致原始值丢失矩阵置零用原矩阵存储标记时覆盖了信息标记区和数据区分离,或者从右下角开始处理
DFS 死循环被围绕的区域/岛屿数量缺少 visited 标记或标记时机不对在入栈/入队前就标记,不要等到出栈才标记
柱状图栈底残余未处理最大矩形循环结束没有清空单调栈数组后追加哨兵 0,让所有元素自然结算

4.3 面试现场的讲题节奏建议

矩阵题在面试里通常不难,能让考官眼前一亮的不是你写出正确答案,而是你展现出的结构化拆解能力。我在面试别人和模拟面试的时候,比较认可这样一套表达节奏:拿到题先不要急着写代码,用 30 秒到 1 分钟说清楚“我看到一个二维矩阵题,敏感点是边界处理和状态标记。我的第一反应是用 XX 模型来处理,最坏时间复杂度是 XX,额外空间是 XX”。然后边说边写。

以旋转图像为例,比较好的表述是:“这题我不用直接旋转坐标,先转置再左右翻转,两步都是二维数组的线性操作,不会互相干扰。转置时只遍历上半三角避免重复交换。整体时间复杂度 O(n^2),额外空间 O(1)。”这段话一说出来,考官就知道你平时做题确实总结过,印象分会提高不少。

还有一个我个人的小习惯:面试时写矩阵题的循环边界,先在草稿纸上标一遍“这个 range 的起点和终点”,不要急着下笔。很多边界错误在写循环之前就能被消除,值得花那十几秒。另外,如果你的解法里出现了不止一个嵌套循环,每层循环尽量用带语义的变量名(row, col, top, bottom, left, right),不要清一色用i, j。矩阵题很容易因为 i 和 j 的意义在不同代码段里发生变化而出错,清晰命名能帮你自己和读你代码的人少受折磨。

最后再多说一句。矩阵题刷到后面,你会发现它们考察的并不是高深的数学,而是“确定性问题”——确定下一步往哪走、确定哪些格子已经处理、确定状态之间如何转换。这类能力在真实开发里也非常有用,因为所有二维表格、网格地图、图像像素的操作,本质上都是矩阵操作。把这几道题刷扎实,你练到的不仅是面试技巧,还有处理复杂二维数据结构的底层功底。以后遇到再大的网格问题,心里有了模型,下手就不慌了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询