尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

东华OJ刷题复盘:三数最大、阶乘求和与三角形输出的C语言算法思维

东华OJ刷题复盘:三数最大、阶乘求和与三角形输出的C语言算法思维 东华OJ刷到第7~9题这个阶段我坦白讲已经不是“照着课本抄代码”就能蒙混过去的难度了。前面几题基本在练输入输出到了这三题开始真正考察你把一个实际需求转换成程序逻辑的能力。我把自己刷这组题时的完整思路、踩过的坑、以及最后沉淀出来的OJ排错流程整理出来正在刷题的同学可以少走不少弯路。先说清楚一点不同批次的OJ题目序号可能有微调我刷到的第7题是“三个数找最大值”第8题是“阶乘求和”第9题是“输出直角三角形”。如果你手上的题号对不上也没关系这三道题恰好覆盖了选择结构、循环结构、嵌套循环与输出格式三个经典关卡思路是通用的。1. 第7题三个数找最大值比的其实是“比较逻辑”1.1 题目描述与最容易想到的解法题目很简单输入三个整数输出其中最大的那个。很多同学看完题目就开始写if嵌套一层套一层写出来的代码自己能绕晕。我当时第一反应也是这样if (a b) { if (a c) { max a; } else { max c; } } else { if (b c) { max b; } else { max c; } }这代码能过吗能。逻辑有没有问题也没有。但说实话这个写法到了四个数、五个数的时候分支数量会爆炸式增长自己看着都头疼。这题本身不难难的是你愿不愿意停下来想一想“比较”这件事的本质是什么。其实比较的本质就是两两PK谁赢谁留下来继续比。这就像班里选班长不用一次把所有候选人排个序只要先让第一组比赢的人再跟下一个人比一轮下来就知道谁最强了。这就是“打擂台”算法也是我最后采用的写法。1.2 打擂台写法为什么值得你刻意练习打擂台版本的代码非常短#include stdio.h int main() { int a, b, c, max; scanf(%d%d%d, a, b, c); max a; if (b max) { max b; } if (c max) { max c; } printf(%d\n, max); return 0; }我推荐这个写法不只是因为它短而是它把“初始候选值”和“逐一比较”两个动作拆开了。你先把第一个数当作当前最大值然后拿后面的数一个一个跟它比任何一个比它大就换人。这种思路的可扩展性非常强。如果是四个数、五个数你只需要继续加if语句就行完全不需要调整已有逻辑。更进一步如果你学了数组这个思路可以无缝迁移到“遍历数组找最大值”本质上就是同一件事。另外有一个细节必须注意max a;这个初始化不能省。如果不初始化max就直接拿来比较max里存的是一个不确定的垃圾值结果可能完全错误。虽然某些编译器环境下它恰好是0但这种“恰好”是最危险的它在你的机器上跑对了交到OJ上就是错。1.3 这题最容易忽略的三个提交检查点这一题WA的同学大多数不是逻辑错而是栽在输入输出上。我总结了自己和周围同学最容易犯的三个错误。第一个是scanf的格式串。三个%d之间到底要不要写逗号题目说“输入三个整数用空格分隔”那你写%d %d %d或者%d%d%d都行但千万别写%d,%d,%d。后者要求你在输入的时候也带逗号而OJ的测试数据里没有逗号结果就是第一个数读进去了后面两个变量根本没收到值直接WA。第二个是取地址符。很多同学写scanf(%d%d%d, a, b, c);少写了三个。编译的时候编译器不一定会报错但运行时就等着看随机结果吧。这属于C语言里新手最容易踩的坑没有之一。第三个是输出换行。OJ的评测是逐字符比对输出结果的你少一个换行评测系统就判你错。哪怕你答案算得完全正确没有换行就是WA。所以printf(%d\n, max);里的\n一定要养成肌肉记忆。2. 第8题阶乘求和卡住你的不是循环而是数据类型2.1 从“数学公式”到“代码逻辑”差在哪一步第8题长这样输入一个正整数n计算 1! 2! 3! ... n! 的值。数学公式很简单但直接翻译思路就会遇到两个问题一是要不要为每个阶乘单独算一遍二是算出来的数该存到哪种变量里先说不好的写法这也是很多初学者最直接的反应int sum 0; for (int i 1; i n; i) { int term 1; for (int j 1; j i; j) { term * j; } sum term; }这个两层循环从逻辑上没错i的阶乘就内层循环从1乘到i。但你可以算一下它的工作量算1!要1次乘法算2!要2次算3!要3次……算n!要n次总共是123...n次乘法时间复杂度O(n²)。当n只有10、20的时候毫无感觉一旦n到了几千这种写法就会跑得很慢。更关键的是这个写法完全忽视了阶乘本身的性质5!本来就是4!×5你上一个阶乘已经算出来了为什么还要从1重新乘一遍这就是“重复计算”的典型反面教材。2.2 递推优化一行代码把O(n²)降成O(n)好的做法是让当前阶乘建立在上一个阶乘的基础上#include stdio.h int main() { int n; long long sum 0; long long term 1; scanf(%d, n); for (int i 1; i n; i) { term * i; sum term; } printf(%lld\n, sum); return 0; }这段代码的核心就两个变量。term表示当前循环到i时的i!它在上一轮的基础上直接乘i省掉了整个内层循环。整个算法只有一层循环n次复杂度O(n)。你别小看这个“用上一次的结果推导下一次”的思路它就是“递推”思想的雏形。后面你会遇到的斐波那契数列、动态规划本质上都是这个套路搞清楚了当前这一步和上一步的关系就不用每次都从头计算。2.3 int溢出OJ里最隐蔽的判错这一题如果只用int存sum和termn稍微大一点就会出错。这里我给一个具体的数据感受8! 40320没问题12! 479001600已经接近int上限的一半13! 6227020800已经超过int上限约21.47亿直接溢出一旦溢出C语言里的int会“回绕”变成乱七八糟的负数或小数字你的sum当然跟着错。但最坑的是如果你自己拿小数据测试n5、n6这种结果都正常于是你没发现问题信心满满地提交结果WA。所以我强烈建议从写OJ题的第一天起就建立“数据类型边界”的意识。凡是可能变大的整数毫不犹豫用long long。它占8个字节最大能到约922京9.22×10的18次方应付入门阶段的题目绰绰有余。还有一个小细节printf输出long long必须用%lld写成%d拿到的是一个被截断的错误值。这台机器上可能碰巧对换台机器就崩了千万别赌。另外提一句如果你用的本地Windows环境下的老版本编译器有些会要求写成%I64d而不是%lld。但OJ的评测机基本是Linux下的GCC%lld是标准写法以OJ为准就行。2.4 如果n特别大这道题就没那么简单了刷完基础版本之后我额外想了一个问题如果题目改成n可以到1000怎么办long long照样炸掉因为1000!是一个拥有两千多位数字的巨型整数任何内置类型都存不下。到那时候就必须上“高精度算法”了用数组的每一位存一个数字自己模拟乘法进位过程。那是另一类题目的范畴但你可以记住这个演进路线后面学到字符串处理的时候自然就能串起来。第8题用long long能过说明题目本身就在考察你对内置数据类型边界的理解。3. 第9题输出直角三角形专治“看答案就会一提交就错”3.1 嵌套循环的两个角色外层管行、内层管列第9题是输入一个正整数n输出n行的直角三角形第一行1个星号第二行2个星号以此类推。看起来比前两题有意思一点因为它涉及“格式”了。题目本身不复杂但很多人拿到题的第一反应是“循环里打印星号嘛”然后就开始写写到一半卡住了不知道什么时候换行。这就是没搞清楚嵌套循环的分工。嵌套循环里外层循环负责“行”内层循环负责“列”。外层每跑一次代表输出一行内层跑的次数决定这一行有几个星号。对应到这道题第i行应该输出i个星号所以内层循环的结束条件就是j i。#include stdio.h int main() { int n; scanf(%d, n); for (int i 1; i n; i) { for (int j 1; j i; j) { printf(*); } printf(\n); } return 0; }这段代码就是最核心的骨架。你可以在自己的编译器上跑一下输入3输出是* ** ***到这里题目就算完成了吗如果OJ只是要求输出这个确实没问题。但我刷的那一版题目在后面补了一行每个星号之间用一个空格隔开。这就完全不一样了。3.2 星号之间的空格和Presentation Error的真相加上空格要求后n3的输出变成* * * * * *很多同学随手就写for (int j 1; j i; j) { printf(* ); } printf(\n);你会发现输出变成每行末尾多了一个空格像这样* * * * * *最后一行最后那个星号后面跟着一个空格。肉眼几乎看不出来但OJ看得到。评测机是一个字符一个字符地比对标准答案多出来的空格会让它判定输出与答案不符——在OJ里这叫Presentation Error也就是PE。这时候我强烈建议不要用“输出一个退格符”或者“先多输出再删掉”的写法去补救那属于绕弯子而且很容易出新的问题。正常的解法是在输出前判断一下如果是这一行的最后一个星号就只输出星号不输出空格。#include stdio.h int main() { int n; scanf(%d, n); for (int i 1; i n; i) { for (int j 1; j i; j) { if (j i) { printf(*); } else { printf(* ); } } printf(\n); } return 0; }判断条件j i就是“当前是不是本行最后一个”的意思。最后一个不补空格其他位置补一个空格这样既满足了“星号之间用空格隔开”又不产生行尾多余空格。3.3 图形题通用分解法先画框架再填空第9题让我真正学会的不是打印星号而是一套处理图形输出题的固定套路我后来拿它做过好几道类似的题目成功率很高。第一步先别管空格用一个最简单的循环输出一个“没有空格”的三角形确认行数和星号数量关系是对的。这一步过不了后面全白搭。第二步再考虑空格的位置和数量把格式修正。第三步才是交到OJ上根据返回结果判断是逻辑错还是格式错。比如这题如果改成输出一个靠右对齐的直角三角形* * * * * * * * * *那就是每一行先输出若干个空格再输出星号。空格的数量跟行号有关系公式是n - i。同样的三步法照样适用先管星号再管空格最后合并。这种“先骨架后细节”的思路不只适用于图形题。凡是要构造结构化输出的题目我建议都在本地先把框架跑通再加入格式细节。格式往往比逻辑更容易检查出来因为你可以直接拿眼睛对照样例输出。3.4 一道题牵扯出来的本地调试小技巧写第9题的时候我犯过一次特别蠢的错程序在我自己的电脑上运行结果完全正确但一提交就是WA。后来我把输出结果逐字符核对了一遍才发现我把样例里的空格数搞错了。从那次以后我养成了一个习惯写OJ题一定要学会自己构造测试用例不能只依赖题目给的样例。题目给的样例是最简单的验证它能帮你排除明显的错误但它往往不会覆盖所有边界情况。比如“只有一个星号”的n1情况你完全看不出每行末尾有没有多空格因为只有一个字符根本看不出来差异。但OJ会测。这里推荐一个笨但非常有效的办法本地运行你的程序把输出重定向到文件里然后用文本编辑器打开打开“显示空格/制表符”的开关你就能一眼看出行尾有没有多余的空格。这个操作在不同编辑器里位置不一样但基本上都在“视图”菜单下叫“显示空白字符”之类的选项。一行代码配合一行配置能让OJ上的WA变成AC。4. 刷完这组题我沉淀出一套OJ排错自查流程4.1 四层排查法格式、边界、类型、逻辑三道题刷下来我明显感觉到自己在做题的时候不再像无头苍蝇一样乱撞了。我把踩过的坑归纳成一套固定的排查顺序后来每次OJ提交失败我都按这个顺序过一遍先是格式然后是边界接着是类型最后才是逻辑。第一层是格式。是不是多空格了少换行了输出中文标点了这个用肉眼对照样例通常能发现。第二层是边界。你写的循环有没有考虑n等于0、等于1、等于最大值的情况数组会不会越界第三层是数据类型。这个值会不会溢出用int够不够是不是需要long long第四层才是逻辑。前三层都排除了再回头看你算法的思路是不是本身有漏洞。这个顺序是有讲究的格式题最容易修检查成本最低逻辑题的检查成本最高而且最容易让人陷进去反复看代码也找不出问题。先把低成本的排除干净最后再集中精力对逻辑效率会高很多。4.2 样例AC不是真的AC多构造几组“极端输入”我见过不少同学包括我自己早期都是写完代码跑一下样例看到输出一样就激动地提交结果WA。原因很简单OJ的测试数据远不止样例那一组它会覆盖各种边界条件。我拿这三道题给你示范一下什么叫做“自己构造测试用例”。第7题你要测的三个数包含负数、包含0、三个数全相等、最大值出现在第一个、出现在中间、出现在最后一个。第8题n要测1、测2、测一个比较大的值验证它不会溢出。第9题n一定要测1因为这时候最容易出错的是行尾空格和换行。这些测试用例在你脑子里过一遍就行不用真的每个都写在代码里。但如果你不确定花一分钟在本地一个一个输进去验证比反复交OJ试错快得多。4.3 实测下来最顺手的三件“装备”最后分享几个我做OJ题时觉得非常有帮助的小装备全部免费而且配置一次就能一直用。第一件是本地编译时的警告选项。用GCC的话编译命令里加上-Wall -Wextra这两个参数编译器会把你代码里可疑的地方都用警告提示出来比如变量未初始化、类型不匹配等等。很多WA在本地编译阶段就可以被拦住。第二件是使用diff命令或编辑器自带的文件对比功能。把你自己程序的输出保存到out.txt再用diff out.txt answer.txt逐行对比比用眼睛盯着终端窗口看靠谱得多尤其当输出内容巨长的时候。第三件是要建立一个自己的“错误代码博物馆”。我把自己每次提交失败后改正前的代码复制到一个专门文件夹里旁边用注释记下错误原因。三个月后回看这个文件夹你会惊讶地发现自己以前犯过的错都极其典型而且你会很清楚地看到自己是从什么时候开始不再犯这些错的。这三件事不复杂但相信我它们比多刷五十道题的价值更大。做题本身提升的是算法熟练度而这套自查体系提升的是你把错误转化为经验的速度。刷OJ不就是为了这个吗。
返回列表