逆时针打印矩阵全解析:边界收缩与方向模拟及面试避坑 逆时针打印矩阵这道题我第一次看到的时候觉得它就是顺时针打印矩阵把方向反过来简单。可真到动手写各种问题接踵而至单行输出一半就停单列元素重复打3 行 4 列的矩阵走完外圈之后内圈方向直接乱掉。这篇文章把我反复踩过的坑、最终沉淀下来的稳定写法和面试时的答题节奏都整理出来。内容上会先讲清楚题目定义和考察点再给两套解法一套是省空间的四指针边界收缩一套是逻辑更直觉的方向模拟并用手算完整推演一个矩阵。最后是一份边界问题速查表和面试实战建议适合正在准备算法面试的开发者也适合纯粹想把二维数组遍历逻辑吃透的人。1. 先把题目的边界摸清楚1.1 题目到底在说什么题目本身一句话给定一个 m 行 n 列的二维矩阵从矩阵的左上角出发按逆时针方向绕圈由外向内把每个元素依次打印出来。用例子看最直观。3 行 4 列矩阵1 2 3 4 5 6 7 8 9 10 11 12输出顺序是1, 5, 9, 10, 11, 12, 8, 4, 3, 2, 6, 7。走法就是从 1 开始一路向下到 9沿底部一行向右到 12再沿右侧一列向上到 4接着沿顶部一行向左到 2注意 1 已经被访问过不能重复。然后进入第二圈从 6 走到 7。整个路径像一条逆时针旋转的螺旋线每走完一条边就往内收缩一个单位。动手前有两个前置条件要确认矩阵的行数和列数不一定相等也可能是空矩阵矩阵元素具体是什么类型其实不影响核心解题我们只关心位置和访问顺序。这种输出顺序题在面试中出现频率不低常见形态有顺时针打印、逆时针打印、之字形遍历题目名字不同背后的考察内核是一致的。1.2 面试官在这道题里想看到什么这道题不是难题但对细节要求很细。我自己面人的时候如果出这道题主要想看到三点。第一是边界意识。m 和 n 是否相等、是否为空、是否只有一行或一列这些情况会不会让代码越界、死循环或者重复输出。很多候选人正例跑通了一追问如果是单列呢就答不上来代码当场露馅。第二是建模能力。能不能把绕圈打印这个动作提炼成清晰的循环结构而不是用一堆 if 到处打补丁。指针更新的顺序是否一致、状态是否明确、缩进是否正确都能反映一个人写工程代码的整洁程度。第三是复杂度意识。这道题最理想的复杂度是 O(mn)因为每个元素恰好输出一次。空间上如果能做到 O(1) 额外空间就更好了。能在写完代码后主动说出这两个复杂度是明显的加分项。这些考察点其实就是后面几节要逐一展开的内容。先把题目看透再动手写比上来就默写模板要稳得多。2. 两条解题路线边界收缩和方向模拟2.1 路线一四指针边界收缩最省空间先说最经典的边界收缩法。思路可以类比成剥洋葱想象矩阵是好几层矩形套在一起每一轮循环剥掉最外面一圈。维护四个指针top 指向最上面未访问的行bottom 指向最下面未访问的行left 指向最左边未访问的列right 指向最右边未访问的列。初始时 top0bottomm-1left0rightn-1。每一轮循环处理四条边逆时针的顺序是左列从 top 到 bottom逐个输出 matrix[i][left]输出完 left 加 1底行从 left 到 right逐个输出 matrix[bottom][j]输出完 bottom 减 1右列从 bottom 到 top逐个输出 matrix[i][right]输出完 right 减 1顶行从 right 到 left逐个输出 matrix[top][j]输出完 top 加 1。每处理一条边就立刻收缩对应的指针保证已访问的区域不会再被访问。循环继续的条件是 top bottom 且 left right也就是矩阵还没有被剥空。为什么用四指针而不是维护一个 visited 数组因为四指针是在几何形状上做减法天然不占额外空间。对于这种矩阵遍历题如果面试官要求空间复杂度 O(1)基本就是往这个方向写。2.2 路线二方向向量模拟代码更直觉另一条路线是模拟小人走路。给小人一个当前方向每走一步如果下一步越界或者已经访问过就换方向然后继续走。在矩阵坐标系里行方向向下是正方向列方向向右是正方向。逆时针螺旋需要的四个方向依次是向下 (1, 0)、向右 (0, 1)、向上 (-1, 0)、向左 (0, -1)。从 (0,0) 出发依次循环切换这四个方向就能走出逆时针螺旋。关键点是方向数组的顺序必须严格是下、右、上、左如果顺序乱了走出来的就不是逆时针螺旋。方向向量法的优点是逻辑直观不需要同时维护四个指针也不容易搞混边界判断。缺点是必须额外开一个同尺寸的 visited 二维数组来标记已经访问过的格子空间是 O(mn)。2.3 两条路线怎么选我个人的建议是如果空间要求不敏感先用方向模拟法写它更接近人脑的理解过程面试时不容易崩写完再补一句如果要求 O(1) 空间可以改成四指针边界收缩法把 visited 换成四条不断收缩的边界。这样既展示了第一反应的正确性也展示了复杂度意识。反过来如果你对边界收缩法已经滚瓜烂熟直接写边界收缩也完全可以。只是它有一个容易出错的细节我在第四节会专门讲四个方向的循环里后面两个方向的 if 判断不能省省了就会出现重复输出甚至死循环。两种方案的对比如下方案时间复杂度额外空间上手难度出错风险四指针边界收缩O(mn)O(1)中等边界条件容易漏方向向量模拟O(mn)O(mn)低转向逻辑容易绕两个方案的时间复杂度相同因为每个元素都恰好被处理一次。差别只在空间和代码风格。3. 参考实现与手算推演3.1 边界收缩法参考实现直接上代码Python 版本加了注释。我在本地验证过下面这些用例空矩阵、1x1、1xN、Nx1、2x2、3x3、3x4、4x3、4x4输出都正确。def spiral_order_counter_clockwise(matrix): if not matrix or not matrix[0]: return [] m, n len(matrix), len(matrix[0]) top, bottom 0, m - 1 left, right 0, n - 1 result [] while top bottom and left right: # 1. 左列从上到下 for i in range(top, bottom 1): result.append(matrix[i][left]) left 1 # 2. 底行从左到右 if left right: for j in range(left, right 1): result.append(matrix[bottom][j]) bottom - 1 # 3. 右列从下到上 if top bottom and left right: for i in range(bottom, top - 1, -1): result.append(matrix[i][right]) right - 1 # 4. 顶行从右到左 if top bottom and left right: for j in range(right, left - 1, -1): result.append(matrix[top][j]) top 1 return result三个核心细节必须解释清楚第一为什么第 2 步开始要加if left right。因为第 1 步已经把 left 加 1 了如果原始矩阵只有一列此时 left 已经超过 right底行剩余区间不存在必须跳过。不然 bottom 会被错误收缩进而影响后面的全部判断。第二第 3 步和第 4 步为什么是if top bottom and left right而不是只判断其中一个。因为它们各自负责的右列和顶行只有在矩阵还剩下至少两行、至少两列时才是新的、未被访问的边。只剩一行或一列时这条边已经被前两步顺带访问完了再走一遍就会重复输出。第三第 4 步的 range 为什么写到left - 1而不是left。因为 range 的结束位置是开区间不包含结束值本身。要输出 left 位置的元素结束值必须传 left - 1。这个细节是差一错误的高发区。3.2 方向向量法参考实现方向模拟版本的代码。这里先手动把第一个元素放进结果再循环 m*n-1 次每一步先尝试按当前方向走走不了就换方向。def spiral_order_counter_clockwise(matrix): if not matrix or not matrix[0]: return [] m, n len(matrix), len(matrix[0]) visited [[False] * n for _ in range(m)] # 逆时针螺旋的方向序列下、右、上、左 directions [(1, 0), (0, 1), (-1, 0), (0, -1)] row, col 0, 0 di 0 result [matrix[row][col]] visited[row][col] True for _ in range(m * n - 1): nr, nc row directions[di][0], col directions[di][1] if nr 0 or nr m or nc 0 or nc n or visited[nr][nc]: di (di 1) % 4 nr, nc row directions[di][0], col directions[di][1] row, col nr, nc result.append(matrix[row][col]) visited[row][col] True return result这里有一个值得想明白的点为什么转向一次就够了。因为在螺旋路径的每个转折点剩余未访问区域始终是一个矩形环当前方向走不了时新的方向必然能走。唯一可能的例外是最后一个元素已经被访问完但这时循环次数也正好用完了。不过方向向量法的代价是 visited 数组。空间敏感的题目里这个方案不是首选。3.3 用 3 行 4 列矩阵完整推演一遍拿 3x4 的例子手算一次方便对照最终输出是否符合直觉。1 2 3 4 5 6 7 8 9 10 11 12边界收缩法全过程第一轮top0bottom2left0right3左列输出 1、5、9left 变成 1底行输出 10、11、12bottom 变成 1右列输出 8、4right 变成 2顶行输出 3、2top 变成 1。第二轮top1bottom1left1right2左列输出 6left 变成 2底行输出 7bottom 变成 0右列top 已经大于 bottom跳过顶行left 已经大于 right并且循环条件不再满足退出。最终输出是 1、5、9、10、11、12、8、4、3、2、6、7。注意第二轮里6 和 7 是剩余一行中的两个元素左列和底行两个方向各取一部分合起来正好覆盖完整的一行。如果你用方向向量法来走路径完全一致只是不加边界指针。4. 最容易翻车的边界场景复盘4.1 单行矩阵循环提前退出的真相假设输入是 [[1, 2, 3]]期望输出 [1, 2, 3]。边界收缩法会这样走第一步左列因为 top 等于 bottom只输出 matrix[0][0] 1第二步底行left 已经加 1从 index 1 到 2输出 2 和 3bottom 减成 -1。此时循环条件 top0 bottom-1 不成立退出。这个例子在正确版本里没问题但如果你写的第 4 步条件配错了单行矩阵就会出现重复输出。比如第 4 步如果只判断left right而不判断top bottom那么在 top 仍然等于 0、bottom 变成 -1 之后第 4 步会继续从右往左扫一遍顶行把 3 和 2 又打出来一遍。所以处理单行矩阵的要点是理解每一轮循环里水平方向的遍历可能被左列和顶行两个方向分担也可能被底行一个方向全部覆盖。要让代码自动识别该走哪一条水平边而不是两个方向都强行执行。4.2 单列矩阵重复输出的重灾区单列矩阵是另一个经典陷阱。输入是 [[1], [2], [3], [4]]正确输出是 [1, 2, 3, 4]。边界收缩法里第一步左列会从上到下把 4 个元素全部输出left 加 1 后超过 right。第 2 步判断 left right 不成立直接跳过。第 3 步、第 4 步也相继跳过循环退出。这个流程是对的。真正的坑在于内层只剩单列的矩形比如 4 行 3 列矩阵1 2 3 4 5 6 7 8 9 10 11 12第一轮剥掉外圈后内层剩下一列 5、8。如果第 3 步右列的条件漏写了left right它会把这一列当成右侧新列再遍历一遍结果 5 和 8 被打印两次。我当时就在这个问题上栽过输出末尾出现 5、8、5、8第一反应是代码死循环了排查半天发现只是内层单列时左列和右列重合了。解决办法就是在第 2、3、4 步都加上区间判断。这个案例也说明测试用例不能只挑方阵长条矩阵、宽条矩阵和小矩阵都要覆盖。4.3 小矩阵最后一圈的归属问题小矩阵看似简单实际上最容易在最后一步上翻车。2x2 矩阵期望输出是 [1, 3, 4, 2]3x3 矩阵期望输出是 [1, 4, 7, 8, 9, 6, 3, 2, 5]。3x3 这种奇数尺寸方阵的麻烦在于剥完外圈后只剩正中间一个元素 5。负责输出它的既可能是左列方向也可能是底行方向取决于你当前的指针状态。如果 while 循环条件写成了top bottom and left right这种严格小于形式循环会在还有一行、一列剩余时提前退出中心元素直接丢失。另一个隐蔽问题是最后一个元素的归属不清晰时会被两个方向各输出一次。避免这个问题的方法是明确一个原则每个元素只属于它所在的那一圈、那一条边写完代码后一定要用一个奇数尺寸方阵和一个偶数尺寸方阵分别自测。2x2 和 3x3 是我建议所有人在提交代码前必须手跑的两个用例因为它们足够小能最快暴露方向条件的问题。4.4 常见问题速查表我把实际踩过的坑整理成了一张表面试前翻一遍很有用场景典型症状根因修复思路单行矩阵输出后半段重复顶行和底行是同一行被两个方向重复遍历第 3、4 步加上 top bottom 判断单列矩阵元素重复输出左列和右列重合被两个方向重复遍历第 2、3、4 步加上 left right 判断3x3 奇数矩阵中心元素漏掉while 条件写成严格小于提前退出循环条件用 top bottom 和 left right4x3 内层单列末尾出现重复序列内层只剩一列时多个方向访问同一列所有方向遍历前都做区间有效性判断方向向量法 1x1数组越界最后一个元素转向后新方向仍然越界循环步数固定为 m*n-1或加转向保护这张表可以作为面试前的快速备忘。我自己是把它放在手机备忘录里面试前一天翻一遍。5. 面试实战从读题到代码落地的完整节奏5.1 动手前先做三件事很多候选人一看到打印矩阵就开始热情地写代码结果写到一半发现矩阵可能是空的又回头改。更稳的顺序是先澄清需求再说思路最后动手写。澄清需求要问清楚三件事输入一定是非空矩阵吗空数组和空行数组怎么处理输出格式有要求吗是拼成数组返回还是直接打印矩阵元素类型有限制吗会不会有负数或字符串这些问题看起来简单但能反映一个人的工程沟通习惯。问完之后用一两句话说出整体思路比如我准备用四个边界指针模拟剥洋葱每处理一条边就收缩对应的边界直到边界交叉。这句话能让面试官知道你有清晰的方案也是给自己搭框架。5.2 测试用例怎么选才有说服力代码写完不要只跑题目给的例子。主动跑下面这一组基本上面边界情况全覆盖空矩阵 [] 和 [[]]返回空数组1x1 [[5]]返回 [5]1x3 [[1,2,3]]返回 [1,2,3]3x1 [[1],[2],[3]]返回 [1,2,3]2x2 和 3x3最容易暴露内层元素归属错误3x4 和 4x3分别对应比较宽和比较高的矩形能覆盖内层单行与单列。跑这些用例的时候可以边说边验证比如主动说我再用一个 4x3 的用例验证内层只剩单列的情况。这一句话就能体现边界意识。面试官通常不会因为你多测几个用例而觉得啰嗦反而会觉得你训练有素。5.3 面试官追问时可以聊的深度话题如果面试官往下追问通常会集中在两个方向。一个是复杂度。要能脱口而出时间 O(mn)因为每个元素恰好输出一次空间如果采用边界收缩法能达到 O(1)采用方向模拟法是 O(mn)。很多候选人卡在空间为什么不是 O(mn)上其实是因为没理解四指针本身就是对已访问区域的压缩表示边界一收缩那些格子就天然不会再被访问。另一个是无额外空间的变种。比如如果不允许修改输入数组也不允许开新数组怎么做答案就是边界收缩法。如果题目再放宽到可以修改输入数组还可以把访问过的元素标记为特殊值来替代 visited但这会破坏原数据一般需要先和面试官确认。6. 从这道题延伸出去的矩阵遍历方法论6.1 同一套思想能解决哪些兄弟题逆时针打印矩阵并不是孤立题型它背后是一整套矩形区域按方向批量遍历的方法。常见的兄弟题包括顺时针打印矩阵把四个方向的轮换顺序改成右、下、左、上其余逻辑完全一样之字形蛇形遍历矩阵每一行方向交替变化核心是行方向加边界翻转螺旋填充矩阵从外到内按固定方向填入递增数字本质是把读变成写矩阵旋转 90 度先按对角线翻转再按行翻转虽然实现不同但对坐标映射的要求一致。如果把逆时针打印的代码结构吃透遇到这些题目时只需要改方向序列或指针收缩顺序骨架不用大改。6.2 判断一道题能不能用边界收缩套路的三个特征用多了之后我总结出三个特征满足它们基本就可以尝试用边界收缩法遍历路径是分层的每一层绕着当前矩形边界走一圈然后向内推进层与层之间的推进方向是固定的不会出现回头路判断是否结束可以用两条边界是否交叉来描述。反过来如果遍历顺序里有跳跃比如跳到对角线另一端或者路径是蛇形但不是环形的边界收缩就不一定合适这时候往往更适合用方向模拟或者直接做坐标推导。面试时先判断题型再选择套路比背模板稳得多。7. 最后再分享一点我的实战体会这道题我前前后后改过至少三个版本最终保留的是上面这份带判断条件的边界收缩版。每次重写我都会提醒自己一句话不要把看起来能跑当成真的对用非方阵和单行单列的用例逼一逼代码很多隐藏问题就会现出原形。如果面试时时间紧张方向向量法更容易一次写对因为它不需要在脑子里模拟四个指针的收缩关系但写完一定要补一句如果要求 O(1) 空间我会改成四指针版。实际工作中我很少碰到这种打印矩阵的需求但这类题目锻炼的把二维访问顺序抽象成方向加边界的能力在图像处理、棋盘类算法里真能派上用场。这大概就是一道面试题能带给人的全部价值不是背答案而是借它把二维数组的遍历逻辑彻底理清楚。