ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C语言数组逆序存放:从基础到指针,一次讲清边界与格式问题

C语言数组逆序存放:从基础到指针,一次讲清边界与格式问题 数组逆序存放这道题几乎每个学C语言的人都会遇到。PTA上标着20分看起来简单但它恰好踩中了C语言里好几个关键知识点数组下标、内存连续存储、边界条件、输入输出格式。我带过的学生里不少人在这个题上栽过跟头有的把“逆序存放”写成了“逆序输出”有的交换逻辑绕了半天有的在OJ的格式要求上反复吃罚时。这篇文章就把这个实验题彻底拆开从题目的真实意图、基础解法、指针视角、扩展场景到常见报错一次讲清楚。不管你是刚学数组的新手还是准备面试想复习基础这篇都值得看完。1. 题目拆解与核心需求——先搞清楚这道题到底在考什么1.1 原题长什么样这道题的描述通常是这样的本题要求编写程序将给定的n个整数存入数组后再把这n个数逆序存放最后在一行中输出。输入格式是第一行给出一个正整数n1≤n≤10第二行给出n个整数用空格分隔。输出格式是一行中输出逆序存放后的n个整数每个整数后面跟一个空格或者行末不得有多余空格具体看OJ版本。我见过很多同学拿到题就开始写代码结果连题都没读完。这里需要先明确一件事题目要求的是“存入数组后逆序存放”这意味着你要先老老实实把数据读进一个数组再对数组本身做操作最后输出数组内容。不是让你边读边倒着存也不是让你读完之后倒着打印一遍就完事。有些题确实可以直接倒着输出得分但“逆序存放”这道题在多数OJ上不会这么宽松。它会检查数组被修改后的实际内容或者后续操作依赖这个逆序后的数组。所以第一步就是把题目意图看清楚它要的是“数组里的数真的被倒过来了”。1.2 “逆序存放”和“逆序输出”是两码事从字面上看“逆序存放”是对数组内容做修改“逆序输出”只是调整打印顺序。前者改变了数据本身后者只改变了显示方式。放在实际开发里这个区别很关键。我举个例子你从数据库里查出一批记录需要在界面上倒序展示这时候只改显示顺序就够了数据源不用动。但如果后续所有逻辑都基于倒序数据来计算那你就要真的把数组倒过来否则每一处逻辑都得小心翼翼地去“倒着遍历”。很多初学者会想我直接 for (i n - 1; i 0; i--) printf(%d , a[i]); 不就完了吗输出结果确实是从后往前了但数组a里的内容根本没变。如果题目要求“逆序存放后输出”这种写法严格来说是错的。你在自己电脑上跑可能看不出区别因为输出一样但在OJ上如果判题逻辑检查的是数组状态或者你后续还有别的操作就会暴露问题。1.3 20分的实验题踩分点在哪PTA这类OJ的20分题一般不会只给你一个“对或错”的满分判定而是根据测试点分布来踩分。基础功能占大头比如能正确读入、正确交换、正确输出大概能拿一半以上的分数。剩下的分数往往卡在边界情况和格式上n等于1时怎么办n是偶数怎么办行末是否有空格是否换行。我批改过很多次类似的实验作业看到最多的丢分原因不是算法不会而是格式问题。行末多了一个空格OJ直接判格式错误扣掉好几分。所以这道题的本质不光是考你“会不会反转数组”还在考你有没有工程思维——对边界条件敏感对输出格式严格对每一个循环变量的取值都心里有数。2. 对称交换法把逆序操作落到代码上2.1 最直觉的思路两头交换向中间收拢如果把数组看成一行队伍逆序存放就是让队头和队尾换位置第二个和倒数第二个换位置一直换到中间。这个方法叫对称交换也叫双指针交换。它的核心逻辑是a[0]和a[n-1]交换a[1]和a[n-2]交换一直到两端相遇为止。用代码写出来就是三层结构循环、交换、边界判断。交换两个变量的值需要一个临时变量temp这是C语言里的基本功也是很多新手第一次接触“中间变量”概念的场景。int temp; temp a[i]; a[i] a[n - 1 - i]; a[n - 1 - i] temp;每次交换涉及三次赋值操作这是交换两个变量的通用写法。为什么非要一个中间变量因为直接 a[i] a[n-1-i]; a[n-1-i] a[i]; 的话第一步执行完a[i]原来的值就丢了第二步拿到的已经是覆盖后的值。这个道理我反复给学生强调看起来简单但很多人写代码的时候脑子一热就忘了。2.2 循环边界为什么是 n/2奇偶怎么处理这是这道题最容易出bug的地方。循环要执行多少次答案是n/2次整数除法下取整。我见过不少人写成 i n/2或者 i n结果程序跑出来要么数组没变化要么越界。我们来手动模拟一下。n 4时数组下标是0、1、2、3。i0交换a[0]和a[3]i1交换a[1]和a[2]到i2时a[2]和a[1]又是一次交换等于把刚才的交换又换回去了。所以循环条件必须是 i n/2也就是i只能取0和1。n 5时下标是0、1、2、3、4。i0交换a[0]和a[4]i1交换a[1]和a[3]i2时a[2]和a[2]自己换自己没有意义但也不出错。所以 i n/2 时i只能取0和1i2不会执行正好避开这个无意义操作。如果你写的是 i n/2偶数n4时就会多交换一次数组等于没变奇数n5时多交换一次a[2]自己影响不大。也就是说这是一个在奇数情况下可能“侥幸通过”偶数情况下必错的隐藏bug。2.3 完整代码与复杂度分析把输入、逆序、输出串起来完整代码如下#include stdio.h int main() { int n; int a[10]; scanf(%d, n); for (int i 0; i n; i) { scanf(%d, a[i]); } for (int i 0; i n / 2; i) { int temp a[i]; a[i] a[n - 1 - i]; a[n - 1 - i] temp; } for (int i 0; i n; i) { if (i 0) printf( ); printf(%d, a[i]); } printf(\n); return 0; }时间复杂度是O(n)因为每个元素最多被访问两次一次交换一次输出空间复杂度是O(1)只用了常数个额外变量。这个复杂度已经是最优的了因为你至少要把每个元素读一遍、写一遍。即便n最大只有10但分析复杂度的思维要养成以后遇到大数据量题目才不慌。3. 指针视角与内存模型顺带把数组本质看透3.1 数组在内存里是一段连续的空间下标就是偏移量这道题如果用指针来写能帮助你把数组的本质看得更透。很多教材在讲数组时说“数组名是首元素地址”这句话新手往往理解不深。实际上C语言里数组在内存中占据的是一段连续的空间a[i]的意思是“以a为首地址偏移i个元素单位后取出那个位置的值”。这个“偏移”非常关键。int在大多数平台上占4个字节所以a[3]取的是首地址往右数12个字节处的数据。指针变量就是用来存储这种地址的int *p a; 让p指向数组首元素那么p1就指向第二个元素p2指向第三个。这不是简单的地址1而是根据类型大小来移动的指针算术正是C语言高效访问内存的底气。3.2 双指针版本left 和 right 的移动用两个指针分别指向数组头和尾同时往中间走交换它们指向的元素。代码长这样#include stdio.h int main() { int n; int a[10]; scanf(%d, n); for (int i 0; i n; i) { scanf(%d, a[i]); } int *left a; int *right a n - 1; while (left right) { int temp *left; *left *right; *right temp; left; right--; } for (int i 0; i n; i) { if (i 0) printf( ); printf(%d, a[i]); } printf(\n); return 0; }这里有个细节right的初始值是a n - 1而不是a n。因为数组有n个元素下标从0开始最后一个元素是a[n-1]所以指针要偏移n-1个单位。写错成an的话指针指向的是数组末尾之后的那块内存访问它属于未定义行为运气好没崩运气不好直接段错误。while (left right) 这个条件比下标的 i n/2 更直观因为它天然处理了奇偶情况。n为偶数时两个指针在中间“擦肩而过”前循环结束n为奇数时两个指针在中间元素处相遇left right循环停止。你不用去记边界条件只需要理解“没有相遇就继续换”这个逻辑。3.3 数组定义方式的取舍宏定义、const、VLA、固定大小题目给的n不超过10所以直接int a[10]就够了。但实际写代码时很多人会在数组定义大小上纠结用宏定义、用const常量、还是直接写数字我推荐用宏定义也就是在代码开头写 #define MAXN 10然后 int a[MAXN]。这样做的理由很朴素代码的可读性和可维护性。将来题目范围变了比如n不超过1000你只需要改一行宏定义而不是满世界找“10”这个魔法数字。#define MAXN 10也可以用const int MAXN 10。注意C语言里const修饰的变量本质上是“只读变量”不是真正的编译期常量用来定义数组大小时在C89标准下可能不被支持但在C99和更严格的C里通常没问题。刷OJ的话宏定义是兼容性最好的选择。还有一种写法是int a[n]其中n是运行时从scanf读进来的这叫变长数组VLA由C99标准引入。在gcc编译环境下可以运行但C标准不支持部分OJ的编译器也可能报错。稳妥起见刷题和做实验时优先用固定大小数组不要为了省那几行代码去挑战编译器的脾气。4. 从一维到多维逆序操作的扩展与变体4.1 先想明白一个问题数组内容变了吗做这道题的时候我总会问学生一个问题交换前后数组的地址变了吗答案是没变。a还是那个aa[0]的地址还是原来那个地址变的只是每个地址里存放的值。这个概念听起来简单但很多人学到后面会把“数组”和“指向数组的指针”搞混。数组名做函数参数时会退化成指针这也是很多人在写reverse函数时容易困惑的点。比如你写一个函数void reverse(int arr[], int n)函数内部对arr[i]的修改会直接影响main函数里的原数组。原理就是数组参数传递的是地址不是副本。这跟普通int类型参数按值传递完全不同。一维数组、二维数组、指针数组本质上都是“连续内存上的数据布局”逆序操作的核心永远是“对称位置交换”。4.2 二维数组逆序行内逆序与行序逆序二维数组比一维数组多一个维度逆序的做法也分几种。最常见的两种一是把每一行内部的元素逆序比如int b[3][4]交换b[i][j]和b[i][3-j]二是把整个数组的行顺序反过来第一行变最后一行第二行变倒数第二行。行内逆序的代码和一维数组几乎一模一样只不过把a[i]换成了b[i][j]把n换成了列数4。行序逆序稍微麻烦一点因为你交换的是整行数据如果列数不大可以直接用临时数组逐元素交换或者用memcpy批量拷贝。这个题目虽然只要求一维数组但理解二维的逆序能帮你把“对称交换”的思路真正内化而不是只会套模板刷题。字符串数组也是类似的场景。char str[] hello; 逆序后变成 olleh但要注意字符串结尾的\0不能跟着交换。如果你用strlen算出长度len那么str[len-1]是最后一个有效字符str[len]才是\0。交换范围只能是0到len-1碰了\0字符串就废了。这个坑我在实际项目里也踩过处理二进制数据时没有注意结束符导致后面一片乱码。4.3 指针数组的逆序换的是指向不是内容指针数组在热词里也出现得很频繁。所谓指针数组就是数组里每个元素存放的是一个指针比如char *fruits[] {apple, banana, cherry};。这里fruits[0]指向字符串apple的首字符fruits[1]指向banana的首字符。对指针数组做逆序交换的是“指针”本身字符串在内存中的位置不会变。逆序之后fruits[0]会指向cherryfruits[2]会指向apple。这种操作在需要对一批字符串按某种规则倒排时很有用因为你不用去移动字符串内容只移动指针开销小得多。这个思路在工程上叫“改变引用不改变数据”和“逆序存放”相比算是同类操作的进阶版本。5. 实际调试与常见报错排查实录5.1 输出格式问题行末空格和换行这是新手在OJ上遇到最多的报错之一不是“答案错误”而是“格式错误”。输出要求通常是在一行中输出逆序后的n个整数每个整数后面跟一个空格或者行末不得有多余空格不同OJ规矩不一样。你可以采用“先输出第一个数之后每个数前面加空格”的策略这样任何情况下都不会在行末留下多余空格。for (int i 0; i n; i) { if (i 0) printf( ); printf(%d, a[i]); } printf(\n);我见过不少同学用for循环输出每个数后都加个空格然后最后再单独处理最后一个。这样也不是不行但多一步判断代码就多一分出bug的可能。用“前置空格法”是最省心的。5.2 段错误scanf漏了取地址符或数组越界段错误Segmentation fault是C语言新手最害怕的报错一出现就是程序直接崩溃。在这个题里最常见的段错误原因有两个一个是scanf里漏写了取地址符。scanf(%d, a[i]); 少了这等于把一个整数当成了指针传给scanf程序去访问那个“假地址”必崩。正确写法是scanf(%d, a[i]);另一个原因是数组越界。题目说n最大是10但你定义int a[10]下标范围是0到9如果你读入或访问a[10]就越界了。这种越界有时候不会立刻崩溃因为那块内存可能碰巧可读但属于未定义行为运气差一点就段错误。解决方法是养成习惯循环条件里始终写 i n而不是 i n。5.3 交换逻辑问题边界写错、temp类型不匹配交换后数组没变大概率是循环边界写成了i n/2这在上面已经详细说过。另外还有一种情况temp类型和数组元素类型不一致。比如数组是double型你写int temp a[i]小数部分会被截断再赋值回去就丢精度了。在这个题里数组是int一般不会犯这种错但基础要打好以后处理浮点数组、结构体数组时temp类型必须和数组元素类型严格匹配。还有一种隐蔽的写法错误把交换写成了 a[i] a[n-1-i]; 然后缺少临时变量。结果就是数组后半部分的值覆盖了前半部分前半部分的原值丢了再交换时拿到的已经是错的数据。我建议初学者把交换三行代码当成一个固定模板来记每次都先写temp a[i]再写覆盖最后回填。5.4 读入问题循环边界和n的取值还有一个常见的隐蔽坑就是读入环节。题目说第一行输入n第二行输入n个整数。如果你把读入循环写成for (int i 0; i n; i)就会读入n1个数第n1个数实际上读到的是下一行的第一个值或者根本没输入导致scanf返回EOF数组里留下一个未初始化的随机值。最后输出时你会发现末尾多了一个奇怪的数字或者程序行为变得不可预测。排查这种问题时最快的办法是加个print语句把读入后的数组完整打一遍确认数据是否符合预期。很多同学调试就靠printf这不丢人反而很高效。等确认读入没问题再往下查交换和输出。这种分段排查的思路是实际工程里最常用的调试方法。5.5 常见问题速查表错误现象可能原因排查思路OJ报格式错误行末多了一个空格或者忘了换行改用“前置空格法”输出printf(\n)结尾段错误程序崩溃scanf漏写或者数组越界访问检查scanf(a[i])检查循环条件是否in输出结果和输入一样交换循环边界写成in/2改成in/2手动模拟一遍n4输出末尾多了一个随机数读入循环多读一次越界检查读入循环是否in数组前半段全是同一个数交换时没用临时变量覆盖了原值用三步交换模板重写逆序后半段乱码如果是字符串把\0也交换了字符串长度用strlen只交换0到len-16. 这道基础题在工程和算法里的位置6.1 字符串反转、回文判断、链表反转的基础很多人觉得逆序存放这种题太基础、太“实验”离真实开发很远。实际上字符串反转、回文判断、链表反转、大数加减中从低位到高位的处理都离不开“逆序”的思路。比如判断一个字符串是不是回文最直接的方法就是把它反转后和原串比较虽然效率不是最优但逻辑很简单。再比如反转链表对照数组逆序来看数组的逆序是交换位置链表的逆序是调整指针指向思路一脉相承从两端向中间或者从头到尾改变“下一个”的指向。我记得之前在公司review代码时就见过一个同事处理数据流的时候需要把一批日志行按时间倒序输出。他没有去改数据库查询语句而是直接把内存里的日志数组反转了一下然后顺序输出。这就是逆序存放在真实场景中的应用。代码和实验题几乎一样只不过数组元素从整数变成了结构体。所以别小看这20分它训练的是你处理“连续数据”时最基本的操作能力。6.2 函数传参为什么reverse函数能改原数组这道题还有一个很有价值的延伸就是自己写一个reverse函数。多数教材会要求你实现一个函数来逆序数组然后再在主函数里调用。void reverse(int arr[], int n) { for (int i 0; i n / 2; i) { int temp arr[i]; arr[i] arr[n - 1 - i]; arr[n - 1 - i] temp; } }这里就涉及前面提到的那个重要概念数组作为函数参数时传递的是首地址函数内部对数组元素的修改会直接影响原数组。这也是很多人误以为“函数参数是副本改副本不会影响原值”的例外情况。普通int参数、float参数都是按值传递的数组却是按“地址”传递的。更准确地说void reverse(int arr[], int n) 在编译器眼里等同于 void reverse(int *arr, int n)arr的本质是一个指针。搞懂这一点之后你就能明白为什么有些参数带、有些参数不带为什么结构体当参数可能复制一大块内存而数组却能“传进去就改”。这是C语言里一道经典的面试题也是很多刚入门的人绕不过去的坎。6.3 动手练一练手写三版逆序我最后给大家留一个建议虽然听起来有点像作业但真的管用。这个题不要只写一遍就丢试着用三种方式各写一遍。第一遍基础下标法就是上面讲到的 a[i] 和 a[n-1-i] 交换。第二遍指针法用 left 和 right 两个指针对撞。第三遍递归法递归本质上也是一种双指针void reverse_recursive(int arr[], int left, int right) { if (left right) { return; } int temp arr[left]; arr[left] arr[right]; arr[right] temp; reverse_recursive(arr, left 1, right - 1); }递归版本的关键在于终止条件 left right只要还没相遇就交换两个端点的值然后递归处理中间的子数组。理解这个版本后你对“函数调用栈”“数组下标是相对偏移”这些概念都会有一个更直观的感受。我在实际讲课时发现能把这三版都写对的人后面学链表反转基本不费劲因为思维方式已经打通了不管数据是存在连续内存还是分散的节点里逆序的操作本质都是“改变元素之间相对顺序”数组用交换链表用指针重连思路同源。说到底实验7-1-3这道题只是冰山一角它的价值不在于那20分而在于它把“数组”这个最基础的数据结构最重要的一面展现给你连续存储、下标访问、元素交换、边界控制。把这些打磨扎实后面无论学算法、刷LeetCode还是做实际项目你都会感谢现在认真对待这道题的自己。
返回列表