ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

螺旋矩阵题解:四指针法与方向控制法的边界细节与变体实战

螺旋矩阵题解:四指针法与方向控制法的边界细节与变体实战 在LeetCode打卡刷到第54题螺旋矩阵时大部分人的第一反应是“这题不难按题意模拟就行”。结果真到IDE里写完跑测试或者在面试白板上手写时经常出现两种典型翻车一种是四个方向的边界算懵元素重复读或直接越界另一种是代码只适配正方形矩阵换成矩形立刻数组下标越界。我在面试中见过不少候选人栽在这道看似简单的题上它其实非常考验循环不变量的控制能力和边界条件思维。这篇文章不以“能AC”为终点而是把螺旋矩阵的题目本质、两种主流解法、边界细节、变体扩展和实战避坑经验完整拆开帮你在面试和日常刷题中都真正吃透这类题。1. 螺旋矩阵到底在考什么从题目表象到核心考点1.1 一个高频题目家族很多人以为螺旋矩阵就是LeetCode 54这一道题其实它是一整个题目家族LeetCode 54给定一个m×n矩阵按顺时针螺旋顺序返回所有元素。LeetCode 59给定正整数n生成一个包含1到n²所有元素且元素按顺时针螺旋排列的n×n矩阵。LeetCode 885给定R行C列的矩阵从(r0, c0)出发按顺时针螺旋顺序返回矩阵中的所有坐标路径会超出矩阵边界。剑指Offer 29 / 牛客网高频题顺时针打印矩阵与54本质一致多出现在国内公司面试手写题中。把这四道题放在一起看会发现它们共享同一套核心思维确定遍历方向、判断何时转向、管理访问边界。只要把底层逻辑理清楚四道题可以用同一套代码思路顺手解决。1.2 面试官为什么偏爱这道题螺旋矩阵不涉及复杂的贪心、动态规划或高级数据结构代码量适中十几行能写完却能在短时间内暴露出程序员的几个关键素质边界意识矩阵的行列数是否相等、是否为空、是否为单行单列都会直接影响结果。循环不变量的掌控每一圈遍历结束后四个方向的边界如何准确收缩不会重读、漏读。代码的简洁性有人写几十行还漏洞百出有人十几行干净利落高下立判。换句话说螺旋矩阵是性价比极高的面试考察题。这也是为什么大厂面试官总爱在热手阶段抛出它。弄懂它等于给“边界控制”这类题打下一个通用框架。2. 解法一按层模拟的完整推导与正确写法2.1 四指针法的核心思想按层模拟是我个人最推荐的第一种解法。思路非常直观把当前还没遍历的矩阵看成一块矩形区域用四个指针记录这块区域的边界每读完外圈的一层边界就往内收缩一次直到区域消失。四个指针的含义top当前区域最上边的行号。bottom当前区域最下边的行号。left当前区域最左边的列号。right当前区域最右边的列号。遍历的循环条件就是top bottom且left right只要还有未遍历区域就继续。每次循环完整读取当前外圈的四条边然后收缩指针。2.2 代码实现与逐行解读直接看代码。我以Java为例因为这是国内面试最常见的语言class Solution { public ListInteger spiralOrder(int[][] matrix) { ListInteger res new ArrayList(); if (matrix null || matrix.length 0 || matrix[0].length 0) { return res; } int top 0, bottom matrix.length - 1; int left 0, right matrix[0].length - 1; while (top bottom left right) { // 1. 上边从左到右 for (int col left; col right; col) { res.add(matrix[top][col]); } top; // 2. 右边从上到下 for (int row top; row bottom; row) { res.add(matrix[row][right]); } right--; // 3. 下边从右到左需要判断是否还存在 if (top bottom) { for (int col right; col left; col--) { res.add(matrix[bottom][col]); } bottom--; } // 4. 左边从下到上需要判断是否还存在 if (left right) { for (int row bottom; row top; row--) { res.add(matrix[row][left]); } left; } } return res; } }逐段拆解上边遍历完后top因为这一行已经全部读取下一次各方向的遍历都不应该再碰它。右边遍历完后right--同理这一列已经被取走。重点来了为什么下边和左边要加if判断而上边和右边不用因为每轮循环进入时已经保证top bottom且left right上边和右边一定存在。但是做完上边和右边的收缩后现场可能只剩一行或只剩一列。比如一个3×3矩阵去掉上边后还剩2行去掉右边后还剩2列此时下边和左边都存在可以安全读取。但如果是3×1矩阵三行一列上边读完top变成1右边读完right变成0此时top bottom依然成立1 2但left right已经不成立0 0其实等于成立这里我扩展说明。等等让我再仔细推导一下3×1的例子。3×1矩阵初始top0bottom2left0right0。第一次循环上边col从0到0读matrix[0][0]。top后top1。右边row从1到2读matrix[1][0]、matrix[2][0]。right--后right-1。下边判断top bottom1 2成立但此时right-1left0for循环col从-1到0不执行。bottom-- bottom1。左边判断left right即0 -1不成立跳过。left不执行。while条件top bottom1 1成立left right0 -1不成立。退出。结果正确。这里的关键是即使下边判断通过内部的for循环也可能因为边界错乱而不执行只是没有副作用。但如果没有if判断在单纯只有一行1×3矩阵的情形下会发生重复读取1×3矩阵初始top0bottom0left0right2。第一次循环上边col从0到2读1、2、3。top后top1。右边row从1到0不执行。right--后right1。下边如果不加ifcol从1到0读matrix[0][1]值为2这就重复读了。加了if后topbottom即10不成立直接跳过bottom--完美规避。这正是四指针法最经典的坑。我把它单列出来讲因为这个坑能解释90%以上的人第一次写的螺旋矩阵为什么会出错。2.3 为什么四指针法在空间上更优按层模拟没有使用额外标记数组只用了常数级别的指针变量因此额外空间复杂度是O(1)。结果数组是题目要求返回的内容不计入额外空间。而第二种解法需要借助visited矩阵空间是O(m×n)。别看只是多一个二维布尔数组有些面试官会刻意要求“能否用O(1)额外空间实现”按层模拟就是标准答案。3. 解法二方向控制法的适用场景与代码模板3.1 用方向数组替代边界收缩第二种思路是模拟“人”的走法从起点出发一直朝当前方向走走不动了越界或撞上已访问位置就顺时针转向。这种思路对初学者更直觉也更容易扩展到各种复杂螺旋变体。方向数组设计如下顺序为右、下、左、上正好对应顺时针dirs [(0, 1), (1, 0), (0, -1), (-1, 0)]每次尝试前进时先计算下一步坐标如果越界或目标位置已经被访问过就把方向索引加一取模再计算一次新的下一步坐标。3.2 Python实现与关键判断class Solution: def spiralOrder(self, matrix: List[List[int]]) - List[int]: if not matrix or not matrix[0]: return [] m, n len(matrix), len(matrix[0]) visited [[False] * n for _ in range(m)] res [] # 右、下、左、上顺时针 dirs [(0, 1), (1, 0), (0, -1), (-1, 0)] row, col 0, 0 di 0 for _ in range(m * n): res.append(matrix[row][col]) visited[row][col] True nr row dirs[di][0] nc col dirs[di][1] if nr 0 or nr m or nc 0 or nc n or visited[nr][nc]: di (di 1) % 4 nr row dirs[di][0] nc col dirs[di][1] row, col nr, nc return res核心逻辑只有三点走够m×n步就一定遍历完所有元素。每次转向只允许转一次不会出现原地转两圈还找不到路的情况。visited数组是防止回头的关键也是额外空间开销的来源。3.3 两种解法怎么选对比维度按层模拟四指针方向控制法时间复杂度的常数较小直接按方向遍历稍大多次数组访问额外空间O(1)O(m×n)的visited直觉友好度需要对边界收缩有感觉更贴近“走路”的直觉边界判断细节上下左右四条边需要分别处理统一判断越界与访问扩展复杂螺旋较弱改造成本高强适合螺旋III这类变形我的建议是第一遍刷题用方向控制法理解螺旋过程第二遍整理笔记时把按层模拟作为主打解法。如果准备面试两种都掌握主动向面试官说明空间复杂度的差异是很加分的表现。4. 边界条件才是螺旋矩阵的真正分水岭4.1 单行、单列与1×1矩阵这三类输入是检验代码是否严谨的试金石。很多人拿3×3、4×4的矩阵测试全过一提交就翻车往往就是栽在单行单列上。单行矩阵1×5按层模拟中上边把整行读完top后topbottom导致下边判断失效不会重复读。单列矩阵5×1右边把整列读完right--后leftright左边判断失效不会重复读。1×1矩阵上边读唯一元素top后topbottom右边循环row从1到0不执行后续判断全部失效优雅退出。方向控制法在这三类输入上天然安全for循环只跑m×n次visited保证不重走。这也是很多人觉得方向控制法“不容易出错”的原因代价是空间换来了逻辑统一。4.2 奇偶边长与中心元素的归属如果遍历n×n矩阵n为奇数时正中心会有一个单独的元素n为偶数时整个矩阵可以按圈剥完没有孤立中心点。按层模拟中中心元素是在“最后只剩一行或一列”的那一轮被读取的。如果你把四条边的读取范围都写成半开区间比如上边只读到right-1就会在奇数矩阵的正中心漏掉一个元素甚至导致某些用例直接错误。4.3 空矩阵与非法输入的防御函数开头建议统一做三层防御if (matrix null || matrix.length 0 || matrix[0].length 0) { return res; }注意matrix[0].length的判断必须放在matrix.length 0之后否则空矩阵会触发数组越界。这个顺序错乱的bug在面试现场特别常见属于典型的“看起来小但致命”的问题。4.4 循环不变量的口诀我刷题多年总结出一个口诀每条边读满读完就收缩判断后两边循环看区间。具体说就是上、右、下、左四条边都从当前区间的一端读到另一端含头含尾每读完一条边立即收缩对应指针下边和左边在读取前做存在性判断整个while循环只看top bottom left right。这个口诀基本可以帮你写出不出错的四指针版本。5. 变体题怎么用同一套思路打穿5.1 螺旋矩阵II由读到写的对称反转LeetCode 59要求生成矩阵而不是读取但它和54的循环结构完全对称。区别仅在于54从矩阵中取元素加入结果列表59按螺旋顺序把递增数字填入矩阵。class Solution { public int[][] generateMatrix(int n) { int[][] matrix new int[n][n]; int top 0, bottom n - 1; int left 0, right n - 1; int val 1; while (top bottom left right) { for (int col left; col right; col) { matrix[top][col] val; } top; for (int row top; row bottom; row) { matrix[row][right] val; } right--; if (top bottom) { for (int col right; col left; col--) { matrix[bottom][col] val; } bottom--; } if (left right) { for (int row bottom; row top; row--) { matrix[row][left] val; } left; } } return matrix; } }这道题面试出现的频率同样很高很多公司会把它作为54题的追问。如果你能写出54后面试官问“能不能反过来生成矩阵”你只需要把结构镜像一下即可答得又快又稳。5.2 螺旋矩阵III步长规律与方向控制的结合LeetCode 885的螺旋不是等圈收缩而是不断向外扩张步长序列是1、1、2、2、3、3……方向控制法在这里展现出巨大的扩展优势。核心思想是每一步走完后检查坐标是否落在矩阵内是则记录每走完两个方向步长加一。class Solution: def spiralMatrixIII(self, rows: int, cols: int, rStart: int, cStart: int) - List[List[int]]: ans [[rStart, cStart]] dirs [(0, 1), (1, 0), (0, -1), (-1, 0)] r, c rStart, cStart step 1 di 0 while len(ans) rows * cols: for _ in range(2): for _ in range(step): r dirs[di][0] c dirs[di][1] if 0 r rows and 0 c cols: ans.append([r, c]) di (di 1) % 4 step 1 return ans这里有几个容易错的地方走的路径包含矩阵外坐标不能因为“走出去了”就停止或转向必须继续按螺旋走。步长每两个方向增加一格而不是每个方向都增加。while循环判断条件用“已收集数量等于矩阵总元素数”而不是”路径走完“因为路径可能延伸到很远。5.3 面试中的沟通策略我强烈建议在面试中先和面试官确认几个前提条件矩阵是否有空、m和n的范围、是否需要O(1)空间。这些问题不是多余而是能体现你的工程思维。确认完输入约束再动手会避免不少误解。手写过程中主动说出思路“我用四个指针维护当前未遍历区域每读完一条边收缩一次下边左边读之前做存在性判断保证不重不漏”面试官对你的印象会比闷头写代码高一个档次。6. 我的刷题笔记与避坑清单6.1 三个最能筛出bug的自测用例每次写完螺旋矩阵代码我都会额外跑这三组用例1×1矩阵例如[[5]]预期输出[5]。1×5矩阵例如[[1,2,3,4,5]]预期输出[1,2,3,4,5]。5×1矩阵例如[[1],[2],[3],[4],[5]]预期输出[1,2,3,4,5]。这三组用例能直接暴露四指针版本中最常见的重复读取、越界和漏读问题。很多人在3×3上测试全过却在这三组上现出原形。6.2 现场手写时最容易踩的五个坑我整理一下自己在刷题和模拟面试中反复见到的错误类型按频率排序忘记空矩阵和单行单列的防御导致运行时数组越界。下边和左边的读取没有加if判断在外圈收窄后重复读元素。上边和右边的起始位置写错比如把上边写成从左到右不含右端点导致每个拐角都漏元素。方向控制法中忘记判断visited导致走到死路后原地回头甚至死循环。m和n混用在循环里把行号写成列号范围调试半天才找到。这五个坑我全踩过尤其是第2个印象极其深刻。第一次写四指针版本时我用3×3矩阵测试通过换1×5矩阵直接输出重复元素当时卡了十几分钟才意识到下边判断缺失。6.3 从螺旋矩阵沉淀出的通用能力螺旋矩阵的价值不局限在这一道题。边界收缩的思想可以迁移到矩阵转圈变体、蛇形遍历、棋盘方向模拟等场景方向数组的思想可以迁移到迷宫寻路、岛屿遍历、机器人行走等大规模模拟题目中。我建议大家刷完54、59、885后花半小时把三题的代码并排放在一起对比你会发现三个题共享同一个核心方向与边界的动态维护。理解到这个层级以后再遇到任何“走方格”的题目你都会比别人多一条清晰的思考路径。最后再分享一个个人习惯每次AC后我会把代码中所有while和for的边界条件单独圈出来重新推导一遍单行、单列和空输入的情况。这个习惯帮我省下了无数在面试中临时debug的时间。螺旋矩阵这类题想通一次边界就再也不会忘。
返回列表