资讯详情

逆时针打印矩阵:四指针边界收缩法详解与算法面试避坑指南

📅 2026/10/20 12:37:50 | 华诺云谱 👁 阅读
逆时针打印矩阵:四指针边界收缩法详解与算法面试避坑指南
“逆时针打印矩阵”这个标题很多人第一反应是不就是从外往里绕圈输出吗但真上手写代码后会发现绕圈不难难的是绕到一半怎么停下来。它以“面试题29”的编号流传于各类算法题库是更常见的“顺时针打印矩阵”的镜像变体考察的核心几乎完全一致边界条件的处理、方向顺序的设计、以及代码结构的清晰度。这篇文章我会带你把逆时针螺旋的边界收缩法彻底吃透内容包括最优解实现、4x4实例走读、面试现场常见翻车点以及同类题目的扩展思路。适合正在准备算法面试的开发者也适合想补一下矩阵遍历基本功的朋友。1. 题目回顾与整体设计思路1.1 先看清题目到底要什么先还原一下题目给定一个 m 行 n 列的二维矩阵 matrix从左上角 (0, 0) 出发按照逆时针螺旋顺序从外到内依次输出所有元素。拿下面这个 3x3 矩阵举例1 2 3 4 5 6 7 8 9按逆时针方向走输出序列应该是1 4 7 8 9 6 3 2 5。把路线画出来非常直观先沿左边向下走到左下角再沿底边向右走到右下角再沿右边向上走到右上角再沿顶边向左走然后进入第二圈重复同样的方向。这类题在算法面试里出镜率很高原因在于它“代码不长但坑位密集”。题面一句话就能说清但真正写代码时方向顺序、边界收缩、终止条件三个环节只要有一个想岔输出就会缺元素或者重复元素。面试官很喜欢用这道题来考察候选人对循环不变量和边界条件的敏感度。另外要提一句这道题和“顺时针打印矩阵”本质上是一个镜像问题。面试题29的原版通常是顺时针方向逆时针算是高频变体。但不管是正还是逆核心解法模型完全一样只是四条边的打印顺序不同。掌握了逆时针回手改一下方向序就是顺时针。1.2 两种主流解法的取舍面对这道题常见有两种解法。第一种是“方向数组 访问标记”。维护一个方向向量数组比如逆时针方向就是先向下、再向右、再向上、再向左。再开一个同样大小的 visited 二维数组记录哪些格子已经访问过。每走一步先尝试保持当前方向如果下一个位置越界或者已经被访问就切换到下一个方向。这种解法属于“直观模拟”思路非常机械几乎不需要动脑子。参考代码如下def spiral_ccw_by_visited(matrix): if not matrix or not matrix[0]: return [] m, n len(matrix), len(matrix[0]) visited [[False] * n for _ in range(m)] # 逆时针方向下、右、上、左 dirs [(1, 0), (0, 1), (-1, 0), (0, -1)] x y 0 d 0 res [] for _ in range(m * n): res.append(matrix[x][y]) visited[x][y] True nx, ny x dirs[d][0], y dirs[d][1] if nx 0 or nx m or ny 0 or ny n or visited[nx][ny]: d (d 1) % 4 nx, ny x dirs[d][0], y dirs[d][1] x, y nx, ny return res这种方案的好处是逻辑统一不容易漏元素甚至把方向数组改一改就能应对各种变体比如从任意起点开始。坏处是额外需要一个 O(mn) 的 visited 矩阵而且如果面试官追问“能不能不用额外空间”这套代码就有点说不过去。第二种是“四指针收缩法”也是这篇博文要重点讲的方案。用四个变量 top、bottom、left、right 圈住当前还没打印的那部分矩阵范围每一轮从外到内剥一条边剥完这条边就把对应指针往中间挪一格。整个过程不需要额外数组空间上更有优势代码结构也更加对称。面试场合我更推荐四指针法。原因是这道题本身就在考察你对边界条件的掌控力四指针法天然把“边界”这个考点放在最显眼的位置。而且它的时间复杂度是 O(mn)空间复杂度不计输出结果的话是 O(1)这个复杂度分析在面试里非常好讲。1.3 核心心法把绕圈理解成“剥洋葱”四指针法的本质其实就是剥洋葱。把一个矩阵看成一层套一层的“洋葱皮”每一轮循环剥下当前最外圈剥完之后剩下的还是一个更小的矩阵然后继续剥。直到某一次剥完发现矩阵缩成了一个空范围。每一步都重复同样的规律打印当前范围最左边那一列从上往下打印当前范围最底边那一行从左往右打印当前范围最右边那一列从下往上打印当前范围最顶边那一行从右往左。打印完一条边立刻把对应的指针向中间收缩。这四个动作构成一个稳定的“循环节”只要循环节不错位代码的正确性就有保证。我在练习这道题时有一个体会不要试图在脑中模拟太抽象的“螺旋”而是把每一轮的动作写成一个固定的四步模板。第一次写可能觉得啰嗦多写几次就会觉得这个模板非常可靠几乎不会出错。2. 核心实现细节与边界条件2.1 四条边打印顺序下、右、上、左先从方向顺序说起。逆时针从左上角出发最直观的路线是第一步向下沿着矩阵最左边一列从 top 走到 bottom第二步向右沿着矩阵最底边一行从左走到右第三步向上沿着矩阵最右边一列从 bottom 往上走到 top第四步向左沿着矩阵最顶边一行从右往左走。写成符号就是下 → 右 → 上 → 左。如果你写的是顺时针版方向顺序就变成右 → 下 → 左 → 上。两者差别只在方向序列的排列但面试时经常有人写着写着就串台了。这里还有一个容易踩的细节走“向左边”的时候顶边行的最左端元素其实已经被第一步“向下打印左列”打印过了走“向上边”的时候右列的最顶端元素会被包含进来而最底端的元素已经在第二步“向右打印底行”里打过了。这些“重叠位”之所以不会重复打印就是边界收缩在起作用。每打印完一条边对应的 left、bottom、right、top 会向内部收缩藏在边角位置的元素自然不会被第二遍碰到。2.2 为什么每一步之后都要判断边界这一节应该是整道题最关键的地方也是绝大多数人翻车的位置。大循环的入口条件一般是while top bottom and left right但只靠这一个条件远远不够。在执行完某一条边并更新指针之后四个指针之间有可能已经交叉此时不应该再继续执行后面的边否则就会发生重复打印甚至数组越界。拿一个极端的例子来说矩阵是一行三列[[1, 2, 3]]。初始 top 0bottom 0left 0right 2。进入循环后向下打印左边列只有 (0,0) 一个元素输出 1left 变成 1此时 left 还没超过 right继续向右打印底行输出 2 和 3bottom 变成 -1关键来了此时 top0bottom-1说明矩阵范围已经空了。如果代码没有在这里立刻中断下一步“向上打印最右列”就会拿着 bottom-1 的起点去构造循环要么一上来就用越界下标访问 matrix[-1][2]要么进入一个空循环之后又把右边界缩掉最后再跑去重复打印顶行。所以边界判断的正确姿势是每打印完一条边、收缩完指针之后立刻检查是否还有下一条边可以打印。具体来说可以套用下面这套规律打印完左边后left 1如果left right说明左右边界已经交叉直接 break打印完底边后bottom - 1如果top bottom说明上下边界已经交叉直接 break打印完右边后right - 1如果left right直接 break打印完顶边后top 1然后回到 while 条件判断。这套检查从我的实战经验看是必须的少一个都会出问题。尤其是面试官如果刻意给你一个单行矩阵或者单列矩阵那几乎就是在专门等你在这里出错。2.3 边界收缩为什么天然防重复边界收缩是防重复的关键原因很简单每打印完一条边这条边上的全部元素就相当于“永久离开”下一轮的打印范围。举个例子当第一步把左边一列从上到下打完立刻执行left 1下一次向右打印底行时底行的最左端元素就会因为j从新的 left 开始而被跳过。以此类推。所以只要保证每条边打印完之后收缩对应指针元素即使物理上被打印过一次也不会再出现在后续任何一条边的候选范围里。这也是为什么四指针法不需要额外的 visited 数组因为边界本身起到了“标记已访问”的作用。3. 完整代码实现与实例走读3.1 迭代版参考代码Python把上面的分析落成代码就是下面这一版。我习惯把每条边的打印、指针收缩、边界检查三个操作紧挨着写让整个循环节非常清晰。def spiral_matrix_ccw(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 ans [] while top bottom and left right: # 1. 向下打印最左列 for i in range(top, bottom 1): ans.append(matrix[i][left]) left 1 if left right: break # 2. 向右打印最底行 for j in range(left, right 1): ans.append(matrix[bottom][j]) bottom - 1 if top bottom: break # 3. 向上打印最右列 for i in range(bottom, top - 1, -1): ans.append(matrix[i][right]) right - 1 if left right: break # 4. 向左打印最顶行 for j in range(right, left - 1, -1): ans.append(matrix[top][j]) top 1 return ans每一段代码的动作顺序都是统一的先用 for 循环打印对应边然后立刻收缩指针然后判断是否 break。三行连成一个不可分割的整体后代码的正确性会明显提高。时间复杂度是 O(mn)因为每个元素恰好被访问一次。额外空间方面除了存放结果的 ans 数组只用到了四个整型变量所以是 O(1) 的工作空间。面试时把这两句复杂度分析讲出来会显得对代码有充分把握。3.2 用 4x4 矩阵完整走一遍纸上得来终觉浅这里我用一个 4x4 矩阵完整走一遍代码的执行过程1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16初始状态top0bottom3left0right3。第一轮循环向下打印 left0 这一列行从 0 到 3输出 1、5、9、13。然后 left 变成 1。向右打印 bottom3 这一行列从 1 到 3输出 14、15、16。然后 bottom 变成 2。向上打印 right3 这一列行从 2 到 0输出 12、8、4。然后 right 变成 2。向左打印 top0 这一行列从 2 到 1输出 3、2。然后 top 变成 1。第一轮结束后ans 为[1, 5, 9, 13, 14, 15, 16, 12, 8, 4, 3, 2]当前内部范围是 top1、bottom2、left1、right2。第二轮循环向下打印 left1 这一列行从 1 到 2输出 6、10。left 变成 2。向右打印 bottom2 这一行列从 2 到 2输出 11。bottom 变成 1。向上打印 right2 这一列行从 1 到 1输出 7。right 变成 1。检查 left2 right1break。最终结果[1, 5, 9, 13, 14, 15, 16, 12, 8, 4, 3, 2, 6, 10, 11, 7]。总共 16 个元素一个不多一个不少。这里最值得回味的是第二轮第 4 步“向左打印顶行”被 break 掉了。因为走到这一步时内部只剩[6, 7]和[10, 11]这 2x2 的四个元素而 6、10、11、7 已经分别被前面三步打印过如果继续向左走必然碰到已经输出过的 6。这就是边界检查在非正方向情况下起到保护作用的典型案例。3.3 递归版思路与其他语言注意点除了迭代法还有一种递归思路也值得一提。它的核心是先打印当前外圈然后递归处理缩掉一圈的内部子矩阵。def spiral_ccw_recursive(matrix): res [] def solve(top, bottom, left, right): if top bottom or left right: return for i in range(top, bottom 1): res.append(matrix[i][left]) for j in range(left 1, right 1): res.append(matrix[bottom][j]) for i in range(bottom - 1, top - 1, -1): res.append(matrix[i][right]) for j in range(right - 1, left, -1): res.append(matrix[top][j]) solve(top 1, bottom - 1, left 1, right - 1) if matrix and matrix[0]: solve(0, len(matrix) - 1, 0, len(matrix[0]) - 1) return res递归版的思路和“剥洋葱”一脉相承但注意它的每条边都通过起点偏移来避开已经打印过的角点。这个版本在单行、单列的情况下依然安全比如单行时中间的 for 循环会自动跳过。缺点也很明显递归深度是min(m, n) // 2量级矩阵特别长条时会有额外栈空间开销所以面试时把它作为“另一种思路”来聊没问题写代码时我建议还是优先迭代版。如果用 C 写还要留意几个细节一是入口要同时判断matrix.empty()和matrix[0].empty()二是矩阵的 m、n 建议直接用 int不要用 size_t否则边界减到 -1 时会原地爆炸。Java 版本需要注意 List 的使用逻辑和 Python 完全一致。4. 常见问题与排查技巧4.1 为什么输出里会出现重复元素重复打印是最常见的报错现象多数时候问题出在边界检查不完整。典型场景就是我前面说的单行矩阵第一步打印左列之后 left 加 1第二步打印底行时 bottom 减成 -1如果代码没有在第二步之后做top bottom检查第三步“向上打印右列”可能返回空但第四步“向左打印顶行”会因为 top 还没变又把已经打印过的第二、第三个元素重新打印一遍。重复元素还有一种隐蔽的成因四条边的打印范围没有利用更新后的指针而是仍然用了旧的 left、right、top、bottom。比如向右打印底行时for j in range(left, right 1)里的 left 必须是用新值还是旧值答案是第一步已经打印过左下角所以这里的 left 应该用更新后的新值从 left 开始才能跳过左下角。如果代码习惯性写成range(0, right 1)那矩阵左下角和底边的第一个元素就会被打印两次。排查重复问题时建议先用一个 1x3 或 3x1 的极端矩阵跑一遍。这种矩阵最能暴露边界检查缺失的问题。4.2 为什么某些元素被漏掉了漏打印的常见原因有三个。第一个是方向顺序写反了。比如逆时针的序列是下、右、上、左有人习惯性写成顺时针的右、下、左、上那么整个螺旋路径就会整体错位某些夹在边角之间的元素可能在路径中途被跳过。第二个是边界收缩的时机不对。比如向上打印最右列时行的范围是从 bottom 到 top。如果在此之前已经执行了bottom - 1那么起点就会漏掉右下角那一层元素。而如果没执行bottom - 1右下角元素又会被重复打印。所以“打印”和“收缩”必须跟每一步的指针状态严格匹配。第三个是终止条件里的等号处理。大循环条件写成while top bottom and left right是允许等号的因为当 top 等于 bottom 且 left 等于 right 时内部还有最后一个元素需要打印。如果错误地写成while top bottom and left right那无论矩阵是奇数边长还是只剩一行一列都会漏掉中心那部分元素。4.3 面试现场如何快速自查面试时写完代码我会固定用六个用例在脑子里或者草稿纸上快速跑一遍空矩阵预期返回空数组单行矩阵[[1, 2, 3]]预期输出 1 2 3单列矩阵[[1], [2], [3]]预期输出 1 2 3单元素矩阵[[1]]预期输出 12x3 矩阵验证非对称尺寸下的边界收缩3x3 或 4x4 矩阵验证完整螺旋。只要这六个用例能通过代码基本就稳了。额外分享一个经验写代码前可以先在草稿纸上把一个宽高不等的矩阵画出来从左上角开始沿着逆时针方向把路径描一遍。描的时候注意每一个拐点的位置那些拐点其实就是四条边交接的角点。把角点的处理搞明白代码里的边界条件自然就清晰了。4.4 面试谈吐上的小技巧面试官问到“逆时针打印矩阵”时可以主动确认一下起点“默认是从左上角开始方向序列是下、右、上、左对吗”这句话虽然看起来像在问废话但能立刻避免双方对题目定义的偏差。因为有些面试官理解的“逆时针”可能边界条件不同明确之后后面不会跑偏。如果面试官追问“能不能不用 result 数组直接 print 数字”那当然可以把ans.append(...)直接替换成打印语句即可空间复杂度降为 O(1)。如果问“能不能把矩阵原地旋转后再输出”那是另一道题不要当场混淆。5. 同类题目的举一反三5.1 顺时针打印矩阵只需改方向序掌握了逆时针顺时针就等于送分。四指针法里把四条边的顺序改成“右 → 下 → 左 → 上”其余逻辑完全不动。以 3x3 矩阵为例顺时针输出是 1 2 3 6 9 8 7 4 5逆时针输出是 1 4 7 8 9 6 3 2 5。两者路径互为镜像。所以很多题库把这两道题放在同一章练习因为核心代码模型是同一套改几个字就是另一道题。这也是为什么“面试题29”的变体能被反复用来考察候选人的迁移能力。5.2 从任意中心向外螺旋方向数组方案更通用如果题目改成“从矩阵中心出发逆时针向外螺旋打印”四指针法就不太合适了因为中心可能不在角上而且路径不是“一圈比一圈小”而是“一圈比一圈大”。这种场景更适合方向数组 访问标记法。方向数组法好处是天然支持任意起点、任意方向顺序。你只要给一个初始坐标和方向序号然后每步探测下一个位置是否越界或已访问就能模拟任意形态的螺旋路径。所以在刷题时可以把它当备选方案留着万一碰到变体题不至于没有思路。5.3 蛇形遍历与矩阵旋转矩阵相关的遍历变体还有不少。比如蛇形遍历要求第一行从左到右、第二行从右到左、第三行再从左到右。这种题通常是逐行处理遇到偶数行反转输出顺序和螺旋遍历的边界收缩模型完全不同。再比如矩阵旋转 90 度虽然也有坐标变化但它本质是“元素重排”不是“遍历顺序”解题思路是分圈旋转或者先转置再水平翻转。面试官往往把这几类题目放在一起问目的都是看你是否熟练掌握二维数组下标的变化规律。我个人的建议是把“螺旋遍历”“蛇形遍历”“矩阵旋转”“对角线遍历”这四类基础题都练一遍二维数组的题目基本就全覆盖了。这道题后续还能怎么扩展如果你把逆时针打印的结果反过来看其实就是“顺时针从外到内打印”的另一个变体。搞清楚一条遍历路径的全部细节之后任何方向序变体都可以用同一套边界收缩模型解决。我在实际刷题阶段把顺时针、逆时针、从右下角开始、从右上角开始四种版本都写了一遍过程中犯过的错误几乎都集中在同一类某个方向做完之后没有及时收缩对应指针。所以如果你只能记住一个核心要点请记住“打印一条边 → 收缩指针 → 检查边界”是一个原子操作。把它焊死在代码里这道题就翻不出浪花。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑