C++螺旋方阵两种解法:二维数组方向控制与边界处理详解
2026/9/8 14:09:35 网站建设 项目流程

东华OJ刷到基础题第77题螺旋方阵时,我本来以为几分钟就能收工,结果第一版代码WA了,第二版才顺利通过。后来回头看,这一题其实特别有代表性:代码量不大,但方向控制、二维数组下标、边界处理、输出格式这些C++新手最容易糊的点,它全占了。网上能AC的代码很多,但不少人都只是“背下来能过”,真让自己从零推一遍,很容易在细节上翻车。

这篇就把螺旋方阵给彻底讲透。我会先拆题,再给两套完全不同思路的C++写法,一套是逐层剥洋葱,一套是方向数组模拟;然后专门列一下我在OJ提交时踩过的坑和排查过程,最后说说怎么从这道基础题延伸出去练更多矩阵类题目。适合刚开始刷OJ、正在学二维数组和循环控制的朋友,也适合那些早已AC但想弄明白“为什么这么写”的人。

1. 拆题:螺旋方阵到底在考什么

1.1 先把题面拉齐:你要输出的是一个n×n的顺时针矩阵

东华OJ基础题77的题目描述,不同的版本可能措辞稍有差异,但核心场景很一致:输入一个正整数n,生成一个n行n列的方阵。从左上角开始,先往右填1、2、3……,等填到边界就转向下,继续填,然后向左、向上,一圈一圈往内层绕,直到把n×n个数全部填完,最后按行输出整个方阵。

拿n=3举例,输出长这样:

1 2 3 8 9 4 7 6 5

如果你再手推一下n=4,应该是:

1 2 3 4 12 13 14 5 11 16 15 6 10 9 8 7

很多人一看就说“哦,这不就是二维数组填数嘛”。对,它表面上确实只是给二维数组按顺序赋值,但“按顺序”这三个字背后藏着几个很基础又很容易出错的点。东华OJ把它放在基础题序列里,就是因为这道题能同时考察你对循环边界、方向切换和数组下标三个基本功的掌握程度。

1.2 这三个考点才是真正的核心

第一个考点是二维数组的基本使用。你要定义一个足够大的二维数组,比如int a[30][30]或者用vector<vector<int>>,然后通过a[i][j]来访问每个格子。这个大家都会,但细节上仍然有坑,比如数组内部定义的局部数组如果没有初始化,里面存的可能是随机值,后面用这些值做判断就会莫名其妙出错。

第二个考点是状态方向。螺旋填充可以理解为一个小人站在方阵里,手里拿着计数器,从1开始数,每走一格就填一个数。一开始它面朝右走,撞到边界或者走过已经填好的格子就右转90度,继续走。这个“方向状态”怎么用代码表达,是整道题的分水岭。

第三个考点是边界控制。什么时候该转弯?转弯之后下一步落在哪个坐标?循环什么时候结束?很多人的代码在n=3时能跑通,一换成n=5就乱了,多半就是边界写错。尤其当n是奇数时,最中间会剩下一个单格,很多解法在这里会重复填或者漏填。

1.3 动手之前先把两件事定下来

我在带新手做这题时,会让他们先不写代码,口头回答两个问题:第一,填数的方向顺序是什么?顺时针方向下,永远是“右、下、左、上”循环;第二,什么时候停?填满n×n个格子就停。这两个问题想明白后,所有代码都是在为这两条规则服务。

如果方向固定是顺时针,那这个秩序就不会变。你不需要担心某个格子要不要填,只要按规则走下去,每一步填的格子一定是唯一确定的。所以接下来两套思路,本质上都是在回答同一个问题:如何让程序知道“下一步该往哪个方向走”以及“下一步走到哪个格子”。

2. 方案一:按圈填充法,也是最容易手推的思路

2.1 一圈一圈往里“剥洋葱”

螺旋方阵可以理解成一圈一圈的矩形边框嵌套组成。最外层是第一圈,往里缩一圈是第二圈,一直缩到中心。如果n是偶数,最后会没有中心;如果n是奇数,最后一圈就退化成单独一个格子。

这种“按圈填充”的思路,代码里最直观的做法是定义两个坐标:start表示当前圈的左上角行号和列号,end表示当前圈的右下角行号和列号。每处理完一整圈,start加1、end减1,也就是把矩阵往里收缩一圈。

比如n=5,最开始start=0、end=4,填最外面一圈;然后start=1、end=3,填里面一圈;最后start=2、end=2,只剩中心一个格子。这种方式就像剥洋葱,一层一层往里走,所以我也叫它剥洋葱写法。它最大的优点是好理解,你可以在草稿纸上先画出方阵,然后用笔模拟每一圈的填充路径,再对照着写代码。

2.2 四条边怎么填不会重也不会漏

每一圈可以拆成四条边:上边、右边、下边、左边。最容易写错的地方就在角的处理上。比如最外一圈上边如果从start到end都填,那右上角已经被填了;右边这一列再填的时候,行的起点就必须是start+1,从上边结束位置的下一格开始,才能避免重复填右上角。

同理,右边这一列结束时会填到右下角,所以下边这一行从右往左填的时候,列的起点要用end-1,跳过右下角;下边填完会填左下角,所以左边这一列从下往上填的时候,行的起点要用end-1,行的终点是start+1,这样既能绕开左下角,也不会再把左上角重复填一遍。

这个“每条边的起点要让开上一段的终点”的小细节,是整个按圈写法的灵魂。如果不做这个处理,直接让四条边都从start到end,第一圈就会把四个角重复填好几遍,后面的数字顺序全是乱的。我第一次WA就是因为这里偷懒了。

2.3 剥洋葱写法的完整代码

#include <iostream> using namespace std; int a[30][30]; int main() { int n; cin >> n; int num = 1; for (int start = 0; start < (n + 1) / 2; start++) { int end = n - start - 1; // 上边:从左到右,包含左上角和右上角 for (int j = start; j <= end; j++) { a[start][j] = num++; } // 右边:从上到下,起点避开右上角,包含右下角 for (int i = start + 1; i <= end; i++) { a[i][end] = num++; } // 下边:从右到左,起点避开右下角,包含左下角 for (int j = end - 1; j >= start; j--) { a[end][j] = num++; } // 左边:从下到上,避开左下角和左上角 for (int i = end - 1; i > start; i--) { a[i][start] = num++; } } for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { if (j != 0) { cout << ' '; } cout << a[i][j]; } cout << '\n'; } return 0; }

这段代码里,最外层循环的次数是(n + 1) / 2。n=3时是2次,第一圈处理外围,第二圈处理中心;n=4时也是2次,两圈刚好处理完。内层四条边的循环范围,就是上面讲的“上含两端、右含下端、下含左端、左不含两端”。

如果你把n=5的情况在纸上推一遍,会发现中心格子a[2][2]是由“上边”那一段循环单独填掉的,因为当start=2、end=2时,后面三段的循环起点都比终点大,根本不会执行。这个天然行为就让奇数中心不必单独写if判断,代码反而更简洁。

2.4 剥洋葱写法有哪些需要注意的地方

按圈写法最容易出问题的场景是n比较小的时候。n=1时,最外圈循环只执行一次,start=0、end=0,上边给a[0][0]填1,其余三段循环条件不成立,输出正确。n=2时,上边填a[0][0]=1、a[0][1]=2,右边从i=1到end=1,填a[1][1]=3,下边从end-1=0到start=0,填a[1][0]=4,左边不会执行。也没问题。

如果你要改成逆时针,或者改成从右下角开始填,这种按圈思路就得重新推理边的起止顺序,通用性相对差一些。所以在OJ练习里,我更建议你理解它,但日常更推荐用下面这一种写法。

3. 方案二:方向数组模拟法,工程上更稳的选择

3.1 把“方向”也变成数据

按圈写法的核心难点在于,每条边的起止范围都要人肉算一遍,容易错。方向数组模拟法则换了一个思路:不让程序“知道自己在填第几圈”,而是让程序每一格都问自己一个问题——下一步能不能继续往前走?

如果想要表达“先向右走,走不动就转向下,再走不动就转向左,转完继续”,最自然的代码就是方向数组。比如定义两个数组dx和dy,分别表示横向和纵向的偏移量:

int dx[4] = {0, 1, 0, -1}; int dy[4] = {1, 0, -1, 0};

这里的下标0、1、2、3分别代表右、下、左、上四个方向。当前位置是(x, y),当前方向是d,那么下一步的位置就是:

int nx = x + dx[d]; int ny = y + dy[d];

方向数组把“坐标加减”变成了查表操作,代码非常统一。你不再需要分别写“向右时y加1、向下时x加1”这样的分支,只要查表就行,出错的概率低很多。

3.2 什么时候转向:撞墙或者踩到老格子

方向数组写起来不难,关键是怎么判断该转向。原则只有一条:下一步的位置不在方阵范围内,或者这个位置已经填过数了,就右转90度。

“不在方阵范围内”很好理解,就是nx小于0、nx大于等于n、ny小于0、ny大于等于n这四种情况。“已经填过数”的判断,依赖于一个初始为0的二维数组。我们填进去的数都是正整数,所以只要发现下一个格子a[nx][ny]不等于0,说明这个地方早就被填过了,这条路走不通,必须转向。

这里有个前提:数组必须是干净的,也就是初始值都是0。全局数组默认会清零,但如果数组定义在main函数内部,一定要手动初始化,写成int a[30][30] = {};。很多本地跑得好好的代码,一提交到OJ就出问题,就是因为数组里残留了上一次运行留下的脏数据。

3.3 方向数组写法的完整AC代码

#include <iostream> using namespace std; int a[30][30]; int dx[4] = {0, 1, 0, -1}; int dy[4] = {1, 0, -1, 0}; int main() { int n; cin >> n; int x = 0, y = 0; // 当前位置,从左上角出发 int d = 0; // 当前方向,0右 1下 2左 3上 for (int num = 1; num <= n * n; num++) { a[x][y] = num; // 在当前格子填数 // 先按当前方向往前走一步,看看这个位置能不能落 int nx = x + dx[d]; int ny = y + dy[d]; // 走不了就顺时针转向 if (nx < 0 || nx >= n || ny < 0 || ny >= n || a[nx][ny] != 0) { d = (d + 1) % 4; nx = x + dx[d]; ny = y + dy[d]; } // 更新坐标 x = nx; y = ny; } for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { if (j != 0) { cout << ' '; } cout << a[i][j]; } cout << '\n'; } return 0; }

循环从num=1开始,一直执行到num=n×n,每轮先填当前格,再尝试走向下一格。如果当前位置的下一个格子越界或已被填过,说明该转向了,通过(d + 1) % 4把方向从右切到下,从下切到左,从左切到上,从上切回右,形成循环。

这种写法的好处是,不管你当前处在矩阵的哪个角落,你都不用去记“这已经是第几圈、圈的边界是什么”,程序每次只需要判断下一格能不能走。把“撞墙”和“重复”两种情况统一处理,逻辑非常干净。

3.4 为什么我更推荐方向数组作为主力解法

举一个很实际的例子:如果题目改成从右上角出发按顺时针填充,按圈写法需要重新推导四条边的起止范围,大改一遍;方向数组写法只需要把起点改成x = 0, y = n - 1,再把方向数组的顺序调整一下,其他部分完全不用动。如果改成逆时针,也只需要把方向数组改成上、右、下、左的顺序。这种可维护性,在平时练习和笔试手写代码时都很占便宜。

另外,方向数组不只是螺旋矩阵能用。日后遇到迷宫类DFS、BFS题目时,你经常会用到类似的dx/dy数组来表示上下左右四个方向。提前在螺旋方阵这种基础题里把方向数组用熟,后面学图遍历会轻松很多。

我见过不少人觉得,方向数组每次都要“空想走一步再判断回头”,没有按圈写法直观。这个担心正常,但只要在纸上把n=3的流程走一遍,就会很快适应。毕竟在代码里,“试探–判断–决定”本来就是很常见的模式,方向数组把这个模式固定了下来。

4. 提交OJ前必查:从WA到AC的常见坑

4.1 输出格式:空格和换行是PE重灾区

OJ题和普通练习最大的区别是,它比对的是标准答案,不是人眼。很多人本地输出看上去和样例一模一样,一提交却是Presentation Error,问题基本都出在空格和换行上。题目如果要求数字之间用空格隔开,那你最好不要在行尾多打一个空格,也不要每行前面多余几个空格。

我习惯用这样的输出写法:

for (int j = 0; j < n; j++) { if (j != 0) cout << ' '; cout << a[i][j]; } cout << '\n';

这段代码里,第一个数字前面不打空格,从第二个数字开始,每个数字前补一个空格。这样能保证行尾干干净净,不会出现末尾空格干扰判题的情况。换行的话,用'\n'endl在OJ结果上没有区别,但'\n'更快,刷题时更推荐。

4.2 最容易踩的数组初始化坑

方向数组方案依赖a[nx][ny] != 0来判断格子是否被填过。如果数组没有清零,那么里面存的可能是随机值,比如某个没有访问过的格子已经是个非零垃圾值,程序走到那里就以为走不通,提前转向,最后矩阵形状就错了。

全局变量数组int a[30][30];是会默认清零的,这没问题。可一旦你把数组写进main函数内部,比如int a[30][30];放在main里,它就不会清零了。这时候务必补上初始化:int a[30][30] = {};。如果题目是多组输入,每组数据之间还需要用memset(a, 0, sizeof(a));重新清理一次,否则上一组数据残留的非零数字会影响下一组判断。

4.3 本地对但OJ错,排查顺序从这四步走

如果你遇到本地运行结果很正常、提交却出问题的情况,我建议按下面的顺序排查,别一上来就怀疑是编译器差异。

第一步检查数组初始化,尤其是局部数组没有清零这种低级但高频的问题。第二步检查数组边界,看看题目数据范围,n最大是多少,数组有没有开够,一般开a[30][30]a[105][105]。第三步检查输出格式,行尾空格和换行最隐蔽。第四步检查是否有额外的提示输出,比如cout打印了“答案是”之类的内容,这也会被判错。

为了帮你快速对照,我把典型的WA原因整理成了一个速查表:

现象可能原因解决方法
程序不输出或超时死循环,方向一直没转检查转向条件是否覆盖了越界和已填格子
输出矩阵中出现0循环少填了格子检查循环结束条件,是否写成了num < n*n
数字顺序乱掉某个格子被重复填充检查每条边或方向数组转向逻辑
本地AC一提交WA局部数组未初始化或格式问题使用= {}初始化,检查行尾空格
输出结果错位数组开小了按题目上限增大数组容量

4.4 多准备几组边界测试,别只盯着样例

东华OJ这类平台的样例通常只给一两个常规输入,比如n=3。如果你只拿n=3测试,很容易漏掉边界情况。我个人的习惯是提交前至少要本地跑n=1、n=2、n=4、n=5这四组数据。

n=1时,输出就是一个1,大多数代码不会有问题,但能帮你发现循环条件是否会把数组下标算到-1。n=2时,只有2×2四个格子,最外圈也是最后一圈,特别容易测出“下边”和“左边”重复填充的问题。n=5时,矩阵中心独立存在,能测出奇数n时中心格是否被正确填入。这几组都跑对了,再去提交,一次AC的概率会高很多。

4.5 调试小技巧:打印每一步的坐标和方向

如果程序逻辑有问题,光看最终输出很难定位。我调试这类题时,会在填数的循环里临时加一行打印,看看每个数字填到了哪个坐标:

cout << "num=" << num << " x=" << x << " y=" << y << " d=" << d << endl;

这样能直接观察填充轨迹。比如你发现某个数字填到了预期之外的格子,或者方向d没有在应该转弯的位置变化,那问题就很明确了。调试完记得把打印注释掉,不然OJ会判你有多余输出。用VS Code调试C++时,也可以直接在循环里打上断点,监视x、y、d这几个变量的变化,效果一样,还不用反复改代码。

5. 从螺旋方阵说开去:一道基础题能带来的C++训练价值

5.1 螺旋遍历是很多矩阵题目的隐藏起点

螺旋方阵不是东华OJ独有的题目,LeetCode上同样有类似的原型,比如54题“螺旋矩阵”和59题“螺旋矩阵II”。两者的区别是LeetCode版本不一定要求你输入n输出方阵,而是给你一个现成的矩阵让你按螺旋顺序遍历,思路和这题完全相通。

如果你把方向数组那套代码理解透了,以后去写旋转矩阵、蛇形矩阵、矩阵按层遍历,就会发现它们的核心逻辑都是“方向控制加边界判断”。这些东西在同一天的OJ练习里可能显得抽象,但放到后续算法学习中,就是你理解二维数组遍历和状态切换的一块重要跳板。

5.2 建议的进阶练习顺序

AC这道题后,不要急着把代码扔一边,可以做几个小小的变体练习来检验自己是不是真的掌握了。

第一个变体是改成从左上角开始逆时针填充,你只需要调整方向数组的顺序,比如按右、上、左、下,还是按上、右、下、左?自己动手试一次,能帮助理解方向数组的排列和实际行走方向之间的对应关系。

第二个变体是改成输入两个数m和n,输出一个m行n列的螺旋矩阵。这时候方阵变成矩形,要求你对行数和列数分别做边界判断,比正方形多一道坑。第三个变体是尝试只用一重循环,不用方向数组,写出另一种模拟解法。这样对比之后,你对几种方案的优劣会有更直观的体会。

5.3 我个人的实操体会

我在实际练习中,每次做这种二维数组模拟题都会先干一件事:在代码注释里写下三行文字,当前坐标、当前方向、结束条件。别小看这个习惯,很多写着写着就乱的人,都是因为心里只有一个模糊的画面,没有把状态拆出来。把这三行状态写清楚,代码就有了骨架,后面填肉不会偏。

另外,不管你现在用的是Dev C++、VS Code还是在线OJ编辑器,都要在本地留一套能直接跑起来的C++环境。VS Code配好C/C++插件后,调试这种二维数组题非常顺手,打断点加监视能直接看到数组内容,比打印调试直观得多。环境准备好了,刷题效率会明显不一样。

回到螺旋方阵本身,我不建议只背代码。你可以今天用逐层剥洋葱的思路写一遍,明天再用方向数组的思路默写一遍,后天再尝试逆时针改法,每一次都在逼自己理解得更深入一点。等你能闭着眼在白纸上画出n=5的螺旋填充路径,再把代码流畅写出来,这道题才算真正吃透了。那种“二维数组也不过如此”的感觉,就是这么一格一格填出来的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询