资讯动态

PTA螺旋方阵详解:四种解法、常见错误与变体实战

发布时间:2026/10/19 13:49:17 来源:尧图企业网站定制
PTA 上这道“实验7-2-9 螺旋方阵”我估计很多第一次刷到的同学心态都是题目读着简单动起手来全是坑。给定一个正整数 n要你生成一个 n×n 的方阵数字从 1 开始按顺时针一圈一圈地往外绕像蚊香一样铺满整个矩阵。乍一看就是个二维数组填空真正写起来边界判断、方向切换、单行单列的坑一个接一个第一次提交大概率不是 Wrong Answer 就是 Presentation Error。这篇文章我想从解题思路、代码实现、常见错误排查一路讲到这类题往后的变体给正在刷 PTA 的同学一份能直接“抄作业”又能真正吃透原理的参考。无论你现在是刚学到二维数组的初学者还是准备面试想复习模拟类题目的老手这篇都值得花几分钟看完。1. 题目解读与核心考点拆解1.1 螺旋方阵到底长什么样先明确题目在说什么。所谓螺旋方阵就是给定一个正整数 n输出一个 n 行 n 列的数字方阵数字从 1 开始按顺时针方向一圈一圈往内螺旋填充。举个最直观的例子n4 时输出应该是1 2 3 4 12 13 14 5 11 16 15 6 10 9 8 7再看 n5 的情况1 2 3 4 5 16 17 18 19 6 15 24 25 20 7 14 23 22 21 8 13 12 11 10 9PTA 这类题目的判题方式很直接输入一个正整数 n输出这个方阵。常见的输出要求是每个数字占 3 个字符宽度、右对齐也就是用printf(%3d, a[i][j])这种格式。有的版本会用空格分隔数字具体以题面为准但%3d几乎是这类题目的默认格式。搞清楚输出格式特别重要我见过太多思路完全正确、最后因为多了一个空格或少了一个空格被扣分的同学。1.2 题目背后到底在考什么从题目编号“实验7-2-9”就能看出来这题在课程体系里的定位是二维数组和循环结构的综合实验题。前面几题大概是一维数组的增删改查、简单二维数组的读入输出而这题是把二维数组的下标操作、for 循环嵌套、边界条件控制揉在一起考。它的核心考点可以拆成三层第一层是二维数组下标的灵活运用。会写a[i][j] num不算会得能在“第几行第几列”和“当前数字应该放哪”之间建立清晰的映射关系。第二层是边界处理能力这是绝大多数人卡住的地方。填到最外面一圈的右下角时下一步该往哪走什么时候从上边转向右边到达内圈后四个方向的边界又如何确定第三层是模拟能力题目本身不涉及复杂的数学公式就是把“顺时针一圈圈绕”这个过程用代码原样表达出来。这种能力在后续的链表操作、图的遍历、状态机模拟等章节都会反复用到。还有一个容易被忽略的考点循环不变量的维护。每次填充一个格子或一条边你是在维护哪些变量的含义是维护矩阵的上下左右边界还是维护“当前方向 当前位置”这两种思路会直接导向不同的代码风格也是后面要说到的两种主流解法的分水岭。2. 四种主流实现思路与选型分析螺旋方阵的解法非常多我见过各种版本的写法但总结下来无非是四种思路按层模拟、方向控制、递归填充以及少数人用的找规律数学法。下面逐个拆解。2.1 按层模拟法一圈一圈往里填这是最符合人类直觉的思路把方阵看作一层一层的“洋葱皮”从外到内逐圈填充。每一圈都由四条边组成上边从左到右、右边从上到下、下边从右到左、左边从下到上。填完一圈四个边界同时往里收缩一格继续填下一圈。用四个变量top、bottom、left、right分别表示当前未填充区域的上、下、左、右边界。初始时top0、bottomn-1、left0、rightn-1。每一圈执行四次循环先填上边行top列从left到right填完top再填右边列right行从top到bottom填完right--接着填下边填完bottom--最后填左边填完left。外层循环条件是top bottom left right。这种写法的优点是好理解代码结构清晰适合初学者建立空间感。缺点是代码量略多而且必须在循环体内加上单行单列的保护判断否则会出现“填完上边后发现下边和上边是同一条线又回头填了一遍”的问题。2.2 方向控制法撞墙就转弯第二种思路完全不一样不按“圈”来思考而是让数字像小人走路一样沿着当前方向一格一格走走到边界或者已经填过的格子旁边就右转 90 度继续走。这个思路更接近螺旋运动的本质。实现上借助方向数组这是很多算法题里的经典技巧。定义两个数组dx[] {0, 1, 0, -1}和dy[] {1, 0, -1, 0}分别代表向右、向下、向左、向上四个方向。当前方向用变量dir表示初始为 0向右。每一步先试探下一个位置(nx, ny)如果越界或者该位置已经被填充就把dir加 1 取模 4 实现转向然后重新计算下一个位置。方向控制法的代码更紧凑非常适合处理 n 比较大的情况而且扩展性强——想改成逆时针只需调整方向数组的顺序。代价是需要额外用一个二维数组来标记哪些格子已经被走过判断“撞墙”时不仅要看数组边界还要看目标格子是否已被填充。2.3 递归填充法拆解成子问题第三种思路是把一个大方阵拆成最外圈加内部小方阵递归地去填。每一层递归负责填当前层的外圈填完以后把边界往里收缩再递归调用自己处理内层。递归结束的条件是边界交叉即top bottom || left right。递归的代码量最少逻辑也非常优雅。尤其在 n 为奇数时最内层会剩下一个单独的数字递归的终止条件需要仔细处理否则容易差一个数字。递归在递推深度不大的情况下没有问题但如果 n 特别大递归调用的栈开销也要考虑。做 PTA 这题时 n 一般不超过 10递归完全够用不过作为训练前两种非递归解法会更稳一些。2.4 三种思路的优缺点对比这两种解法在时间和空间复杂度上其实没有本质差别都要填满 n² 个格子时间复杂度都是 O(n²)空间上除去输出用的矩阵外几乎不占用额外内存方向数组固定长度 4。真正的差别在于代码风格和出错概率解法代码量理解难度坑点典型应用场景按层模拟中等低单行/单列重复填充初学者建立边界概念方向控制短中死循环、起始点处理面试手写、变体题拓展递归最短中高终止条件、奇数中心理解分治思想我个人建议如果你正在上学校的算法基础课优先把方向控制法吃透。它的思维模式在后续很多题目里都能复用比如机器人行走模拟、迷宫寻路、矩阵遍历等。按层模拟可以作为一种辅助理解的手段写完方向控制法以后再用按层模拟的思路在心里验证一遍结果会理解得更深。3. 代码实现与关键细节打磨理论聊完直接上代码。我分别给出方向控制法和按层模拟法的完整 C 语言实现并逐段拆解关键设计。3.1 方向控制法完整代码与逐段解析#include stdio.h int a[105][105]; // 全局数组自动初始化为0 int main() { int n; scanf(%d, n); // 方向数组右、下、左、上 int dx[] {0, 1, 0, -1}; int dy[] {1, 0, -1, 0}; int dir 0; int x 0, y 0; // 当前位置 int num 1; a[x][y] num; while (num n * n) { int nx x dx[dir]; int ny y dy[dir]; // 判断下一位置是否有效 if (nx 0 || nx n || ny 0 || ny n || a[nx][ny] ! 0) { dir (dir 1) % 4; // 撞墙右转 continue; } x nx; y ny; a[x][y] num; } for (int i 0; i n; i) { for (int j 0; j n; j) { printf(%3d, a[i][j]); } printf(\n); } return 0; }这段代码有四个细节值得反复琢磨。细节一为什么用全局数组。全局变量声明在函数外部C 语言标准保证它会自动初始化为 0这就省去了手动 memset 的麻烦。更重要的是方向控制法判断“这个格子是否已经填过”靠的就是“值为 0 表示未填”因为合法数字从 1 开始0 天然就是“空”的标志。如果乱用局部数组且不初始化里面的值是随机的判断逻辑就直接失效。细节二为什么先单独填a[0][0]。因为螺旋运动的每一步都是“先看下一步能不能走能走才赋值”。如果不提前处理起点循环里第一次就要判断下一步位置0,1是否有效这在逻辑上会绕弯。干脆先把 1 号位放了让循环统一处理“每走一步放一个数”思路就干净了。当然也可以把起点赋值整合进循环但那样初始方向、起始位置都要额外判断得不偿失。细节三转向用dir (dir 1) % 4。这里的取模运算保证方向在 0、1、2、3 之间循环切换。有的同学喜欢用if (dir 3) dir 0; else dir;效果等价但取模写法更简洁修改方向数组时也不容易漏改。细节四while 循环里用continue后再次计算nx、ny。这意味着一次转向不一定够极端情况下可能连转三次才能找到可走的路但每次循环只转一次方向然后回到循环开头重新试探。因为题目保证至少有一个格子可走只要num n*n所以不会有死循环。这个“试探-转向-重试”的模式和机器人避障的简化逻辑非常像。3.2 按层模拟法完整代码与逐段解析#include stdio.h int a[105][105]; int main() { int n; scanf(%d, n); int top 0, bottom n - 1; int left 0, right n - 1; int num 1; while (top bottom left right) { // 1. 填上边行 top列从左到右 for (int i left; i right; i) { a[top][i] num; } top; // 2. 填右边列 right行从上到下 for (int i top; i bottom; i) { a[i][right] num; } right--; // 3. 填下边行 bottom列从右到左 if (top bottom) { for (int i right; i left; i--) { a[bottom][i] num; } bottom--; } // 4. 填左边列 left行从下到上 if (left right) { for (int i bottom; i top; i--) { a[i][left] num; } left; } } for (int i 0; i n; i) { for (int j 0; j n; j) { printf(%3d, a[i][j]); } printf(\n); } return 0; }这段代码最关键的是第 3 步和第 4 步前面的if判断。很多同学抄代码时把这两个 if 漏了结果 n3 或 n4 时总能跑对一到 n1 或 n2 就出现诡异的数字覆盖。我用 n1 来说明白为什么必须要有这两个判断。n1 时初始状态top0, bottom0, left0, right0。进入循环第一步填上边填的是坐标 (0,0)数字 1 就位然后top变成 1。此时top bottom说明上边界已经越过下边界理论上整个矩阵都填完了。但如果没有 if 保护第二步“填右边”的循环是for (i 1; i 0; i)循环体一次都不执行不会出问题可到了第三步“填下边”因为top已经变成 1而bottom还是 0循环条件i bottom不成立也不会执行。这里的连锁反应最终导致最后只有左上角一个数字被填整段代码虽然没报错但输出错得离谱。加上if (top bottom)和if (left right)后n1 时第三步和第四步直接跳过输出就是单个数字 1。同理n2 时第二圈只有一个 2×2 的壳若不加保护填完上边和右边后下边会重复覆盖已经填过的区域。3.3 通过循环不变量理解两种解法用一个统一的概念可以把两种解法串起来循环不变量。按层模拟法的循环不变量是“每一轮 while 循环开始时top、bottom、left、right四个变量精确地表示尚未填充的矩形区域”。只要你在每一轮循环结束时正确地更新这四个变量那么下一轮循环开始时所有关于“下一个要填的位置在哪里”的推导都建立在一致的基础上。很多同学写着写着就乱套根因是某一轮里少更新了某个边界导致后续推理全部崩塌。方向控制法的循环不变量则是“每一步执行后当前位置记录在一个已被正确标记的格子内当前方向是合法的下一步参考方向”。每次循环只做两件事试探下一步不能走就转方向能走就走一步并填数。由于“已填格子”这个判断条件会严格阻止重复访问所以算法一定会在填满 n² 个格子后自然终止。理解循环不变量最大的好处是当你调试代码时不需要把整个流程在脑子跑一遍只需要在关键节点插入验证语句检查边界变量或当前坐标是否符合预期。这是从“会写代码”到“能发现代码问题”的分水岭。3.4 输出格式与提交时的细节处理很多同学代码逻辑没问题却在 PTA 上连续吃 Presentation ErrorPE原因就出在输出格式上。螺旋方阵最常见的格式要求是每个数字占 3 位宽度右对齐也就是printf(%3d, a[i][j])。需要注意%3d对一位数会输出两个空格再加一个数字即“空格空格数字”每一行行首也会有空格判题系统是逐字符比对的所以整体格式只要统一就行。有的题目版本会改成“数字之间用一个空格分隔”。遇到这种要求就不能用%3d了需要在每行内区分“是否是该行第一个数字”。正确写法是for (int i 0; i n; i) { for (int j 0; j n; j) { if (j 0) printf(%d, a[i][j]); else printf( %d, a[i][j]); } printf(\n); }建议提交前先把题目要求的输出示例复制到本地自己跑一遍用 diff 工具对比肉眼查空格和换行是最容易漏的。另外一定要单独测试 n1、n2 这两个最小边界超过半数的螺旋方阵提交错误都藏在这两个用例里。4. 高频错误与排查实战记录4.1 最容易翻车的三类错误我把在 PTA 和实际教学中见过的高频错误总结了三大类第一类是全盘错乱型输出的数字顺序里出现了从外层跳回内层、或者数字互相覆盖的情况。这通常是按层模拟法没加单行单列保护导致的方向控制法则很少犯这种错。典型特征是 n3、n4 时输出中间某个位置被重复赋值后面的数字顺序全错。第二类是越界崩溃型程序运行时报段错误或输出结果中混入垃圾值。常见原因是方向控制法的方向数组写错顺序或者按层模拟法的循环条件写成了top bottom而不是top bottom导致最内圈根本没进去。还有一种可能是数组开小了比如题目说 n≤10你只开了int a[10][10]访问到 a[9][9] 没问题但填到第 10 个格子就越界了。开数组宁大勿小。第三类是格式错误型这个最常见也最冤枉数字之间多了空格、行末多了空格、每行数字占位宽度不足、换行符缺失。上一小节已经讲过解决方法这里不再重复。4.2 一个典型 Wrong Answer 案例的完整复盘有一次一个同学拿着代码来找我他说 n1、2、3、4 在本地全部跑对了但一提交就是 Wrong Answer。我让他把输出重定向到文件再用题目样例做 diff第一眼就看到 n5 的时候中间那个 25 跑到了左上角。整个矩阵从外圈看是正确的但内圈从第三圈开始就乱了。原因他一行一行查了很久没发现他的按层模拟写着写着把“填左边”那步写成了for (int i bottom; i top; i--) { a[i][left] num; }问题出在他在这步之前把bottom--已经执行了所以“填左边”时漏掉了最下面那个格子。回头看他的代码本来正确的顺序应该是“填下边 → bottom-- → 填左边”但他把 bottom-- 提到了填下边的循环之前导致下边没填就收缩了边界。这种“边界变量更新时机错误”几乎不可能通过看代码发现只有借助小规模用例逐个比对才能定位。后来我把 n5 的手算结果打印成表格把每一圈的 top、bottom、left、right 在每一轮循环结束时的值也打印出来他一眼就看到了 25 所在位置对应的边界状态不对。这个案例告诉我们一件事遇到逻辑看不懂的错与其盯着代码死看不如把边界变量的变化过程印出来。数据会帮你把问题定位到具体的那一轮循环。4.3 调试螺旋方阵的实用技巧分享几个我实测高效的调试方法。技巧一小规模穷举。写一个循环让程序自动跑 n1 到 n6然后把所有结果打印出来和手算对照。手算带 n6 不现实但可以只检查每个矩阵的关键特征第一行是否是 1 到 n最后一列是否是 2n-1 往下走矩阵中心位置的数字是否等于 n²奇数或正确的一对数偶数。这些特征能快速捕捉大部分错误。技巧二中间变量打印。在关键位置加上临时的 printf打印方向、当前坐标或边界值。调试完再删掉。比如方向控制法里每次转向时打印一下“转向前位置、转向后的方向”能立刻看出哪里多转了一次。技巧三纸上走一遍最内圈。很多人搞不清奇数方阵中心那个格子到底在哪个环节被填充。纸上画一个 5×5 的格子用笔模拟程序走最内圈此时top2, bottom2, left2, right2按层模拟会先填上边也就是 25 这个位置然后 top 变 3后面的循环因为条件不满足全部被跳过。走完这一步整个流程就通了。技巧四用“水印”检查重复覆盖。在按层模拟的每个赋值语句后面多加一个计数如果总赋值次数少于 n²说明某些格子被跳过如果数组里某个位置被赋了两次说明循环边界重叠了。这个计数检查可以写成一个辅助函数跑完一次自动报告比肉眼盯输出高效得多。5. 从螺旋方阵延伸出去的实战价值5.1 螺旋矩阵的常见变体题螺旋方阵做熟以后有几个高频变体几乎是面试和竞赛的常客我强烈建议顺带练一练因为它们用的就是同一套思维模型。第一个变体是“逆时针螺旋方阵”。解法非常简单把方向数组从右、下、左、上改成下、右、上、左或者把按层模拟的四条边顺序反过来填。很多同学会以为要重写整个算法其实只是方向和顺序的调整。第二个变体是“螺旋读取”。给定一个二维矩阵要求按螺旋顺序把元素读出来输出成一维数组。这个题不要求填数而是要求遍历核心区别在于遍历时要注意“已经读过的格子”和方向控制法里的“已填充格子”是同一套判断逻辑。第三个变体是“非方阵的螺旋矩阵输出”比如 3×4 的矩阵按螺旋顺序输出。这会打破“方阵”的舒服感因为最内圈可能不是完整的圈而是一条线甚至一个点这时候边界条件的组织方式就特别考验对 top/bottom/left/right 的理解。建议做完基本题以后自己把代码改成支持任意行数和列数的版本几乎是必考题。第四个变体是“由外到内蛇形填数”即每一圈的行内不是按顺序而是来回蛇形。这个只改变了局部填充方向整体框架不变适合用来检验对螺旋过程的理解深度。5.2 这道题教会我的算法思维刷题刷多了你会发现真正让你成长的往往不是“这道题的答案”而是这道题逼你想清楚的几个抽象概念。第一个概念是边界变量优先于具体坐标。在按层模拟里不直接想“下一个填哪”而是问“当前未填充区域的边界是什么”。这种从“用坐标描述位置”上升到“用边界描述区域”的思维方式在后续处理矩阵旋转、图像裁剪、图形学裁剪算法等场景中会反复出现。第二个概念是模拟的本质是保持不变量。螺旋方阵看起来像脑筋急转弯其实是一道标准的“小规模状态模拟题”。你只需要保证每一步的状态转移移动方向 赋值是在不变量约束下进行的最终的输出就会正确。这个思想在复杂系统模拟、编译器状态机、操作系统的进程调度模拟里是同一套方法论。第三个概念是选择最优表达方式。方向数组把“方向”这种抽象信息编码成数组和索引是一种非常经典的建模手法边界变量把“区域”编码成四个整数则是另一种视角。同一个问题换一种表达方式代码的清晰度和扩展性天差地别。这道题给了你同时练习两种建模的机会非常难得。这题做完以后我还会建议你把方向数组法改成逆时针再改成从矩阵中心向外螺旋填充。这几种改法表面上是改参数实际上是在训练你“识别哪些代码是核心逻辑、哪些代码只是参数配置”的能力。能够自由调整方向、起点、形状说明你已经真正理解了解法而不只是背下了代码。我在实际带新人的过程中发现能把这道题吃透的人在后来的链表遍历、迷宫问题、二叉树层序遍历里很少再栽跟头。因为那些题本质上都在做同一件事把“当前位置”与“下一步方向”的关系维护清楚同时守住边界条件。所以如果你现在正被这道题卡得头疼别急着抄答案照着上面的思路自己把两种方法各写一遍再手动跑几个用例花一个小时彻底弄懂它比刷十道简单题都值。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑