☰
C语言OJ基础题实战:数字、字符串与二维数组一次吃透
2026/10/8 19:11:50 网站建设 项目流程

前阵子整理电脑里的刷题笔记,翻出一堆东华OJ的提交记录。54到56,加上58到60,一共六道题整整齐齐躺在我的“基础练习”清单里,只有57题单独标了个红叉,因为当时卡了我整整一个晚上。这组题我前后刷了三遍,每次重刷都有新收获,所以今天干脆把它掰开揉碎聊一聊。这批题表面看是考C语言语法,实际上练的是三样东西:数字处理、字符串操作、二维数组的坐标感。把这几道吃透,等于把“输入输出+遍历+分支判断”这套地基彻底打牢,后面再碰排序、查找、递归这些进阶内容就不会发怵。适合刚学完循环和数组、想在OJ上系统刷题的新手,也适合准备校招笔试前回来快速找回手感的老手。

1. 先把这一批题的底摸清

1.1 题号断档背后藏着什么信息

我当时的刷题清单上,这组题标的是“54-56,58-60”,中间活生生缺了个57。很多人第一次看以为是我眼瞎抄错了,其实不是。57题当时我确实写了,但没AC,所以那天的总结里它被单独拎出来挂起,剩下的六道题才进了“已完成”列表。这在刷OJ时特别常见——大家基本不会真的从头到尾一股脑刷完,而是先挑相同难度的题目集中练,刷到一道卡住了就跳过去,后面再回头收拾它。

这批题里,54到56给我的感觉偏“数字和字符加工”,比如拆位、判断、转换,思路直白,就是考察你把一个整数或者一串字符按规则重新组织的熟练度。58到60则明显偏“二维数组和格式化输出”,你需要跟行列下标打交道,输出格式稍有不对就判错。东华OJ这种按题号排的题库有个规律,相邻题号通常要么难度相近,要么属于同一个专题,断档往往只是个人做题顺序造成的,不影响整体参考价值。

1.2 这批题在刷题路线里到底处于什么位置

如果给编程学习画条路线图,这批题的坐标大概在“语法已入门、算法未开始”的过渡带。它们不会考你快排、二分、动态规划,也不会考链表、栈、队列这些数据结构,充其量就是循环套循环、数组套数组。但恰恰是这种“看起来简单得一逼、写起来经常过不去”的题目,最能暴露一个人在工程习惯上的漏洞:scanf和getchar混用会不会出乱子、数组下标有没有越界、多组输入有没有正确处理EOF、输出末尾到底允不允许多一个空格。

很多同学急着去啃《算法竞赛入门经典》里的难题,却在这类基础题上反复吃WA。我的建议是,先把这几十道基础题刷到“闭眼能过”的熟练度,再往后走。就像练球一样,姿势没固定,上场就打比赛,结果只能是动作变形。这批题的价值就是用来固定姿势的。

2. 刷题前的三个习惯:输入、边界、输出

2.1 多组输入拿到手先做三件事

东华OJ很多题不是单组数据,而是“多组测试,以EOF结束”或者“先给一个n,再给n组数据”。这两种读法看起来差不多,实际写起来完全是两回事。

先说要处理到文件结尾的那种,标准写法是:

int n; while (scanf("%d", &n) != EOF) { // 处理 n }

这个!= EOF很多人会漏写,漏了以后本地编译器跑起来好像一切正常,但交到OJ上要么是超时,要么是“只过了一部分数据”。原因很简单,OJ会把所有测试文件一次性喂给你的程序,你必须把所有组都读完,跳过任何一组都会被判定失败。本地跑正常是因为你只输入了一组数据就手动关掉了窗口。

另一种是先读个数n再循环处理n次的,这种反而容易在换行符上翻车。比如输入格式是先给一个n,紧接着n行是字符串,你用scanf读完n之后,缓冲区里还躺着一个换行符,如果不处理,第一行gets或者fgets读到的就是空串。这也是经典坑了,我当年就吃过这个亏,后来养成习惯:读数字之后要读字符串,就在中间放一句回收回车的代码。

int n; scanf("%d", &n); getchar(); // 吃掉数字后面的换行 for (int i = 0; i < n; i++) { fgets(str[i], 105, stdin); }

另外还有一点,fgets读进来的字符串末尾会带一个换行符,特别是当你用fgets做整行读取时,经常要手动把末尾的'\n'替换成'\0'。这个不处理,后面做字符比较、字符串拼接、求长度全都会差出一个字符来。我总结了一个操作顺序,拿到多组输入的题先做三件事:第一,看清是EOF结束还是计数结束;第二,规划好换行符由谁吃掉;第三,明确每组数据之间有没有空行需要特殊处理。这三件事搞定了,题目就成功了一半。

2.2 边界自查:数组开多大、循环到哪停

边界问题是最给我“惊喜”的地方。很多WA查了半天,结果根本不是算法错,是数组越界或者循环少了一次。

先说数组开多大。题目说字符串长度不超过100,你就开char s[101]或者干脆char s[105],千万别刚刚好开char s[100]。因为C风格字符串末尾要存一个'\0',如果输入恰好有100个有效字符,那你第101个位置必须放结束符,数组大小不够,读写越过界,本地可能没事,OJ上可能莫名其妙RE或者WA。同理,整数数组如果最大输入是1000个数,我一般开a[1005],给自己留个余量。别小看这多出来的几个字节,它对内存的浪费可以忽略不计,省下的调试时间却是实打实的。

再说循环边界。写for (int i = 0; i < n; i++)的时候,要随时问自己:循环体里有没有用到i+1?如果有,最后一次循环是不是会越界?最简单的例子是判断相邻两个字符是否相同,如果从0遍历到n-1,那s[i+1]在i=n-1时读到的就是结尾的'\0',逻辑就会出错。通常这种题正确的做法是遍历到n-1,也就是for (int i = 0; i < n - 1; i++)。还有一个经常被忽略的:很多人写数组逆序,从0到n/2交换a[i]和a[n-1-i],这个写法没问题,但如果你把上限写成n,那相当于交换了两遍,结果等于没换,这种错误特别隐蔽。

边界自查清单我建议固定写成这样几条:数组够不够放结束符、循环上限会不会越界、空字符串/零值/极端大小有没有处理、多组数据之间变量有没有重置。每次提交前花30秒过一遍,能省下一整晚的WA。

3. 逐类拆解法:数字、字符串、矩阵

3.1 数字类:%10 与 /10 背后的那套逻辑

东华OJ这批题里有一类特别经典,就是给你一个整数,让你做各种处理,比如倒序输出、统计每位数字、判断是不是回文数、或者做进制转换。这类题的共同核心只有一个:拆位。拆位的基本操作就是%10取最后一位,/10删除最后一位,循环往复直到数字变成0。

当时我写这类题的模板很简单:

while (n > 0) { int digit = n % 10; // 取当前最后一位 // 对 digit 做你想做的事 n /= 10; // 抹掉最后一位 }

这个模板有三个易错点必须提醒。第一,如果n本身是0,你进去循环体了吗?没有。所以单独处理n == 0的情况,比如要统计0的个数或者倒序输出0,就得分情况讨论。第二,拆位之后得到的数字顺序是反的,比如123拆出来是3、2、1,如果你要正序组装,要么先算出位数再倒着填,要么用递归,要么先把每位存进数组最后再逆序输出。第三,如果要处理负数,循环判断条件不能只写n > 0,要写成n != 0,否则负数直接跳过循环体。

进制转换是这类题里稍微进阶一点的版本。十进制转其他进制,核心思路是“除基取余,倒序输出”。这句口诀说得轻巧,但实际写起来还有几个坑。比如转十六进制时余数是10到15,你要把它转成字母A到F,新手经常忘了这步,直接输出10、11这些数字,然后被WA淹没。再比如结果可能是0,你得主动输出一个0,而不是让程序什么都不输出。我习惯把进制转换写成一个独立函数,输入十进制的n和目标进制base,返回一个字符串,这样就算以后遇到十六进制转二进制这类题也能直接复用。

3.2 字符串类:桶统计和双指针谁更实用

这批题里字符串相关的比例不低。常见的考法有统计某类字符的数量、判断回文、大小写转换、单词个数、去掉多余空格等等。它们的基本功其实是三样:数组充当桶、双指针扫描、以及字符ASCII码运算。

桶统计最适合解决“统计每个字母出现次数”这类题。思路是开一个长度26的整型数组,初始化为0,然后遍历字符串,cnt[str[i] - 'a']++,这样每个位置对应一个字母的频次。这个- 'a'的操作本质上就是把字符映射成数组下标,理解了这一点,以后遇到统计数字出现次数(- '0')、统计ASCII字符出现次数(直接当下标)都是同一个思路。我见过很多写法是用大段switch判断字符,效果一样,但代码又长又容易漏,没必要。

双指针则集中在回文串、字符串翻转这类题上。判断回文串的经典做法是左指针从0出发,右指针从末尾出发,两边往中间走,每次比较s[left] == s[right],一旦不相等就不是回文。要注意的是,右指针开头的初始值是strlen(s) - 1,不是strlen(s),我敢说十个WA里至少有两个是这个原因。字符串尽可能避免边删除边遍历。很多题希望你去掉某些字符,比如删除空格,新手常见的写法是找到空格就调用一次删除函数,然后把后面所有字符往前搬。这个写法复杂度是O(n^2),数据一长就超时。更聪明的做法是维护一个“写入位置”的指针,遍历一遍,遇到不需要的字符跳过,遇到需要的就放到写入位置并让指针往后移动,最后在写入位置补个结束符,一趟搞定。

3.3 矩阵类:二维数组的坐标变换

58到60这几道给我的感觉是二维数组集中爆发。矩阵转置、旋转、按某种规则遍历打印,听上去是图形题,本质上全是坐标变换。

先记住一个原则:二维数组的行列下标一定是从0开始的,写双重循环时,外层代表行,内层代表列。绝大部分错都错在把行的范围和列的范围搞混。比如一个n行m列的矩阵,遍历所有格子是这样的:

for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { // 处理 a[i][j] } }

矩阵转置的写法更刁钻一点。交换a[i][j]和a[j][i],则内层循环的起点必须是j = i,而不是j = 0。如果从0开始,你会发现交换操作做了两遍,结果转回去了,矩阵纹丝不动。这个问题我在初级班见过无数次,每次讲完还有人问“为什么程序跑完没变化”。

旋转矩阵则是另一套坐标公式。把矩阵顺时针旋转90度,等价于新矩阵的每一行是由原矩阵的列倒序得到的。所以我在写这类题时从来不背公式,而是直接按这个思路手工试一个3×3矩阵,把对应的行列关系写出来,再归纳成代码。比如原矩阵里的a[i][j],顺时针旋转90度后应该到b[j][n-1-i]这个位置。你不需要死记,只需要会推,现场推一遍只要一分钟,比背书靠谱多了。这类题还有一个共同陷阱:如果题目要求原地旋转,那你就不能开新数组,而是要用临时变量暂存要覆盖的位置;如果是把结果输出到新数组,那就别在原数组上瞎折腾,老老实实开个新数组。

4. 从 WA 到 AC:排错实录

4.1 Wrong Answer 的五个隐蔽原因

我翻了三遍自己的提交历史,把这组题里遇到过的WA原因按频率排了个序,基本集中在五个地方。

第一个是变量没重置。很多人喜欢在循环外定义变量,然后每组数据进来继续累加,结果第二组数据的结果从第一组的基础上往上加,全错。典型场景是统计字符个数、计算累加和。解决方法很简单:每个测试用例开始时把相关变量重新赋值为0。这个错本地不好查,因为本地你通常是跑一组数据,当然看不出来。

第二个是数据类型不够大。题目明明说n不超过100000,你用int大概没事;但如果题目说“两个整数相乘”,你最好下意识看一下会不会超过约21亿。很多题就是在这里挖坑,负数数据出现,排序或者比较逻辑就用错了。我这批题里没有特别大的数据,但养成打眼估算范围的习惯没坏处,特别是以后刷到素数、阶乘、组合数这些,忘了用long long卡一年都正常。

第三个是数组越界后不报错只乱输出。某些OJ编译器对越界不做运行时检查,你读出垃圾值、写入越界位,程序照跑,但答案莫名其妙对不上。排查方法很原始也很有效:把数组开大一点点,能解决大部分偶发问题。我一直默认把数组上限开成“题目最大规模+5”,这个习惯帮我躲过很多雷。

第四个是负数没考虑。数字类题目很容易出现“输入是负数”的测试数据,题面没保证非负,你的拆位循环就必须按n != 0写,输出时也要注意符号位。我之前有次写反转整数,把负号留在原数字后面输出,结果全错。

第五个是多字符输出比对出错。某道题要求输出“Yes/No”还是“YES/NO”,大小写写错也会WA。这看起来低级,但人在高强度刷题时真的可能手滑。我的办法是每次提交前把题面复制到笔记里,对着末尾写输出,写代码时不开小差。

4.2 Presentation Error 一点也不冤

PE这个判错在东华OJ上特别常见,它说的不是答案算错,而是“空格和换行的位置不对”。不少学生拿到PE第一反应是“OJ有病吧,我内容不是对了吗”,但实际上PE在正式考试里一样算错。

最常见的PE有三类。一是行尾多空格。比如输出一行数字,要求数字之间用空格隔开,你的循环在每输出一个数字后就打一个空格,导致最后一个数字后面也带空格。正确做法是单独处理第一个或最后一个:要么第一个数字前面不带空格,要么最后一个数字后面不带空格。我习惯写成“第一个数字之间加空格”的逻辑,也就是对第一个元素特判,后面每个元素输出前先打一个空格,这样末尾永远不会有多余空格。

二是多了空行。题目要求“每组数据之间用一个空行隔开”,但没说最后一行后面还有空行。很多人在每组处理完就习惯性打印printf("\n\n"),结果最后一组后面也会跟了一个空行,PE。处理方法是,在循环里判断“是不是第一次输出”,如果不是第一次,就先输出一个换行,再输出本组答案。这样最后一组后面就干干净净。

三是换行和空格混在一起。最典型的是矩阵输出,要求每行元素之间用空格隔开,行与行之间换行。新手对着行尾空格的标准写法,能写出非常别扭的if嵌套。我自己的模板是这样的:每一行内,列下标为0的元素直接输出,列下标大于0的先输出空格再输出元素;行结束统一输出换行。这样既没有行尾空格,也不会有多余空行,任何矩阵题套这套逻辑都成立。

4.3 本地怎么模拟OJ判题

OJ上的WA、RE、PE,很多在本地根本没复现过,因为本地你手工输入的数据和OJ上的测试数据不一样。后来我学会了一个特别实用的技巧:造数据。

具体操作是,先用一个脚本或者手工写出几组“刁钻”的输入,比如空数组、只有一个元素、所有元素相同、逆序输入、极大极小值,然后把你的程序跑一遍,打印输出看一眼。更硬核一点的做法是,拿一个能保证正确的暴力解法当“对拍器”,生成随机数据,同时跑暴力解和你的优化解,对比结果。一旦不一致,用二分法一点点缩小数据范围,很快就能定位到出错的那一步。

还有一个经验是,别太信任IDE的自动补全和调试器。OJ题不像工程代码,很多错误在编译器看来完全合法,但逻辑就是不对。用调试器打断点可以,但更重要的是把关键变量的值printf打印出来,特别是循环边界、数组下标、指针指向的位置。我调到后面基本是靠“打印中间结果”解决90%的问题,代码提交前再把调试用的printf删掉就行。如果没有删掉,万一OJ要求输出必须严格匹配,你多打印一行调试信息也会被判成WA。

5. 把这批题的代码沉淀成模板

5.1 必背小模板:不只会写,更要会改

刷完这批题以后,我把里面反复出现的套路整理成了几个小模板,之后刷别的OJ也能直接用。第一个是前面说过的“EOF多组输入”模板;第二个是字符桶统计,这个直接覆盖了所有字母频次相关的题;第三个是数字拆位;第四个是矩阵行列坐标转换的标准双重循环。

我贴一个我个人很常用的字符串读取加清理模板,它避免了很多坑:

char str[105]; fgets(str, sizeof(str), stdin); str[strcspn(str, "\n")] = '\0'; // 去掉fgets留在末尾的换行符

strcspn这个函数可能有些新手没见过,它的作用是查找字符串里第一次出现某个字符的位置。这里用它找到换行符并替换成字符串结束符,一行代码解决清理问题。比手写循环判断简洁得多。类似的还有判断字符类型时,不要写又长又臭的if (c == 'a' || c == 'b' ...),而是用<ctype.h>里现成的isalpha、isdigit、isupper、islower,再配合toupper、tolower做转换。很多教科书不强调这些函数,但实际刷题时它们真的香,代码短,还不会写错ASCII偏移量。

模板的意义不在于让你死记硬背,而是让你提前把“每次都要做的那部分”固定下来,省下思考时间,把注意力集中在题目本身的思路上。比如我每次写矩阵题,第一件事就是先敲一个外层行内层列的循环骨架,再去想具体逻辑。骨架固定了,就不容易手滑犯边界错。

5.2 刷题节奏和心态:这组题怎样刷效率最高

最后聊点刷题节奏的事。这批题从题号上看连在一起,但它们并不是同一专题,所以我建议分两轮刷。第一轮先把54到56一起做,因为它们都是短小精悍的数字和字符串题,适合集中突击,建立手感。做完以后暂停一下,总结一下自己用到了哪些套路,把代码模板更新一遍。第二轮再去啃58到60,这时候你已经熟悉了判题系统和输出格式的脾气,二维数组的操作也更容易一次性通过。

每道题给自己定个时间底线。我当时给自己定的规矩是基础题20分钟内没思路就看题解,看完题解必须关掉答案自己重新写一遍。这招很关键,很多人看题解一时爽,看懂和会写之间隔着十万八千里。你看完题解以为会了,第二天再让你写,照样卡壳。所以我在刷这批题时,不管当时做没做出来,隔一天以后都会重新找个地方默写一遍,写不出来的地方就是真正的薄弱点。

另一个实在的心得是,一定要留一份自己的错题记录。不需要写得多华丽,记三样东西就够了:题号、初次提交的错因、正确思路。比如我当时的记录里就有这样一行:“55题WA,原因:用gets读了数字后残留的换行符,解法:读完数字后用getchar清缓冲”。这种记录到后面复习时价值极高,比把什么公式抄十遍都有用。

我自己后来刷这组题最大的体会就是,不要小看任何一道“简单题”。东华OJ上这道题可能只有几百人通过,代码量也就在二三十行之间,但要把每个边界、每个空格、每组输入都处理干净,恰恰最考验一个人能不能沉住气。把54-56、58-60扎扎实实刷完,后面再遇到那些动不动就上百行的题目,至少你不用担心基本功拖后腿了。遇到底层逻辑没想明白的题,回头再把这几道重新做一遍,往往能获得新东西——我就是这么反复刷了三遍,每次重看54题拆位那段代码,都觉得自己对循环和余数的理解又清楚了一点。

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

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

立即咨询