东华OJ第46-50题详解:数组、字符串、排序与算法基础入门
2026/9/13 2:40:55 网站建设 项目流程

1. 先聊聊东华OJ第46-50题这组题目

前阵子把东华OJ的题从第1题往后刷,刷到46-50这组的时候,明显感觉和前面那些纯考语法、考输入输出的题不太一样了。前面十几道题基本就是“照着模板敲”,考察的是你会不会用scanf、printf、if-else、for循环。但从这个区间开始,题目开始加入了一些“需要自己想清楚再动手”的成分,比如对数组做处理、字符串的边界判断、查找和排序的逻辑组织。用大白话说就是:从“这个语法我会不会”过渡到“这个问题的解法我想不想得到”。

这组题适合两类人看。一类是正在刷东华OJ、刚好卡在40到60题之间的初学者,另一类是想了解OJ判题规则、想搞清楚为什么代码本地跑得好好的上传就WA(Wrong Answer,答案错误)的同学。我会把每题的题型判断、核心思路、容易踩的坑全部拆开讲一遍,也会给出可以直接复用的代码框架。尤其是那些“本地运行正常、OJ上一交就错”的经典原因,我会单独拿出来分析,因为这可能是刷OJ过程中最打击人、也最能涨经验的地方。

先交代一下背景。东华OJ指的是东华大学在线评测系统,刷题方式和主流OJ平台完全一样:题目会给出若干组输入样例和输出样例,你提交的代码通过标准输入读取数据,通过标准输出打印结果,裁判程序会用隐藏的测试数据来比对你的输出。乍一听很简单,但隐藏数据往往专门挑你代码的漏洞,比如最大数据量、空数据、重复元素、边界值、数组下标越界,等等。46-50这组题几乎把上面这些坑都踩了个遍,所以我建议你不要只看“AC了就完事”,而是要顺手把这五道题涉及的思维方式梳理出来,后续刷到更难的数据结构题时,基础会扎实很多。

我整理这组题的时候,把每一道的题型、考点、难度系数、典型坑点都过了一遍,先给一个速查总览,后面再逐题展开。

题号核心考点难度常见失分点
46数组元素统计与去重偏低计数数组初始化、重复输出的处理
47字符串处理与边界判断中等字符串结尾符、长度计算
48顺序查找/二分查找中等升序前提、找不到时的返回值
49最大公约数与最小公倍数偏低数据类型溢出、辗转相除的终止条件
50结构体排序与多关键字比较中等偏上比较函数写错、稳定性要求

从这个表能看出来,第46到第50题正好覆盖了算法入门阶段最核心的几个基础算法:数组操作、字符串、查找、数论基础、排序。这也是很多学校OJ在布置作业时比较惯用的出题思路——前几道题巩固语法,中间这组题开始考察算法思维。把这一组啃下来,后面再做链表、栈、队列、递归这种更抽象的内容,至少不会被“读不懂题”卡住。

2. 逐题思路拆解:从读懂题到写出解法

2.1 第46题:数组统计与去重,先想清楚“用什么存数据”

第46题我印象比较深,因为它的核心考点在“统计”和“去重”这两个动作上。题目给出一组数据,让你输出出现次数满足条件的元素,或者输出去重之后的结果。不同学校的OJ版本可能细节略有差异,但骨干逻辑是一致的:你得在遍历数据的过程中,记录下每个元素出现了多少次,再根据条件决定输出什么。

最容易想到的做法是两层循环:外层遍历每个元素,内层再从头扫一遍统计它出现了几次。数据量小的时候能过,但数据量大一点就容易超时。我这里更推荐用“计数数组”的思路,这也是后面很多题的基础:如果数据范围有限(比如元素是0到100之间的整数),直接开一个int 数组,下标表示元素值,数组存的是这个值出现的次数。遍历原始数据的时候,每读到一个数x,就执行 count[x]++,这一步的时间复杂度是O(n),比两层循环的O(n²)快一个量级。

这里有一个初学者必踩的坑:计数数组一定要先清零。很多人定义 int count[1000]; 就直接用,本地跑的时候运气好没问题,但OJ的测试数据一来,上一次运行残留的脏数据就会导致统计结果完全不对。正确姿势是写完 int count[1000] = {0}; 或者用 memset(count, 0, sizeof(count)); 来初始化。

再说说去重。如果题目要求按原顺序输出不重复的元素,我建议你在遍历原数组的同时,用另一个标记数组记录“这个值是否已经输出过”。每访问一个元素,先查标记数组,如果没输出过就输出,然后把标记置为1。这道题的高频错误是把重复元素也输出了一遍,原因往往是漏掉了标记数组的更新逻辑。

2.2 第47题:字符串处理,所有问题几乎都出在边界

字符串题在OJ里是“看起来简单、做起来全是坑”的典型。第47题通常是回文判断、字符统计或字符串反转这一类。题目本身不难,但字符串有几个特殊性:结尾有 \0、输入可能包含空格、下标从0开始、长度需要单独计算。任何一个环节没注意,WA就等着你。

以最常见的回文判断来说,最简单的写法是把字符串反转后和原串比较。但很多同学会踩这样一个坑:用 char str[100]; gets(str); 读取,然后直接计算 strlen(str),却忘记了数组中实际还有一个结尾的 \0。虽然在很多场景下 strlen 已经帮你把 \0 排除在外了,但自己写循环时经常会多算一位或少算一位,导致判断出错。

我个人更推荐用双指针思路:一个指针从字符串头部开始,另一个从尾部开始,两边往中间走,直到相遇或交叉。每次比较 str[i] 和 str[j],只要发现不等就说明不是回文。这个写法代码量少,也不容易出错,而且时间复杂度同样是O(n)。判断结束条件是 i < j,这个地方不要写成 i <= j,否则中间那个元素会被比较两次,虽然不影响结果,但逻辑上不够干净。

另外要特别提醒:如果题目要求处理包含空格的字符串,千万不要用 scanf("%s", str),因为 %s 遇到空格就停止读取了。这个时候应该用 gets() 或者 fgets(),但需要注意 fgets 会把你敲的回车符也读进来,需要手动把结尾的 \n 替换成 \0,否则后面的判断就全乱了。这个细节是我自己在刷题时踩过多次的坑,列出来给各位提个醒。

2.3 第48题:查找类题目,别忽视“数据是否有序”这个前提

第48题是查找题,无非是给你一个数列和一个目标值,让你输出目标值在数列中的位置或判断是否存在。这类题的解法严重依赖于输入数据是否有序,所以拿到题第一步不是写代码,而是先判断数据是不是按升序(或降序)排好的。

如果数据无序,那只能用顺序查找,从第一个元素开始往后逐个比,遇到相等的就记下下标,循环完了还没找到就输出-1。这个没什么好说的,注意下标从0开始还是从1开始和题目要求对齐就行。东华OJ这类题有时候要求输出的是“第几个元素”,也就是1-based下标,你如果直接输出了数组下标(0-based),就会导致全部结果都比标准答案小1,这种错误很难排查,因为样例往往刚好把下标绕过去。

如果题目明确说数据已经按升序排序,那就要考虑用二分查找。二分查找的模板很固定:左边界 l 设为0,右边界 r 设为 n-1,循环条件是 l <= r,取中点 mid = (l + r) / 2,然后比较 nums[mid] 和目标值的大小,决定把区间缩小到左半部分还是右半部分。这里有个隐蔽的坑是 mid = (l + r) / 2 在 l 和 r 都很大的时候可能溢出,虽然OJ题目数据一般不会变态到那种程度,但养成写 mid = l + (r - l) / 2 的习惯总归是好的。

写二分查找容易错的地方还有一个:循环结束后 l 的位置其实代表了“第一个大于等于目标值的位置”,这个性质在后面的“插入位置”类题目里会用到,但如果你只是要判断某个值是否存在,别忘了在循环里找到匹配时及时 return,否则你会得到 l 而不是真实位置,答案自然就错了。

2.4 第49题:最大公约数和最小公倍数,记住“先除后乘”

第49题是数学类基础题,一般让你算两个正整数的最大公约数(GCD)和最小公倍数(LCM)。这题的算法本身很固定,最大公约数用辗转相除法,也叫欧几里得算法。代码短到只有几行,但它背后有一个很重要的细节很多人没注意到。

辗转相除法的核心是:gcd(a, b) = gcd(b, a % b),一直递归或循环到余数为0,此时的除数就是最大公约数。用循环写就是这样:

int gcd(int a, int b) { while (b != 0) { int temp = b; b = a % b; a = temp; } return a; }

这段代码建议直接背熟,因为它太常用了。最小公倍数的公式也不是 b 乘 a 再除最大公约数这一种写法,需要注意运算顺序:

lcm(a, b) = a / gcd(a, b) * b

注意,这套写法的顺序很重要。如果写成 a * b / gcd(a, b),当 a 和 b 比较大的时候,中间的乘积可能溢出int的范围,导致结果完全错乱。先除后乘,就不会有这个问题。这也是很多老手反复强调的一个点,刷题时千万别忽略。

2.5 第50题:结构体排序,比较函数是唯一难点

第50题通常就开始上综合难度了,常见考点是“学生信息排序”或者“成绩排名”,输入一组记录(比如学号、姓名、成绩),要求按某个规则排序输出。没有结构体概念的同学,可能还在用好几个平行数组分别存学号、姓名、成绩,排序的时候手动同步交换几个数组,代码写起来又长又容易漏。

正确思路是定义一个结构体,把每一条记录的所有字段打包在一起,然后用C标准库里的 qsort 排序。比如:

typedef struct { int id; char name[50]; int score; } Student;

qsort 的用法是固定的,四个参数分别是要排序的数组首地址、元素个数、单个元素大小、比较函数指针。关键是写比较函数。如果你想按成绩从高到低排,写成:

int cmp(const void *a, const void *b) { Student *sa = (Student *)a; Student *sb = (Student *)b; return sb->score - sa->score; }

这里有一个初学者常犯的错误:把返回类型写成 int,结果return两个分数之差没问题,但如果你要对字符串排序,直接 return sa->name > sb->name 就完全不对了,必须用 strcmp。另外,如果成绩相同,题目可能要求按学号升序排,这时比较函数要写成“先比成绩、成绩相等再比学号”的多关键字比较,漏掉任何一个排序条件都会导致和标准答案不一致。

3. 现场写代码:一段能直接复用的参考实现

3.1 第50题的完整参考代码(C语言)

很多同学问过我,结构体排序到底怎么把输入输出串起来,我直接贴一段自己当时提交时能过题的完整代码,以“N个学生信息按键值排序输出”为例,方便你直接对照。需要注意,不同学校OJ的题目细节不太一样,但框架可以复用。

#include <stdio.h> #include <stdlib.h> #include <string.h> typedef struct { char id[20]; char name[50]; int score; } Student; // 比较规则:分数降序,分数相同按学号升序 int cmp(const void *a, const void *b) { Student *sa = (Student *)a; Student *sb = (Student *)b; if (sa->score != sb->score) { return sb->score - sa->score; } return strcmp(sa->id, sb->id); } int main() { int n; while (scanf("%d", &n) != EOF) { Student stu[1000]; for (int i = 0; i < n; i++) { scanf("%s %s %d", stu[i].id, stu[i].name, &stu[i].score); } qsort(stu, n, sizeof(Student), cmp); for (int i = 0; i < n; i++) { printf("%s %s %d\n", stu[i].id, stu[i].name, stu[i].score); } } return 0; }

这里有两个点值得说。第一是 while (scanf("%d", &n) != EOF),OJ题目经常用“多组测试数据直到输入结束”的方式给出数据,如果你只读一组就退出,只能过样例,过不了全部分数。第二是 qsort 的比较函数签名必须是 const void * 类型,很多第一次用的人在这里犯迷糊,强制类型转换之后就对了。

3.2 输入输出细节和OJ判题机制

聊到OJ,就不得不澄清一个很多新手没搞明白的问题:OJ的判题系统完全不看你代码里的注释、变量名、代码风格,它只看两件事——程序的输出和标准答案是否完全一致,以及是否在规定时间和内存内跑完。所以代码长一点、变量名丑一点也不怕,怕的是输出多了空格、少了换行、或者末尾多打印了一个空行。这些格式错误(PE)很多时候只凭肉眼根本看不出来,解决办法是下载样例数据,自己终端里跑一遍,然后用diff命令逐字节比较输出。这也提醒我们,刷OJ时第一优先级不是“写出很优雅的代码”,而是“写出能精确匹配输出的代码”。

再展开说一下“多组输入”的问题。很多题目会写“输入包含多组测试数据,每组占一行”,但不会明确告诉你一共有几组。这时候 while (scanf(...) != EOF) 几乎是万能写法,它能帮你一直读到文件结束符。使用它的前提是,每次循环体内都要重新初始化需要用到的变量或数组,否则上一组数据残留的值会污染下一组的计算结果。

4. 刷题时踩过的坑:常见错误与排查对照表

4.1 编译出错和本地能过、OJ过不了的经典原因

刷OJ最让人崩溃的一件事就是本地编译器跑得好好的,一交上去就报编译错误(CE)或者答案错误(WA)。结合我的经验,最常见的几个原因如下:

  • 使用了非标准头文件或函数。比如 Turbo C 环境里常用的 conio.h、getch(),在OJ的评测环境里根本不存在。
  • 把 int main() 写成了 void main()。部分编译器会容忍,但OJ的编译器往往比较严格。
  • 中文标点混进了代码。全角分号、全角括号、中文引号,肉眼很难看出来,但编译器一遇到就报错。
  • 数组开小了。题目说数据量最多1000,习惯性开了100,本地测试用少量数据自然没问题,OJ用最大数据一测就数组越界,表现可能是WA、RE(运行时错误)甚至TLE。
  • 忘了处理多组输入,或者处理完之后没有重置全局变量。

我在这里整理了一个“错误类型排查速查表”,每次提交WA之后按表格逐项排查,效率会高很多:

错误类型典型表现优先排查方向
编译错误 CE提交后直接编译失败头文件是否齐全,函数签名是否正确,是否有中文符号
答案错误 WA程序能跑,结果不对边界条件、初始化、比较函数逻辑、多组数据残留
运行时错误 RE程序崩溃退出数组越界、除零、空指针、递归无出口
时间超限 TLE长时间没有输出算法复杂度过高,是否该用更优解法
格式错误 PE结果对但多空格/少换行输出格式、行尾空格、末尾空行

4.2 边界数据与初始化问题

上面这个表里,WA是出现频率最高的错误,而这个错误里又有很大一部分是“边界数据和初始化”导致的。我举三个具体例子。

第一个例子,数组统计题。int count[1000] = {0}; 写不写初始化,本地测试可能都没问题,但只要OJ的测试数据里有超过某个阈值的数字,count 数组就会越界。更隐蔽的是,如果你忘了在每组测试数据之间重置 count,上一组数据的统计结果就会叠加到下一组上,导致输出错得莫名其妙。

第二个例子,字符串题。用 char str[100]; scanf("%s", str); 读入正常字符串没问题,但如果输入里包含空格,scanf 读到空格就停了,此时字符串只读了一半,后续所有基于完整字符串的逻辑全部错乱。这种错很难从代码本身看出来,必须回头去读题目,确认输入格式里到底允不允许空格。

第三个例子,排序题的比较函数。qsort 的比较函数返回正数、零、负数分别表示a排在b后面、两者相等、a排在b前面。很多人写成绩排序时只写了 return a->score > b->score,返回的是1或0,没有负数的情况,这会导致排序结果不稳定,和标准答案不一致。正确的写法是用差值 return b->score - a->score,或者显式判断返回1、-1、0。

4.3 实用的调试与排查技巧

说完了错误类型,再分享几个实际排查问题的小技巧。第一个技巧是“中点打印法”。在代码关键位置加printf输出中间变量,比如二分查找里每次更新的 l 和 r,统计题里每读一个数后 count 的变化,看一遍运行过程的日志,基本能定位是逻辑问题还是数据处理问题。不过提交前一定要把调试用的printf删掉或注释掉,否则输出多了调试信息,必WA。

第二个技巧是“用最小样例测试”。遇到WA时,不要直接去网上搜答案,先试着构造几个极端情况:空输入、只有一个元素、所有元素都相同、元素已经是排好序的、元素全部逆序、数值取到题面上限。这些数据往往能瞬间击穿你的代码逻辑。举个简单的例子,做回文判断时,字符串长度是1是不是回文?答案是。如果你没考虑这种情况,判断逻辑就可能出错。做去重时,所有元素都一样时,输出一个还是多个?这些都是边界条件。

第三个技巧是“重看一遍题目”。有时候WA不是代码问题,是读题问题。题目要求“输出元素在序列中的位置”,可能说的是第几个位置而不是数组下标;题目要求“按成绩降序,成绩相同按姓名升序”,你只实现了成绩降序,漏了姓名。这种低级失误在连续刷题疲劳的时候特别容易犯,所以每次WA之后,第一件事不是改代码,而是重新读三遍题。

5. 从46-50题说开去:算法入门阶段的刷题建议

5.1 做题顺序与时间分配

很多人刚开始刷OJ的时候有个误区:觉得题目做得越多越好,于是一路狂刷,遇到不会的题就查题解、背代码,表面上看进度很快,实际上基础完全没打牢。我个人的体会是,像东华OJ 46-50这种“基础算法过渡组”的题目,适合慢下来精做。每道题做完之后,对比一下自己的解法和其他人的解法,看看有没有更优的思路;再把题目的条件改一改,比如数据量变大、要求变成倒序输出、增加关键字,重新写一遍。这种变式训练比闷头刷10道新题更管用。

时间分配上,我建议每一道题留出“三遍时间”:第一遍自己独立思考,哪怕想不出来也要先把暴力解法写出来;第二遍对照题解或参考代码,搞清楚优化点在哪里;第三遍关掉所有资料,从零开始自己写一遍。这个流程看起来费时间,但只要坚持几组题下来,你的代码能力会有肉眼可见的提升。我刷46-50这五道题,按这个流程走,总共花了大概一个周末——第一天做前两题并复盘,第二天冲刺后面三题,时间也算不亏。

5.2 用“错题本”思维复盘

刷OJ的另一个建议是建一个自己的错题本。不需要很复杂,一个文档就行,记录以下信息:题号、题目在考什么、我的错误解法是什么、错因是什么、正确思路是什么。尤其要记的是“为什么我没想到这个思路”,这比单纯记录代码重要得多。我翻了一下自己的刷题日志,46-50这组题里,我最常见的错因是三类:没有初始化数组、比较函数写错、漏了多组输入,这些问题如果不记下来,下次换个题目依然会犯。

复盘还有一个作用,就是帮你发现自己对某个知识点掌握不牢。比如第50题如果你做排序时反复出错,说明你对结构体和qsort的理解还停留在“背模板”阶段,最好的补救方式不是继续做新排序题,而是回头把结构体、指针、内存分配这一节重新过一遍。这也是我认为“从题目回溯知识点”比“按顺序啃教材”更高效的原因——带着问题学习,印象会深刻得多。

5.3 从AC到理解:别急着做完就划掉

最后再聊一点心态上的东西。OJ上的AC(Accepted)只是说你过了评测,说明你的输出和标准答案一致,但完全不代表你已经理解了解法。我见过不少同学,复制粘贴别人的代码过了AC,然后开心地标记为“已掌握”,等到期中考试或者面试手写代码时,大脑一片空白。46-50这组题本身不难,但它们是“从模仿到独立解题”的分水岭。如果你能做到拿到题后不查任何资料,30分钟内写出能通过全部测试数据的代码,那这组题才算真正刷透了。

我个人判断一道题是否吃透,会问自己三个问题:第一,如果数据量扩大100倍,我的解法还会超时吗?第二,如果输入数据是特殊值,我的代码能正确处理吗?第三,如果让我给另一个同学讲出这道题的思路,我能不看代码把逻辑说清楚吗?这三个问题都能答上来,这道题才算真正属于你了。46到50这五道题,其实每道题都值得用这三个问题自测一遍。数组统计题问自己数据量很大的时候计数数组会不会爆;字符串题问自己输入包含空格和超长字符串时怎么办;查找题问自己数据完全逆序时二分查找还成立吗;数学题问自己两个数都是int上限时先乘后除会不会溢出;排序题问自己遇到完全相同学号或者说如果比较函数漏了第二个关键字会怎样。把这些问题在脑子里过一遍,比多做十道题有用得多。

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

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

立即咨询