回型矩阵螺旋遍历:二维数组边界控制与算法面试实战解析
回型矩阵这个板子题我几乎在每一次帮人突击算法面试的时候都会拿出来当第一道二维数组入门题。它看起来就是个“转圈遍历”但真正能一次写对的人少之又少尤其是当你把矩阵从方阵换成矩形、从奇数行换成偶数行、甚至从顺时针换成逆时针的时候那一堆边界条件直接能把人绕晕。这篇文章我就把自己的实操思路完整拆一遍包括两种主流写法、调试过程中踩过的坑以及从这道板子题延伸出去的一串变形题希望能帮你把它彻底吃透而不是停留在“背代码”的层面。1. 问题还原与思路拆解1.1 题面到底在问什么先还原一下这道题最经典的描述给定一个 m 行 n 列的矩阵按照顺时针螺旋顺序返回矩阵中的所有元素。举个例子输入1 2 3 4 5 6 7 8 9输出就是[1, 2, 3, 6, 9, 8, 7, 4, 5]。这个过程很像“剥洋葱”从最外层一圈开始按上边、右边、下边、左边的顺序把元素取走然后往里收缩一层继续重复同样的动作直到所有元素都被取完。为什么它是“板子题”因为它把所有二维数组题目的基本功都揉在了一起索引访问、循环边界控制、状态切换逻辑、以及对特殊情况空矩阵、单行、单列的防御处理。很多看起来更花哨的题目比如螺旋填充、旋转图像、蛇形遍历本质上都能追溯到这道题上。很多人第一眼觉得这题“很简单”无非就是四个方向循环嘛。但实际上手之后才会发现真正决定代码对不对的不是四个循环本身而是循环之间的边界衔接以及何时该停止。这个特点让它成了一个特别好的练手对象代码量不大但非常考验思维的严谨性。1.2 两条主流解法的取舍针对回型矩阵业界其实已经形成了两套成熟套路我都推荐你掌握因为它们在面试中各有用途。第一套叫“分层模拟法”也叫“边界收缩法”。核心思路是定义四个边界变量top、bottom、left、right分别表示当前还没有被遍历的矩形的上、下、左、右边界。每遍历完一整圈就把四个边界同时向内收缩一格直到边界交错或者重合。这套思路的优点是直观、代码简短、不需要额外空间而且非常适合给面试官现场讲清楚过程。第二套叫“方向状态机法”。它不显式区分“圈”而是模拟一个“小机器人”从左上角出发按右、下、左、上的固定方向顺序走。每到下一个格子之前先判断一下是否越界或者是否已经访问过如果是就右转九十度继续走。这套思路的优点是通用性强遇到不规则图形或者自定义路径时也能用缺点是通常需要额外开一个visited二维数组来记录访问状态空间上不如方法一。这两套方法从复杂度上看其实一模一样时间都是 O(mn)因为每个元素恰好访问一次空间上方法一是 O(1)不计输出数组方法二是 O(mn)因为要开 visited 数组。但实际面试中我更建议优先把“分层模拟法”写顺因为它的代码更少、边界逻辑更直白。方向状态机则可以当作一种“备用方案”储备后面我们在变体题里还会用到它。2. 核心实操代码实现与参数细解2.1 分层模拟法手写拆解Python先用 Python 把这套“边界收缩法”完整写出来这是我最常用、也最推荐新手优先掌握的版本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 col in range(left, right 1): res.append(matrix[top][col]) # 右边从上到下top1 避免重复右上角 for row in range(top 1, bottom 1): res.append(matrix[row][right]) # 只有当前不是单行、单列时才需要处理下边和左边 if top bottom and left right: # 下边从右到左right-1 避免重复右下角 for col in range(right - 1, left - 1, -1): res.append(matrix[bottom][col]) # 左边从下到上bottom-1 避免重复左下角 for row in range(bottom - 1, top, -1): res.append(matrix[row][left]) top 1 bottom - 1 left 1 right - 1 return res这段代码里最关键的是三个地方。第一个是while top bottom and left right这个循环条件。很多人容易写成觉得边界相等之后再进入循环会重复遍历。但实际上如果矩阵是奇数行或者奇数列最内圈会只剩下单独的一行或者单独的一列这时候top bottom或者left right如果不允许相等就会把这一行/列漏掉。所以这里必须用确保最后那条“单边”也能被正确处理。第二个是为什么上边循环直接range(left, right 1)而下边循环必须写range(right - 1, left - 1, -1)。这其实是为了避免四个角被重复遍历第一个循环处理上边时已经包含了左上角和右上角第二个循环处理右边时从top 1开始就是为了跳过右上角同理第三个循环从right - 1开始是为了跳过右下角。四个角各被处理一次不多不少。第三个是那个容易让人忽略的if top bottom and left right判断。如果当前这一圈只剩一行比如 1×5 矩阵那么上边循环就已经把整行走完了再走“下边”就会重复如果只剩一列比如 5×1 矩阵右边循环就已经把整列走完了再走“左边”同样重复。加上这个判断就能把单行、单列的退化情况安全挡在外面。这一步是绝大多数人第一次写这道题翻车的根本原因。2.2 方向状态机写法与适用场景再给你看看第二套写法“方向状态机法”。它模拟的是“走路”的过程代码看起来更酷而且思路更容易扩展到其他题目def spiralOrder(matrix): if not matrix or not matrix[0]: return [] rows, cols len(matrix), len(matrix[0]) total rows * cols visited [[False] * cols for _ in range(rows)] res [] # 方向顺序右、下、左、上 dirs [(0, 1), (1, 0), (0, -1), (-1, 0)] r c 0 di 0 for _ in range(total): res.append(matrix[r][c]) visited[r][c] True nr, nc r dirs[di][0], c dirs[di][1] # 越界或者已经访问过就右转九十度 if nr 0 or nr rows or nc 0 or nc cols or visited[nr][nc]: di (di 1) % 4 nr, nc r dirs[di][0], c dirs[di][1] r, c nr, nc return res这套写法的核心在于“方向数组”的顺序。(0, 1)表示向右(1, 0)表示向下(0, -1)表示向左(-1, 0)表示向上正好对应顺时针。每次前进之前先预判下一格的位置如果发现越界或者已经走过了就切换方向。因为每走一步肯定会被放进res所以用一个total变量控制总步数就够了不需要像方法一那样维护四个边界。这套方法的缺点就是需要额外的visited数组不过它在处理“任意形状的螺旋路径”时非常方便。比如以后遇到那种不是标准矩形、但是要求按螺旋方向遍历的题目方法一的边界收缩就不太好用了而方法二只要把“越界/访问过”的判断改成“是否命中障碍物”思路完全可以平移过去。从我的实测经验来看如果你时间有限只练一种那我建议练方法一如果你想把基础打得更扎实两种都要能默写出来。面试时先写方法一如果面试官追问“能不能不用额外空间”你说“可以我再用边界收缩法做一个”会是非常加分的表现。3. 避坑实录边界条件与测试技巧3.1 最容易翻车的几个边界回型矩阵的代码一旦出问题99% 都出在边界和退化场景上。我把实战中高频翻车点整理成了下面这张表你可以直接拿来当自查清单输入场景期望输出容易犯的错误空矩阵[][]没判断matrix[0]直接抛异常1×1 矩阵[[1]][1]循环边界用top bottom导致中心元素丢失1×n 矩阵[[1,2,3]][1,2,3]没加单行判断第三/第四个循环重复输出m×1 矩阵[[1],[2],[3]][1,2,3]没加单列判断下边循环重复输出2×2 矩阵四元素正常顺序四个角重复被访问3×3 矩阵中心元素最后输出while条件用了导致中心丢失2×3 矩阵六元素正常顺序底部循环条件写错导致越界访问这些坑里最隐蔽的是 2×3 这种“宽比高多”的情况。这时候第一圈走完并不是一个完整的正方形下边循环需要严格从right - 1倒着走到left如果你把终止条件写成了left 1那么左下角就会被漏掉。反过来3×2 这种“高比宽多”的情况左边循环也会出现类似问题。还有一个最基础的防御型判断if not matrix or not matrix[0]。很多人只判断not matrix但输入如果是[[]]也就是一行空列表程序依然会因为访问matrix[0]越界或者无法取长度而出问题。这道题在 LeetCode 风格的环境中可能不会给你这种输入但在笔试题、机试题里一切皆有可能所以这个防御判断我建议每次都写上。3.2 调试与自测三板斧我也不知道是多少次踩坑之后才总结出下面这几个调试技巧。每次写回型矩阵我都会下意识地用上它们。第一个技巧是把四个边界变量打印出来。在方法一的每个 while 循环开头加一行临时的print(top, bottom, left, right)然后运行一个小矩阵用例观察边界收缩过程是否和预期一致。你会发现很多错误根本不需要断点就能看出来比如top越过bottom时还多走了一圈、left和right收缩方向反了这些在打印结果里一清二楚。第二个技巧是构造一套固定测试集。我自己常用的测试集是1×1、1×3、3×1、2×2、2×3、3×2、3×3、4×4。不要觉得这些用例多实际上每一类都对应一类边界问题。比如 1×3 和 3×1 分别验证单行、单列2×3 和 3×2 验证不对称矩形3×3 和 4×4 分别验证奇数和偶数大小的矩阵。每次改完代码把整套跑一遍全绿了才算稳。第三个技巧是用遍历次数做最终确认。在代码末尾加一句断言assert len(res) len(matrix) * len(matrix[0])。如果结果长度对不上说明一定有重复或遗漏。这个方法尤其适合方向状态机法因为它的循环是total次很容易忘记某些元素没被访问到。我自己就踩过一次印象特别深的坑某次在 while 条件里偷懒写了top bottom and left right结果用一个 3×3 的用例一测输出长度只有 8中心那个元素丢了。当时排查了半小时才发现是边界条件的问题。从那以后我写任何二维数组遍历题第一件事就是检查循环条件是还是以及单行单列的分支是否写全。4. 变体延伸从板子题到活用的路线4.1 常见变体一网打尽回型矩阵是一个母题下面能引出来一串变体面试里换汤不换药地考。我把它们一个个过一遍你就知道为什么我总说板子题一定要吃透而不是背会。第一个变体逆时针螺旋输出。这个最简单把方向顺序反过来就行。如果是方法一把四个循环的顺序从“上、右、下、左”改成“左、下、右、上”再对应调整边界收缩即可如果是方法二把方向数组改成[(0, -1), (1, 0), (0, 1), (-1, 0)]即左、下、右、上。我实测这个变形五分钟内就能改完。第二个变体给定 n生成一个 1 到 n² 的回型填充矩阵。这是回型矩阵的“反向操作”把读矩阵改成填矩阵。逻辑完全一样只是把res.append(matrix[top][col])换成matrix[top][col] num外层维护一个从 1 到 n² 的计数器。这类题目考的就是你能不能把同一套框架反向使用。第三个变体从中心向外螺旋。思路是倒着走起点在正方形矩阵正中心方向顺序换成左、上、右、下之类的。因为走得越来越远不能用简单的边界收缩法方向状态机法配合“步长递增”的模式会更顺手。这类题目出现频率不高但一旦出现它是很好的区分度题。第四个变体蛇形矩阵也叫之字形遍历。它不是螺旋而是“第一行从左到右、第二行从右到左、第三行又从左到右”的交替遍历。虽然看起来和回型矩阵不太一样但它同样是在考“二维数组坐标变换”而且解决思路里同样会出现“长度控制”和“方向翻转”两个关键点。第五个变体旋转图像。把 n×n 矩阵顺时针旋转 90 度。经典做法是先沿对角线翻转再左右翻转但如果你用回型矩阵的“由外到内分圈”框架来做也能得到一套可读性不错的方法每圈四条边做旋转交换。这一题可以作为回型矩阵的进阶巩固。用力方向对了你会发现这些变体题完全不需要死记硬背因为它们考察的本质其实都是同一件事你对二维数组索引的敏感度和对循环终止条件的控制力。4.2 从 Python 到其他语言移植面试里如果要求用 C 或者 Java 写这道题有几个坑和 Python 不太一样这里单独拎出来说一下。用 C 写的时候最常见的问题来自size_t类型。matrix.size()返回的是size_t它是无符号整数。如果你写一个for (int i size - 1; i 0; i--)这样的循环当size为 0 时size - 1会变成一个极大的正数循环根本不会正常结束。所以我的建议是在 C 实现里边界变量一律显式声明为int把matrix.size()的结果强转成int避免无符号数带来的隐式陷阱。另外 C 中vectorvectorint的空判断和 Python 也有区别。你需要写if (matrix.empty() || matrix[0].empty())才能把所有空输入的情况都覆盖到。如果只判断matrix.empty()遇到[[ ]]这样的输入下一行访问matrix[0][0]时依然可能触发未定义行为。用 Java 写的时候主要关注数组下标和循环变量类型。Java 的int比 C 的size_t省心一些但要注意matrix[0].length在空数组上调用会抛ArrayIndexOutOfBoundsException或NullPointerException所以防御判断同样不能少。另外Java 的二维数组有可能是“行长度不一致”的虽然回型矩阵默认是标准矩形但如果你在练习时自己构造了不规则数据遍历时最好先用matrix[i].length而不是matrix[0].length。还有一个各语言通用的经验不要在一开始就把代码优化得很“聪明”。比如有人喜欢用位运算、用三元表达式压缩行数但这样导致的结果往往是边界逻辑被藏起来出了问题很难排查。我自己的习惯是第一遍写最直白、最啰嗦、每行都注释清楚的版本确保逻辑不出错等测试全过了再考虑能不能精简成更优雅的写法。这道板子题尤其如此很多看起来写得漂亮的短代码其实都在边界处理上留下了隐患。从实际面试表现来看面试官往往更看重你“能不能用最直接的方式解决问题”而不是“代码写得有多花哨”。所以宁可多写几行也要保证一次通过。最后再分享一个我自己的习惯我会在项目代码库里专门建一个matrix_template文件夹里面放着回型矩阵的三种语言版本以及对应的测试用例。每次要刷二维数组题之前先花五分钟把这题重新默写一遍再开始做变体。这样一个简单的动作能帮你把二维数组的边界控制能力保持在很稳定的水准上。如果你现在还处在“看着会、写就错”的阶段别急那是太正常不过的了。先照着文章里的方法一写十遍每写一遍都把四个边界值在草稿纸上画出来相信我很快你就会发现螺旋遍历这件事变得像呼吸一样自然。