作为一个早就该把OJ刷完、却一直拖到现在的人,这两天总算把东华OJ的71到75题整理完了一遍。这批题在整套题库里不算难,但正好卡在从“语法熟悉”到“会写一点小逻辑”的过渡段,很多同学就是从这里开始分化的——写得顺的后面越刷越顺,卡在这里的很容易弃坑。这篇就把我自己的思路和踩坑记录整理出来,如果你也在刷东华OJ,正好卡在71到75这个区间,可以直接照着我这个路子走。
先说清楚,东华OJ不同年份、不同老师开的课,题号顺序偶尔会微调,所以如果你手里的71到75和我这版不完全一样,也别慌。我的原则是只看知识点,不背题号。这套题核心考的就是:数组处理、字符串统计、矩阵操作、简单数学、排序。搞定这五样,后面遇到大部分基础题都不虚。
1. 这批题目到底在考什么
1.1 从题目定位看能力分水岭
东华OJ前70题基本是顺序结构、分支、循环、简单函数,属于“照着例子写就能过”的阶段。从71题开始,题目开始要求你自己设计数据怎么存、流程怎么组织、边界怎么处理,难度提升更多体现在思维层面,而不是语法层面。
打个比方,前70题就像教你认识螺丝刀、扳手、电钻,每把工具单独演示一遍。到了71到75题,开始要求你拿着这些工具组装一张桌子:你得自己判断先拧哪颗螺丝、哪里需要加固、哪里留缝。代码量不大,但每一步都得想清楚为什么。
我在刷这批题时的体感很明确:数组去重、字符串计数、矩阵转置、完数判断、字符串排序,这五个点几乎每个都是后续题目里的“基础建材”。比如去重逻辑在后面的集合题、图论题里都会反复出现;矩阵转置是二维数组处理的典型样板;字符串排序则是字典序处理的第一课。如果这批题你只是把代码抄了一遍过掉,后面一定会回来补课。
1.2 适合谁来参考这篇记录
如果你是刚学到数组和字符串、准备开始刷OJ的在校生,这篇可以直接当操作手册用。每道题我都给了完整的C语言实现思路和代码,但我不建议你直接复制交上去,因为OJ系统查重是标配,而且抄一遍对你没有任何帮助。
如果你已经能独立AC一些题,但总在边界条件、格式输出这些隐蔽坑里翻车,这篇里的“问题排查”部分可能对你有用,我专门整理了自己WA掉的那些原因。
如果你是在准备面试、刷算法题的职场人,这批题相对偏基础,但恰好可以用来快速找回写代码的手感。我自己就是空窗了挺久后拿这套题热身,一天刷完,第二天写工作代码明显顺手很多。
2. 刷题前先搞定这些准备
2.1 编程语言和编译环境怎么选
东华OJ对语言没有太多限制,C、C++、Java、Python都支持。但我个人建议,如果不是老师强制要求,首选C语言刷这套题。原因很朴素:这套题考的就是C语言课程的核心知识点,用C写能直接暴露你对指针、数组、字符串操作到底熟不熟。用Python的话,很多题一两行就绕过去了,爽是爽了,但等于没练。
编译环境我用的是本地的VS Code加MinGW-w64,顺手配了C/C++扩展。如果你还在用那种很老的IDE,也别焦虑,能编译能调试就行,OJ不看这个。我主要看重本地调试方便,断点一打,数组内容直接看,比printf大法舒服太多。
本地写代码有一个好处:可以自己造测试数据。OJ上答案错误只是给一个WA,不会告诉你哪里错。在本地多验证几个边界场景,能省掉好多次无意义的提交。
2.2 输入输出格式这些“潜规则”
东华OJ的判题规则和主流OJ基本一致,但有几个点值得专门提醒,都是我自己踩过坑后总结出来的。
第一,输出多组数据时,要么每组之间用换行分隔,要么按题目要求输出,绝不能多打空格。很多同学喜欢在每一行结尾统一加空格,这在OJ的字符串比对里几乎必WA。我的习惯是循环里第一个数单独输出,后面的数先输出空格再输出数值,这样行尾就不会有多余空格。
第二,有些题目要求多组输入直到EOF。这需要用while(scanf("%d", &n) != EOF)这种写法,我第一次做的时候没处理,直接只读一组数据,交上去半天想不通为什么WA。后来才意识到题目描述里那句“多组测试数据”不是摆设。
第三,注意数组越界。C语言不检查越界,越界了程序不一定会崩,但可能在关键时候给你一个莫名其妙的输出。最安全的做法是数组定义时多留一点余量,比如题目说最多100个元素,你定义成105,不丢人。
2.3 本地调试的小技巧
我调试时最常用的是printf大法和断点结合。printf大法适合快速确认逻辑走到哪一步了,断点适合看某个变量在每一轮循环里的变化。
还有一个我觉得很实用的小技巧:写一个简单的数据生成器,配合自己的代码做批量测试。比如写一个随机生成数组的小程序,然后一遍遍地跑你的去重代码,看结果是不是稳定。这个习惯看起来很原始,但在刷题阶段能帮你培养对代码的“手感”。
3. 第71到73题:数组和矩阵的基础操作
3.1 第71题:数组去重
我做的第71题是数组去重,输入一个整数数组,输出去重后的数组,保持原有顺序。说来简单,但里面有个小坑:不能排序。因为题目要求保持原有顺序,这意味着你不能先排序再相邻去重,否则顺序就变了。
思路不复杂:双重循环,外层循环遍历每个元素,内层循环检查它在前面的元素里是否出现过,没出现过就输出。时间复杂度是O(n^2),对于题目给的数据规模完全够用。如果你想优化,可以开一个哈希表或者用布尔数组标记,但按OJ的数据量来说没必要,代码越简单越不容易出错。
我当时第一次交就WA了,原因是我在输出格式上犯了之前说的毛病:每输出一个数就往后面加了个空格,然后最后一个数又额外换行,OJ比对时尾随空格直接判错。后来改成“第一个数直接输出,后面的数先打空格再打数值”,立刻AC。
#include <stdio.h> int main() { int n, i, j; int a[1005]; int used[1005] = {0}; // 标记当前元素是否已经输出过 while (scanf("%d", &n) != EOF) { for (i = 0; i < n; i++) { scanf("%d", &a[i]); } int first = 1; // 控制输出格式 for (i = 0; i < n; i++) { int repeat = 0; for (j = 0; j < i; j++) { if (a[j] == a[i]) { repeat = 1; break; } } if (!repeat) { if (!first) { printf(" "); } printf("%d", a[i]); first = 0; } } printf("\n"); } return 0; }这题看起来简单,但我觉得它值得认真对待的地方在于:去重是很多复杂问题的子步骤。你不会写这个,后面做并查集、做集合运算、做哈希相关的题目都会很吃力。
3.2 第72题:统计单词数量
这题是字符串处理的经典题:给定一行英文句子,统计其中单词的数量。难点在于,单词之间可能有多个空格,句子首尾也可能有空格,不能简单按“空格个数+1”来算。
我的做法是遍历整个字符串,用一个标志位记录“当前是否正在一个单词里”。遇到字母或数字时,如果之前不在单词里,单词数加一,然后标记为在单词里;遇到空格时,标记为不在单词里。这样无论连续多少个空格,都只会触发一次计数。
这里要注意输入方式。scanf("%s")遇到空格就停了,读不了一整行。需要用到fgets或者gets。我在本地测试时用fgets,因为gets在C11标准里已经被移除了,在不支持旧标准的编译器上会报警告甚至报错。
#include <stdio.h> #include <string.h> int main() { char s[1005]; while (fgets(s, sizeof(s), stdin)) { // fgets会读入换行符,把它去掉 s[strcspn(s, "\n")] = '\0'; int count = 0, in_word = 0; for (int i = 0; s[i] != '\0'; i++) { if (s[i] != ' ' && s[i] != '\t') { if (!in_word) { count++; in_word = 1; } } else { in_word = 0; } } printf("%d\n", count); } return 0; }有个容易错的点:多组数据输入时,fgets会把上一行的换行符也读进来。我第一次没做strcspn处理,每句话后面都跟了个换行符,判断条件里没排除它,导致单词统计结果永远多或少一个。这种问题在本地单组测试时完全看不出来,一上多组数据就暴露。
3.3 第73题:矩阵转置
第73题是矩阵转置,常见版本是3乘3矩阵,输入九个数,输出转置后的矩阵。转置就是把行和列互换,c[i][j] = a[j][i]。
这道题对新手来说,真正的难点不是转置逻辑,而是矩阵的排列格局。OJ要求输出成方阵形式,每一行之间有换行,同一行数字之间用空格分隔。我看到很多同学卡在这里:逻辑对了,格式错了;格式对了,换行多了。实际上,你只需要在printf里控制一下:每一行开始前不放空格,行内数字用printf(" %d", x),行尾换行,基本就不会错。
#include <stdio.h> int main() { int a[3][3], b[3][3]; int i, j; while (scanf("%d", &a[0][0]) != EOF) { for (i = 0; i < 3; i++) { for (j = 0; j < 3; j++) { if (i == 0 && j == 0) continue; scanf("%d", &a[i][j]); } } for (i = 0; i < 3; i++) { for (j = 0; j < 3; j++) { b[i][j] = a[j][i]; } } for (i = 0; i < 3; i++) { for (j = 0; j < 3; j++) { if (j > 0) printf(" "); printf("%d", b[i][j]); } printf("\n"); } } return 0; }矩阵转置本身是个超高频考点,后面学二维数组、矩阵乘法甚至图像处理时都在用。我建议你把“双层循环控制二维数组遍历”这个模式彻底练熟,做到一看见矩阵题,代码条件反射就能写出来。
4. 第74和75题:从数学到排序
4.1 第74题:找完数
第74题是完数判断。完数是那些“等于它所有真因子之和”的数,比如6等于1加2加3,所以6是完数。常见问法是:找出1000以内的所有完数,并按指定格式输出。
这题的数学原理很简单,但代码里藏着一个效率问题:如果从1循环到n判断每个因数,1000以内没问题,但换到100000呢?所以我在写的时候做了个小优化:只循环到sqrt(n),每找到一个因数就同时把对应的那个大因数也加上。当然,要注意平方数的特殊情况,别把一个因数加两遍。
#include <stdio.h> #include <math.h> int isPerfect(int n) { int sum = 1; // 1一定是真因子 for (int i = 2; i <= sqrt(n); i++) { if (n % i == 0) { sum += i; if (i != n / i) { sum += n / i; } } } if (n == 1) return 0; return sum == n; } int main() { for (int i = 2; i <= 1000; i++) { if (isPerfect(i)) { printf("%d\n", i); } } return 0; }第一次写的时候,我直接在主函数里做了两层循环,代码又乱又容易错。后来把“判断完数”单独封装成函数,主逻辑一下子清晰了。这也是我觉得刷OJ应该早点养成的习惯:一个功能一个函数,别把什么都堆在main里。后面题目的复杂度上来后,这个习惯能救你很多次。
4.2 第75题:字符串排序
第75题是排序相关,常见版本是输入若干字符串,按照字典序从小到大输出。字典序不是字符串长度,而是逐字符比较,和英文字典的编排方式一致。
这题有两个实现维度:一个是排序算法本身,一个是字符串比较。C语言里字符串比较用strcmp,交换字符串不能直接赋值,要用strcpy到临时数组。
如果你刚学到选择排序或冒泡排序,用它们来排字符串是个很好的练习。如果已经学了qsort,也可以直接用,但要写比较函数,这正好是锻炼函数指针的机会。
#include <stdio.h> #include <string.h> void sortStrings(char s[][100], int n) { for (int i = 0; i < n - 1; i++) { for (int j = i + 1; j < n; j++) { if (strcmp(s[i], s[j]) > 0) { char tmp[100]; strcpy(tmp, s[i]); strcpy(s[i], s[j]); strcpy(s[j], tmp); } } } } int main() { int n; char s[105][100]; while (scanf("%d", &n) != EOF) { for (int i = 0; i < n; i++) { scanf("%s", s[i]); } sortStrings(s, n); for (int i = 0; i < n; i++) { printf("%s\n", s[i]); } } return 0; }这题我踩过一个有意思的坑:用scanf("%s")读字符串时,它确实能跳过空格,但如果题目里要求读的是“含有空格的字符串”,这里就会读错。你需要根据题意判断要不要换成fgets。另外,交换字符串时,临时数组的长度一定要足够大,否则strcpy越界,程序可能不报错但输出乱七八糟。
5. 做题中踩过的坑和问题排查
5.1 常见编译报错的解决办法
刷OJ难免遇到编译报错。我见过最多的是expected ';' before '}',这类纯粹是语法问题,检查分号和大括号就行。
还有一类是implicit declaration of function,意思是你调用了没用头文件声明过的函数。比如用了strlen却忘了#include <string.h>,用了sqrt忘了#include <math.h>。编译器一般会提示你,照着提示补头文件就行。
还有一个比较隐蔽的:undefined reference to 'main'。这个报错说明你主函数名字打错了,或者代码里根本没有main函数。OJ判题时是单独编译提交的源文件,如果main拼写不对,整个程序没法运行。
5.2 逻辑对了但WA怎么排查
编译过了、本地跑样例也对,一提交就WA,这是最磨人的情况。我的经验是按顺序排查三件事。
第一,你是不是没处理多组数据。很多题明写或暗示“输入包含多组测试数据”,你如果只处理一组,样例可能碰巧能过,但实际测点多组输入时必WA。解决办法就是统一用while(scanf(...) != EOF)包一层。
第二,输出格式有没有和题目要求完全一致。这里的“一致”指的是:不能有多余空格,不能少换行,也不能多换行。尤其注意最后一个输出项后到底要不要换行,看题目怎么说。
第三,你的边界条件处理有没有问题。比如数组长度为0、输入的是负数、字符串为空、n等于1等等。这些极端情况往往藏在隐藏测试点里,样例根本不会出现。
5.3 用“造数据”的方式自查
如果你反复WA但找不到原因,我强烈建议自己造几个边界测试数据。比如数组去重那题,造一个“所有元素都一样”的输入,再造一个“所有元素都不一样”的输入,再造一个“空数组”,就能看出你的逻辑有没有漏洞。
这个方法看起来原始,但非常有效。造数据的过程其实就是逼自己重新读题、重新梳理思路的过程。很多时候,WA的根因是漏看了一个限制条件,比如数组元素的取值范围、字符串的最大长度,这些信息在题目描述里都有,造数据时会被迫回头再看一遍。
5.4 关于抄题解这件事
我不反对看题解,但我反对直接抄。刷OJ的本意是练自己的代码能力,不是刷通过率。如果一道题想了半小时以上还是完全没有思路,你可以去看题解,但看完之后要把代码关掉,自己从头写一遍。写不出来的部分再回头看,看完再关掉重写。这样反复几轮,才是真正把思路吃透。
我自己的习惯是:每AC一道题,就在本地注释里写下这题的核心思路和踩坑点。这样过两周回来翻,一眼就能想起来这题在考什么、我当时卡在哪。这个习惯帮我省掉了大量重复踩坑的时间。
6. 关于刷OJ这件事,我自己的一些体会
71到75题这批题目,说实话难度不大,但我在整理过程中依然感触挺深。很多看起来很基础的代码,隔一段时间不写,真的会手生。我这次刷题前有大概两个月没怎么写C,一开始连fgets和scanf混用的边界问题都要想一下才反应过来。
刷OJ的价值不在AC那一下的快感,而在逼你去面对自己含糊的地方。数组越界你不查,它迟早会在别的题里咬你一口;scanf的换行符问题你不弄明白,换个题目描述还是会在同样的地方栽跟头。把这些小坑挨个填平,后面才会越走越顺。
最后分享一个小建议:刷题不要贪多,每天两三道就足够了,关键是每道题都吃透。你代码写完之后,可以试试换一种实现方式,比如把双重循环去重改成哈希标记,把冒泡排序换成选择排序。同一个问题,多写几遍不同写法,比刷新题更练基本功。这套71到75题刷完,你至少应该能做到:看到数组题不慌、看到字符串题知道用什么读入、看到矩阵题手不抖。如果还有哪里不稳,回头再刷一遍,稳了再往后面的题走。