news 2026/10/20 3:55:08

C语言实现顺时针打印矩阵:四边界法与边界控制详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言实现顺时针打印矩阵:四边界法与边界控制详解

菜鸟到大厂面试,最怕碰到的就是这类“看着简单,一写就废”的题。顺时针打印矩阵,剑指 Offer 29,C语言版本,几乎每家公司在考察数组和边界控制时都会拿它当试金石。题目本身抽象,一句话概括就是:给你一个 m 行 n 列的矩阵,按顺时针顺序从外到内把每个元素输出一遍。但真上手写,你会发现最难的不是遍历本身,而是怎么控制四个方向的转向时机,以及矩阵不是正方形时那些奇奇怪怪的退化情况。

这篇文章我从零开始拆解这道题:先讲清楚核心思路,再给你一份可以直接运行的 C 代码,最后把我踩过的边界坑全部列出来。无论你是第一次刷题的新手,还是想快速捡起 C 语言实现的老手,照着下面的思路走,十分钟内能搞出一版稳定的解法。

1. 题目分析与核心解题思路

1.1 题目到底在问什么

输入是一个二维数组,比如:

1 2 3 4 5 6 7 8 9

输出应该是1 2 3 6 9 8 7 4 5。注意方向变化:先向右走到底,再向下走到底,再向左走到底,再向上走到底,然后收缩范围,继续下一个内圈。整个过程像是一圈一圈剥洋葱,每一圈走四条边,直到所有元素都被访问。

关键点在于:矩阵不一定是正方形,可能是3x4,可能是1x5,也可能是一列。如果直接用“按层模拟”的写法,必须处理好只剩一行或只剩一列时的重复输出问题。

C 语言里没有现成的二维数组边界判断,所有越界检查都要自己写,这也是这题在 C 语言下的难度所在。C 语言考察的是你对指针、下标、循环条件的掌控力,而不是像 Python 里可以直接用pop作弊。

1.2 两种常见解法的取舍

这道题主流解法有两种:模拟螺旋路径和维护四个边界。

模拟螺旋路径的思路是:从(0,0)出发,用dir表示当前方向,走到下一个位置如果越界或者已经访问过,就右转 90 度。这个思路直观,但必须额外申请一个m x n的二维 visited 数组,空间复杂度 O(mn)。对 C 语言来说,如果矩阵特别大,动态申请 visited 本身就是个麻烦事,而且容易在释放内存时出幺蛾子。

四个边界法就优雅得多。定义top, bottom, left, right四个变量,初始分别指向矩阵的上边界、下边界、左边界、右边界。然后循环执行四步:

  1. 从左到右遍历当前 top 行,之后 top++;
  2. 从上到下遍历当前 right 列,之后 right--;
  3. 从右到左遍历当前 bottom 行,之后 bottom--;
  4. 从下到上遍历当前 left 列,之后 left++。

每次循环都检查top <= bottom && left <= right,循环退出条件就是边界交错。

我推荐用四边界法,因为空间复杂度 O(1),而且边界条件更可控,不会漏元素也不会重复输出。面试时写这版,面试官很难挑出毛病。

1.3 时间复杂度与空间复杂度先说清楚

时间复杂度 O(mn),因为每个元素恰好访问一次,没有额外重复遍历。空间复杂度 O(1),只用了四个整型变量,没有申请额外数组。这个复杂度是这题的“标准答案”,面试时主动说出来,属于加分项。

2. 四边界模拟法的关键细节

2.1 四个方向访问的边界控制

把矩阵看作一个平面直角坐标系,行是row,列是col。四个方向分别是:

方向行变化列变化终止条件
向右row 不变col++col == right
向下row++col 不变row == bottom
向左row 不变col--col == left
向上row--col 不变row == top

每个方向走完后,对应边界收窄。这里特别容易出错的是第三、第四步:如果这一圈已经只剩一行,第三步向左遍历会再次访问这一行,如果只剩一列,第四步向上遍历会再次访问这一列。所以要加上额外的“防重复”判断。

具体判断逻辑:

  • 在做第3步(从右到左遍历 bottom 行)之前,要先确认top <= bottom。因为第1步结束后top已经加过了,如果此时top > bottom,说明这一圈只有一行,第1步已经把所有元素输出完了,第3步不需要执行。
  • 在做第4步(从下到上遍历 left 列)之前,要先确认left <= right。因为第2步结束后right已经减过了,如果此时left > right,说明这一圈只有一列,第4步不需要执行。

这个细节是无数人踩坑的地方。我见过太多人写完代码,测试3x3没问题,一测3x4或者1x5就输出重复元素,问题就出在这个防重复判断上。

2.2 循环条件的两种写法对比

你可以用while (top <= bottom && left <= right)作为外层循环,也可以用while (count < total)计数来控制。两者各有优劣。

边界法常用边界判断:

while (top <= bottom && left <= right) { // 四步遍历 }

这个写法干净利落,循环次数正好是“圈数”。但要注意,如果矩阵是0x0(理论上可能),初始条件就不满足,直接跳过循环,返回空结果,这样也正确。

计数法常用count统计已输出元素数量,当count == total时退出。这种方法更直观,想漏一个都难,但每次循环都要判断count,逻辑上略显冗余。

我个人推荐边界法,因为面试时更好解释,而且代码更精简。核心是要把四个边界的更新顺序想明白。

2.3 一维矩阵的特殊处理

如果m == 1或n == 1,其实就是一条线。四边界法依然适用,只是第三步和第四步会被防重复判断直接跳过。例如1x5的矩阵{1,2,3,4,5},初始top=0, bottom=0, left=0, right=4,第一步从左到右输出全部,top变 1,此时top > bottom,外层循环结束。整个过程只输出了 5 个元素,没有重复。

如果是单列5x1的矩阵,第一步输出matrix[0][0],top变 1,第二步从上到下输出matrix[1][0]到matrix[4][0],right变 0,此时left > right,外层循环结束。没毛病。

3. C 语言完整实现与逐段解读

3.1 函数签名设计

在 C 语言环境中,我们通常把输出逻辑封装成一个函数。返回值用void,直接打印到标准输出。如果要返回数组,需要额外用指针参数传递,但这道题核心是考察思路,打印即可。

#include <stdio.h> void spiralOrder(int matrix[][100], int m, int n) { // 实参传递时,第二维需要指定大小 }

不过,matrix[][100]这种写法限制了列数必须固定为 100,不够通用。更灵活的做法是使用动态二维数组,或者直接用int**指针。但 C 语言笔试常见格式是给定matrix数组以及matrixSize,用变长数组(VLA)的特性也可以。

这里为了方便演示,我用int matrix[][4]这类固定列数的方式测试。实际面试时,建议这样处理:

void spiralOrder(int** matrix, int matrixSize, int* matrixColSize) { // matrixSize 表示行数 m,matrixColSize 指向每行列数 n }

这个签名符合力扣风格的 C 接口。下面我用这种接口写完整代码。

3.2 完整代码与注释

#include <stdio.h> #include <stdlib.h> void spiralOrder(int** matrix, int matrixSize, int* matrixColSize) { if (matrixSize == 0 || matrixColSize[0] == 0) { return; } int top = 0; int bottom = matrixSize - 1; int left = 0; int right = matrixColSize[0] - 1; while (top <= bottom && left <= right) { // 第一步:左到右遍历 top 行 for (int j = left; j <= right; j++) { printf("%d ", matrix[top][j]); } top++; // 第二步:上到下遍历 right 列 for (int i = top; i <= bottom; i++) { printf("%d ", matrix[i][right]); } right--; // 第三步:右到左遍历 bottom 行(防重复判断) if (top <= bottom) { for (int j = right; j >= left; j--) { printf("%d ", matrix[bottom][j]); } bottom--; } // 第四步:下到上遍历 left 列(防重复判断) if (left <= right) { for (int i = bottom; i >= top; i--) { printf("%d ", matrix[i][left]); } left++; } } } int main() { int row1[] = {1, 2, 3, 4}; int row2[] = {5, 6, 7, 8}; int row3[] = {9, 10, 11, 12}; int* matrix[] = {row1, row2, row3}; int matrixSize = 3; int matrixColSize[] = {4, 4, 4}; spiralOrder(matrix, matrixSize, matrixColSize); return 0; }

输出结果:

1 2 3 4 8 12 11 10 9 5 6 7

这是 3 行 4 列的矩阵,输出顺序完全正确。

3.3 逐段代码逻辑拆解

第一步遍历top行时,行下标固定为top,列从left到right递增。完成后,上边界向下一行移动,即top++,这样下一圈就不会再访问这一行。

第二步遍历right列时,列下标固定为right,行从top到bottom递增。注意这里的top已经是更新后的新值,也就是说第一步访问过的元素不会被重复访问。完成后right--。

第三步遍历bottom行时,行下标固定为bottom,列从right到left递减。这里的right已经是更新后的值,所以最右侧那列不会重复。但行bottom在第二步中已经被访问过最右下角那个元素了吗?仔细看:第二步是按行从top到bottom遍历right列,确实访问了matrix[bottom][right]。第三步这里我们从right-1开始递减,因为right已经减过 1,所以起点是matrix[bottom][right-1],没有重复。但前提是top <= bottom,如果top已经在第一步后超过bottom,说明矩阵只有一行,第一步已经搞定,第三步就没必要执行了。

第四步同理,left列从bottom到top递减,此时left还是旧值,因为还没更新。需要判断left <= right,防止单列重复。

这四个步骤的顺序不能变。如果你先做第四步再做第三步,整个输出顺序就乱了。

3.4 测试不同形状的矩阵

我专门测试了几种常见边界情况,大家可以对照验证自己写的代码。

3 行 3 列:

1 2 3 4 5 6 7 8 9

输出应为1 2 3 6 9 8 7 4 5。我的代码运行结果一致。

1 行 4 列:

1 2 3 4

初始top=0, bottom=0, left=0, right=3,第一步输出 1 2 3 4,top变 1,外层循环结束。不会执行第二三四步。输出1 2 3 4,正确。

4 行 1 列:

1 2 3 4

初始top=0, bottom=3, left=0, right=0,第一步输出 1,top=1;第二步输出 2 3 4(从 i=1 到 3),right=-1;第三步判断top<=bottom为真(1<=3),进入循环for (int j=right; j>=left; j--),此时right=-1, left=0,循环条件j>=left即-1>=0为假,直接跳过,但注意bottom--是在循环外,所以 bottom 从 3 变 2;第四步判断left <= right,0 <= -1为假,跳过。外层循环继续,此时top=1, bottom=2, left=0, right=-1,条件top<=bottom && left<=right因为left<=right为假而结束。没有重复输出。正确。

空数组:

matrixSize = 0;

直接 return,不输出任何内容。正确。

4. 常见 Bug 与调试心得

4.1 经典的“多输出一个元素”问题

如果你把第三步的防重复判断写成if (top <= bottom)没错,但把第四步的防重复判断漏了,对于 3 行 3 列矩阵不会出错,因为第三步和第四步都满足条件。但如果矩阵是 3 行 2 列:

1 2 3 4 5 6

正确输出是1 2 4 6 5 3。流程如下:第一步输出 1 2,top=1;第二步输出 4 6,right=0;第三步 top<=bottom(1<=2),输出 5,bottom=1;第四步 left<=right(0<=0),输出 3,left=1。外层循环条件top<=bottom && left<=right变成1<=1 && 1<=0,不满足,退出。共输出 6 个元素,正确。

如果漏了第四步的判断,第一步输出 1 2,第二步输出 4 6,第三步输出 5,第四步无条件执行,从 bottom=1 向上到 top=1 只输出一次 3,也不会重复,因为此时 bottom 已经是 1,只有一个元素。所以单看这个例子不一定会漏。但如果是 1 行 5 列,漏了第三步判断,就会重复输出整行。

调试这类问题最好的方法不是靠肉眼,而是用一个小的矩阵(比如 3x3、2x3、3x2)走一遍,用笔在纸上画出每个边界的变化。我刷题这么多年,遇到边界类题目永远先画表,不要高估自己的脑内 debug 能力。

4.2 变长数组与指针传递的坑

在 C 语言中,matrix[i][j]这种写法要求编译器知道第二维的大小。如果你拿到的是int** matrix,它本身是不带行列信息的,必须配合matrixSize和matrixColSize使用。最常见的错误是:

int matrix[3][4] = {...}; int** p = (int**)matrix; // 错误!

这种强制转换会导致后面访问p[i][j]时地址计算完全错误,因为二维数组在内存中是连续排布的,而int**期望的是“指针数组”。正确的传递方式应该直接传int matrix[][4],或者在调用时用数组指针int (*matrix)[col]。

如果接口限定为int**,那调用方必须真正构造一个指针数组,每个指针指向一行。我在上面的代码里就是这么干的:用row1, row2, row3三个数组,然后int* matrix[] = {row1, row2, row3}。这样matrix[i]就是第 i 行的地址,matrix[i][j]才能正确访问。

4.3 打印格式与字节流问题

这道题在力扣中要求返回 int 数组,但在 C 语言本地测试时,大家习惯直接打印。打印时要注意最后一个元素后面不要带多余的空格。如果你用printf("%d ", matrix[top][j]),最后会多一个空格,判题系统一般忽略空白,但有些平台会严格比较字符串,那样会判错。

更严谨的写法是判断是否已输出第一个元素,或者先输出一个标记。当然,如果只是刷题,多一个空格没什么大问题,但如果是输出到文件、做精确比对,就要处理一下。

我个人在本地测试时喜欢这样:

int first = 1; // 每个 printf 前判断 if (first) { printf("%d", val); first = 0; } else { printf(" %d", val); }

这样输出整洁,也更符合实际项目的需求。

4.4 性能分析与优化空间

这个算法已经是 O(mn) 时间、O(1) 空间,理论上没有更优解,因为每个元素至少要访问一次。但你可以做一些微小的优化,比如把matrixColSize[0]存到局部变量n里,减少多次间接寻址。编译器一般也会帮你做这件事,但显式写出来代码更清晰。

如果矩阵是稀疏的、不需要全部输出,那就另当别论。但就这道题而言,没有优化空间,面试时不用画蛇添足。

一个可以提的优化点是:把打印改成回调函数或者通过输出数组返回,这样函数可以被复用到不同场景,比如你后续要做图像处理中的像素螺旋采样。把“输出”这个动作抽象出去是代码设计层面的加分项。

5. 从这道题延伸出的面试考点

5.1 逆时针打印与贪吃蛇遍历

面试官常会在你答完顺时针后追问:“如果改成逆时针呢?”其实只要把四步方向反转即可:先从上到下,再从左到右,再从下到上,再从右到左,同时边界更新逻辑不变。核心思想还是控制四个边界,换方向只是换遍历顺序。会了顺时针,逆时针只是三分钟的改动。

另外,有些变体会要求你从任意起点开始螺旋遍历,或者给定步长、跳跃式访问。处理手段都是维护方向数组,配合越界判断。面试时如果能引申出这个方向数组方案,会显得你举一反三的能力很强。

5.2 二维矩阵的原地旋转

剑指 Offer 里还有一道矩阵旋转的题,和螺旋打印有相似之处:都需要精确定位行列变换关系。顺时针螺旋打印本质上是在二维坐标上做线性遍历,遍历顺序是(0,0) -> (0,n-1) -> (m-1,n-1) -> (m-1,0) -> (1,0) ...。它和一维数组下标转换有关,但比旋转简单,因为不需要交换元素。

面试官问螺旋打印,大概率会接着问旋转矩阵或之字形打印。建议你花半天时间把这三道题一起刷掉,效果事半功倍。

5.3 边界条件在真实开发中的意义

你可能觉得这种题只在面试中出现,实际工程里用不到。其实图形学里扫描线填充、地图切片处理、神经网络的卷积核滑动,都要用到类似的边界控制。比如图像处理中常见的“环形滤波”,就是按照螺旋顺序遍历像素邻域。能把这道题的边界想清楚,写那些代码时会少很多年轻时的幼稚错误。

我就是当年因为没处理边界条件,导致某个图像处理模块在边缘像素上出现越界访问,最后查了一天才发现是螺旋遍历少了一条判断。从那以后,凡是写遍历逻辑,先画图、列边界条件,再写循环体。

6. 最后的实操建议

这里给大家一个“考场三步法”:

第一步,拿到题先写四个边界变量,不要急着写循环。先在草稿纸上画一个3x4的矩阵,标出top/bottom/left/right,模拟走一圈,把每一步输出的元素列出来。

第二步,确定外层循环条件。while (top <= bottom && left <= right)作为模板,背下来即可。

第三步,写四个for循环,注意每一步对应的边界变量是旧值还是新值。写完检查第三步和第四步的防重复判断是否齐全。

我当初刷这道题刷了至少五遍,每一遍都能发现新的理解死角。第一遍漏了边界,第二遍漏了指针传递,第三遍搞混了左闭右开区间,第四遍终于代码跑通了,但一测1xN就重复。第五遍才算彻底掌握。所以如果你第一次没写对,太正常了。关键是记住这么一句话:螺旋矩阵的核心不是四个循环,而是两个 if。

今天这篇文章以 C 语言为例,把四边界法的代码逐段拆开,希望你能动手在本地编译器里跑一遍,试着把矩阵改成5x1、1x5、5x3各种形状,看看输出是否符合预期。跑了这几个用例之后,你会彻底理解防重复判断的意义。这道题不吃天赋,吃细心。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 3:54:43

让AI读懂团队踩过的坑:隐性知识文档化与RAG实践

1. 这事是怎么发生的&#xff1a;我盯着满屏"已读"文件&#xff0c;突然意识到不对劲前阵子我接手了一个知识库整理的项目&#xff0c;手头有大概六百多份文档——产品手册、技术方案、会议纪要、客户反馈、历史邮件截图&#xff0c;什么都有。团队的想法很直接&…

作者头像 李华
网站建设 2026/10/20 3:53:45

基于Python+Django的黄瓜批发市场管理系统开发实战

你还在纠结毕业设计选什么题目&#xff1f;作为一个从学生时代走过来、又带过不少实习生的人&#xff0c;我太清楚这套“Python Django 管理系统”组合意味着什么了。今天要聊的这个“基于PythonDjango的黄瓜批发市场管理系统”&#xff0c;说白了就是典型的管理信息系统题目…

作者头像 李华
网站建设 2026/10/20 3:53:30

Qt自绘滑动选择器:惯性滑动与刻度对齐的控件实现

简介&#xff1a;这是一份基于Qt框架实现的自定义滑动选择器工程源码&#xff0c;适合有一定Qt基础、希望系统学习自定义控件绘制与交互的开发者。控件支持水平与垂直两种模式切换&#xff0c;并在背景、滑块、边框配色以及值域、初始值上提供灵活配置&#xff0c;可嵌入音量调…

作者头像 李华
网站建设 2026/10/20 3:53:30

真实项目复盘:从‘rea‘代号到实时错误分析平台的架构实践

接项目单子的时候最怕碰见这种命名&#xff1a;三个字母&#xff0c;无注释&#xff0c;无文档&#xff0c;连邮箱前缀都是它。我之前接到过一个小项目&#xff0c;代号就叫“rea”&#xff0c;需求方给的说明只有一句话&#xff1a;“让业务方实时看到系统异常。”前后端一共四…

作者头像 李华
网站建设 2026/10/20 3:52:56

C#连接S7-1200 Modbus TCP:固件兼容性与高可靠通讯实战

简介&#xff1a;本资源是一套面向工业自动化开发者的C#与西门子S7-1200 PLC基于ModbusTCP协议通信的完整实践工程&#xff0c;适用于具备基础C#编程与PLC概念的中级开发者&#xff0c;解决工业现场上位机与PLC数据交互的核心需求。压缩包共46个文件&#xff0c;含13个核心C#源…

作者头像 李华
网站建设 2026/10/20 3:52:52

深入Android FFmpeg播放器:解码链路与JNI实现解析

简介&#xff1a;一个基于FFmpeg的安卓视频播放器工程源码&#xff0c;面向想在安卓端深入掌握NDK多媒体开发的C、C和Java工程师。项目完整演示了FFmpeg交叉编译、动态库集成、JNI接口设计以及播放器核心流程&#xff1a;先配置NDK环境并编写构建脚本&#xff0c;将编译好的so库…

作者头像 李华