深入剖析螺旋遍历:边界收缩与方向向量法

发布时间:2026/9/14 6:21:56
深入剖析螺旋遍历:边界收缩与方向向量法 第一次刷到“蜗牛排序”这个词的时候我先是愣了几秒——排序算法里哪有蜗牛这种说法点进题解页面才发现它就是LeetCode 54题Spiral Matrix的中文圈戏称因为螺旋遍历的路径实在太像蜗牛壳的纹路了。说实话这道题在算法题里属于“看起来人畜无害一写就翻车”的典型很多刷题刷到中期的朋友都会在边界条件上栽跟头。这篇题解不打算只贴一份能过的代码而是想好好聊聊螺旋遍历的两种主流写法、它们各自的适用场景、边界条件为什么容易出错、以及从这道题延伸出去的一票变体题希望能让不同基础的读者都能一次性把这一个知识点吃透。1. 蜗牛排序的本来面目一道被名字耽误的经典题目1.1 题目到底在问什么给定一个 m 行 n 列的矩阵 matrix按照顺时针螺旋顺序返回这个矩阵中的所有元素。举个例子对于下面这个 3×3 矩阵1 2 3 4 5 6 7 8 9螺旋遍历的输出顺序是1, 2, 3, 6, 9, 8, 7, 4, 5。从左上角出发先沿着第一行一直往右走到头然后向下走到最后一行再向左走回最左边最后向上走回第二行的起点下方接着再往右……每一圈结束后可遍历的范围就向内收缩一圈直到所有元素都被访问完。这里要注意区分“螺旋遍历”和“蛇形遍历”。蛇形遍历是每一行从左到右、下一行从右到左呈 S 型折返而蜗牛排序在一圈内始终是朝同一个方向走完一整条边然后转弯路径是盘旋向内的。二者名字虽然都带“蛇”或“蜗牛”但是完全不同的两种遍历规则千万不要混为一谈。1.2 为什么这道题值得单独写一篇题解先说结论螺旋遍历的代码量不大但边界条件极易出错尤其是单行、单列、非方阵这几类输入几乎是面试手写时的“重灾区”。从我刷题和带新人的经验来看这道题的考察价值在于三件事。第一它考察对二维数组下标控制的熟练度很多写业务代码写得多的人反而不太习惯同时对行和列两个方向做边界收缩。第二它是一系列矩阵遍历题的“母题”理解了螺旋遍历的框架LeetCode 59 题螺旋矩阵 II、885 题螺旋矩阵 III、剑指 Offer 29 题基本可以在一个下午全部拿下。第三这类模拟题的思维方式和竞赛中的很多网格题是一致的比如有些搜索题会在螺旋路径上做标记或维护状态底层都是这套边界控制逻辑。所以别因为 LeetCode 把它标成“中等”就轻视也别因为网上题解多就草草抄一遍。真正吃透它后面能省下不少时间。2. 边界收缩法最直观、最适合面试手写的解法2.1 核心思路四句话边界收缩法的思路非常朴素把未遍历的区域看成是一个矩形用四个变量记录矩形的上下左右边界top 0bottom m - 1left 0right n - 1第一步从left到right遍历top这一行结束后top第二步从top到bottom遍历right这一列结束后right--第三步如果top bottom从right到left遍历bottom这一行结束后bottom--第四步如果left right从bottom到top遍历left这一列结束后left循环执行上述四步直到top bottom或left right。每次走完一条边就把这条边从“未遍历区域”中切掉。最开始理解的时候可以把它想象成削苹果皮每转一圈苹果就小一圈直到削完。这个比喻虽然有点粗糙但能帮助记住“每走完一条边边界要立刻收缩一次”这个关键动作。2.2 完整代码C 与 Python 双版本先看 C 版本class Solution { public: vectorint spiralOrder(vectorvectorint matrix) { vectorint res; if (matrix.empty() || matrix[0].empty()) { return res; } int top 0, bottom matrix.size() - 1; int left 0, right matrix[0].size() - 1; while (top bottom left right) { // 左 - 右遍历当前最上面一行 for (int j left; j right; j) { res.push_back(matrix[top][j]); } top; // 上 - 下遍历当前最右边一列 for (int i top; i bottom; i) { res.push_back(matrix[i][right]); } --right; // 右 - 左需要确认 top 和 bottom 没有交错 if (top bottom) { for (int j right; j left; --j) { res.push_back(matrix[bottom][j]); } --bottom; } // 下 - 上需要确认 left 和 right 没有交错 if (left right) { for (int i bottom; i top; --i) { res.push_back(matrix[i][left]); } left; } } return res; } };再看 Python 版本逻辑完全一样def spiral_order(matrix): res [] if not matrix or not matrix[0]: return 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: for j in range(right, left - 1, -1): res.append(matrix[bottom][j]) bottom - 1 if left right: for i in range(bottom, top - 1, -1): res.append(matrix[i][left]) left 1 return res2.3 为什么“走完一条边收缩一次”是关键我见过不少初学者把四个方向的遍历写在一起然后一整圈走完再统一更新四个边界。这样在方阵里可能碰巧能跑对但一旦遇到非方阵或者行数列数相差较大的情况就会出现漏元素或者重复读元素。原因很简单四个方向对边界的消耗是不同步的右行消耗的是上边界下行消耗的是右边界左行消耗的是下边界上行消耗的是左边界。如果非要等一整圈结束再统一收缩那么内层循环的边界条件就必须额外考虑当前已经走到第几圈逻辑会复杂很多。所以在写的时候我习惯把“走完一条边”和“收缩对应边界”绑定在一起当作一个不可分割的状态更新。这样循环不变量一直很清楚每次循环开始时未遍历的区域始终是[top..bottom] × [left..right]这个矩形不会出现需要脑补中间状态的情况。3. 方向向量法当题目条件变多时的通用框架3.1 方向状态机与转向条件边界收缩法有一个隐性前提矩阵是完整矩形从左上角出发顺时针螺旋。如果题目变成“从任意一个格子出发按螺旋顺序收集元素”或者“在网格上螺旋走 K 步记录经过的格点”边界收缩法就不够灵活了。这时候第二种解法——方向向量法是更好的选择。思路是这样用一个方向数组表示四个移动方向int dirs[4][2] {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; // 右下左上维护当前位置(x, y)和当前方向dir。每次先尝试按照当前方向走到下一个格子(nx, ny)如果这个格子越界了或者已经访问过了就转向也就是把dir加一对 4 取模然后再走。伪代码while 访问的元素个数 总元素个数: 输出 / 记录当前格子 计算 next (x dirs[dir][0], y dirs[dir][1]) if next 越界 or 已经访问过: dir (dir 1) % 4 重新计算 next x, y next这种方法本质上是一个状态机当前状态是“位置 方向”一旦下一步不可达就切换方向。3.2 visited 数组用额外空间还是原地修改方向向量法需要一个办法判断“这个格子是否已经访问过”。最常见的做法是开一个同样大小的二维布尔数组visited [[False] * n for _ in range(m)]每走一个格子就把visited[x][y]置为 True。这种做法空间复杂度是 O(m*n)但逻辑清晰、实现简单而且在真实工程里不会破坏原始数据。另一种做法是原地标记把已经访问过的格子改成某个特殊值比如 0。这种方法省空间但有明显风险。如果矩阵里本身就允许出现 0就会把“已访问”和“原始数据是 0”混在一起导致重复访问或漏访问。即使题目保证了矩阵元素都是正数我也建议慎用原地修改因为面试官非常喜欢追问“如果矩阵里有 0 怎么办”一旦你用了原地标记却说不清楚会显得考虑不周全。3.3 两种解法的对比与选型建议对比维度边界收缩法方向向量法时间复杂度O(m*n)O(m*n)空间复杂度O(1)不计结果数组O(m*n)通常需要 visited代码量稍短稍长适合场景标准螺旋遍历面试手写变体题如任意起点、螺旋走 K 步、带障碍物理解门槛低直观中等需要理解方向状态机有一个误区需要特别提醒不要觉得方向向量法“更高级”就一定更好。对于 LeetCode 54 这种标准题边界收缩法不管是写起来、调试起来、还是解释起来都更舒服。方向向量法的优势只有在变体题里才体现得明显。我自己的习惯是标准题用边界收缩变体题优先考虑方向向量两个都要能不假思索地写出来。4. 复杂度与边界情况最容易翻车的地方4.1 性能分析为什么一定是 O(m*n)不管用哪种写法每个元素都只会被访问一次所以时间复杂度是 O(mn)这也是此类遍历问题的下界——你总得把每个格子都看一遍才能输出结果。空间复杂度上边界收缩法如果不把结果数组算进去是 O(1) 的额外空间方向向量法则因为 visited 数组额外空间是 O(mn)。在实际面试中面试官不会在这道题上卡复杂度分析但会追问“能不能优化空间”。这时候你要能说清楚边界收缩法本身已经是 O(1) 额外空间除了结果数组所以没有进一步优化空间。这个回答通常就能过关。4.2 单行、单列、空矩阵等特例逐个过我建议每写完一个矩阵遍历的解法第一件事就是拿下面几组特殊用例去验证空矩阵[]直接返回空数组。这一步很多人会漏因为matrix.size()可能为 0但matrix[0]已经越界所以要先判空再判matrix[0].empty()。单行矩阵[[1, 2, 3, 4]]期望输出[1, 2, 3, 4]。边界收缩法在走完“左到右”后top变成 1此时while条件top bottom已经为假循环结束不会进第三步第四步所以输出正确。单列矩阵[[1], [2], [3], [4]]期望输出[1, 2, 3, 4]。2×2 矩阵[[1, 2], [3, 4]]期望输出[1, 2, 4, 3]。1×1 矩阵[[1]]期望输出[1]。非方阵比如 3×4 或 4×3这是最能体现边界收缩法优势的用例因为top、bottom、left、right各自独立更新天然支持非方阵。多提一句刷题平台有时候给的测试用例会很刁钻比如矩阵是[[6, 9, 7]]这种单一长条或者是[[7], [9], [6]]这种单一竖条。如果代码里没有第三、第四步的if判断这些用例会直接报数组越界或者重复输出。这是我在多次辅导别人写这道题时反复看到的错误。4.3 一份常见的错误示范while (top bottom left right) { for (int j left; j right; j) res.push_back(matrix[top][j]); top; for (int i top; i bottom; i) res.push_back(matrix[i][right]); right--; for (int j right; j left; --j) res.push_back(matrix[bottom][j]); // 少了 if bottom--; for (int i bottom; i top; --i) res.push_back(matrix[i][left]); // 少了 if left; }这个错误在单行矩阵[[1, 2, 3]]下会立刻暴露第一轮完成后top变成 1此时第二、第三、第四步其实都越界了。少了if (top bottom)的保护第三步会去访问matrix[1]也就是不存在的下一行导致运行时错误或者随机值。正确写法就是在第三、第四步前分别加上边界重叠检查。5. 蜗牛排序的经典变种从螺旋填充到蛇形遍历5.1 LeetCode 59螺旋矩阵 II 是它的镜像题LeetCode 59 题要求给定一个正整数 n生成一个 n×n 的矩阵矩阵元素按螺旋顺序填入 1 到 n^2。比如 n3输出1 2 3 8 9 4 7 6 5这题和 54 题是镜像关系54 是从矩阵里“读”出螺旋序列59 是把序列“写”进矩阵。解题框架完全一样只需要把代码中的“读操作”换成“写操作”matrix[top][j] num; matrix[i][right] num; matrix[bottom][j] num; matrix[i][left] num;边界收缩的顺序、if 判断的位置、以及循环结束条件都跟 54 题一模一样。如果你能不看任何提示把 54 题写对59 题就是一道“照猫画虎”的题很快就能过。5.2 逆时针、任意起点、从内到外等其他变体螺旋遍历的变体非常多但只要理解方向状态机的本质处理起来就有规律可循。逆时针螺旋如果是边界收缩法把遍历顺序改成“从上到下、从右到左、从下到上、从左到右”如果是方向向量法把方向数组改成{{1,0},{0,-1},{-1,0},{0,1}}这个顺序也就是“下、左、上、右”。从任意起点出发的螺旋遍历这个用方向向量法最自然。先定位到起点然后按照方向数组依次尝试向右、下、左、上移动同时用 visited 数组记录访问状态直到访问完所有格子。从中心向外螺旋填充这个稍微难一点常见于某些竞赛模拟题里“从信号源向外扩散”的场景。一个直观做法是从中心格开始按“右、下、左、上”的顺序步数逐渐增加地走或者直接用方向向量法配合“两步一转向”的规律实现。这些变体看似五花八门但它们共享同一个底层框架位置、方向、转向条件。抓住这三件事什么变体都只是改改参数而已。5.3 竞赛和面试题里的“伪包装”在真实的面试和竞赛中螺旋遍历很少作为一道孤立的大题出现更多时候是作为某个复杂题目的一个步骤被嵌套进去。比如有些搜索类题目会要求你“按螺旋顺序把某个区域的元素累加”这时候你需要先把螺旋遍历函数单独写出来再做后续处理。ICPC 区域赛和网络赛中类似“在一个网格上定位并标记某个区域”的题目经常用到这种边界收缩的思想。洛谷上也有很多普及组题目表面上是“蛇形填数”“螺旋矩阵”实际上都是同一个知识点的不同包装。如果你能把 54 题的两种写法都吃得透透的看到这类题基本就是白送分。6. 实战踩坑记录与刷题建议6.1 我自己写这道题时踩过的两个典型坑第一个坑是把循环终止条件写成了top bottom left right而不是。一开始我觉得“一圈遍历完就收缩一圈只剩中心一个元素的时候再单独处理”结果在 4×4、5×5 这类偶数边长矩阵中反复漏元素尤其是在处理最内层 2×2 区域的时候逻辑非常痛苦。后来干脆改成并配合内部 if 判断一下子清爽了。这个教训总结起来就是能用“边走边收缩”解决的事不要额外搞特殊分支。第二个坑是方向向量法里忘了在转向后立刻重新计算下一步坐标。我当时写了类似这样的逻辑if 下一步越界: dir (dir 1) % 4 # 这里忘记更新 nx, ny结果就是方向虽然切换了但当前这一步还是用旧的方向坐标去走导致连续遇到越界甚至死循环。后来我强制自己在转向后立刻重新计算nx和ny再也没有出过这种问题。6.2 刷题路径和参考资料的安排我给不同基础的朋友一个练习路径建议。如果是零基础新手先把 LeetCode 54 题的边界收缩法写明白跑通单行、单列、非方阵几组用例然后看一遍 59 题自己实现一遍螺旋填充最后用 885 题螺旋矩阵 III 来体验方向向量法。如果是在准备面试54 题要做到 5 分钟内独立写出边界收缩法并且能口头解释清楚为什么必须在第三、第四步加 if 判断。面试官追问空间优化时要能明确回答“边界收缩法已经是最优空间”。如果是在准备算法竞赛可以把洛谷上关于螺旋矩阵、蛇形填数的普及组题过一遍再找一两道区域赛模拟题练练手。重点是练习把螺旋遍历作为一个子模块嵌入到更大的解法中。参考资料方面B 站上灵茶山艾府有一系列题解视频讲得很细对于理解边界条件和模拟类题目的思维框架很有帮助。牛客和力扣的中文题解区这道题的热度高所以高赞答案普遍质量不错可以作为补充材料。英文讨论区也可以看但中文社区里这类基础题的高质量题解已经够用了。最后再分享一条我觉得最实用的经验拿到任何“按某种规则遍历”的题目先在草稿纸上把路径画出来只要把路径图搞清楚了代码就是翻译工作。画图这一步看起来笨但真的能帮你避开一大半边界 bug。我写螺旋遍历这么多次从来没有一次是靠硬想代码结构写对的都是先在脑补路径再去套框架。蜗牛排序这种题只要路径图画明白了它就是一道手速题。