1. 先别急着交卷这道 10 分题的含金量比看起来大在题目集里翻到第 7 章第 3 题看到“逆序的三位数”这五个字加一个刺眼的 10 分很多人的第一反应是这么简单的题五分钟搞定交完就走。我当年也是这么想的结果第一发提交就挂在一个我自己都没意识到的用例上——输入120我输出了021。那一刻我才明白这道题真正考的不是“会不会写循环”而是“能不能把一个自然语言描述翻译成判题机愿意接受的字节序列”。这道题做的事情非常朴素读入一个三位数把它各位数字的顺序倒过来输出新的数。它能解决的问题也很具体——帮你把“输入解析、数位拆解、边界处理、输出格式”这四件最基础的事一次性走通。适合谁来参考刚接触编程语法的同学、准备带新人的师兄师姐、以及刷了几百题但对基础细节仍然含糊的人。别小看最后这一类我见过太多能写线段树却在scanf返回值上翻车的人。我打算把这道题彻底拆开从题面里那些容易被跳过的字眼到三种不同解法的取舍逻辑再到多语言的完整代码、自测用例表、判题结果对照表最后延伸到它可以变出的七八个练手方向。全篇都是我实际操作和带人过程中积累的东西能直接抄作业也能拿去当讲稿。2. 题目还原一句话背后藏着四个判断点2.1 逐字读题哪些字是陷阱哪些字是安全区题面通常就两三行给定一个三位数请输出它逆序后的数。就这两三行至少要抠出四个判断点。第一个判断点是“三位数”这个词。它限定了输入范围也就是 100 到 999 之间的正整数百位不可能为 0。这一条直接决定了我们拆位的时候不用考虑“首位是零”这种输入情况n / 100拿到的必然是个 1 到 9 的数字。第二个判断点是“逆序后的数”注意是“数”不是“字符串”。数是没有前导零这个概念的021在数学上就是21。这两个字之间的差别就是这道题最大的坑。第三个判断点是“正整数”还是“整数”。题面说三位数一般默认正数不会出现-123。但如果题面写了“整数”且没有明确排除负数稳妥的做法是先按正数写一版提交看结果再决定要不要补负号分支。第四个判断点是输出格式。绝大多数判题机做的是逐字节比对末尾多一个换行可能没事多一句“请输入三位数”就必然报错。这四条抠完题目的轮廓才算清楚。2.2 判题机到底在比什么一个被严重低估的常识我在带新人的时候发现很多人对判题机制的理解是空白的他们以为程序会“看懂”自己在做什么。实际情况是判题机把你的程序输出和标准答案都当成一堆字节从头到尾逐个比对通常只做两件宽容的事忽略每行末尾多余的空格忽略文件最末尾多余的空行。除此之外全都要对上。这就解释了一个特别常见的现象本地运行看着完全正确一交就错。因为你在本地看到的请输入一个三位数321在判题机眼里是请输入一个三位数321这一整行而标准答案是321第一个字节就不一样。所以从这道题开始就该养成一个习惯所有面向判题机的程序代码里一个提示字符都不要有输出就是纯结果加一个换行。还有个小细节值得提一下printf(%d\n, rev);和printf(%d, rev);在绝大多数平台上都能过但如果题目明确要求行末换行或者你后面要循环输出多组结果还是老实加上\n。我自己是习惯性加上的省得以后改题的时候忘。2.3 三条技术路线字符串、数学、格式化怎么选同一道题至少有三条路能走通选哪条取决于你想练什么。第一条是字符串反转把输入当字符串读进来倒着输出。优点是直观几乎不用动脑缺点是必须额外处理前导零否则120会变成021。第二条是数学取模用整除和取余把每一位抠出来重新按权值拼回去或者用循环“边取边拼”。这条路最贴近“数”的本质也是这道题真正想让你练的。第三条是格式化输出把三位分别取出来直接按逆序的格式打印。这条路最短但对位数固定的题才有效。解法代码量前导零处理通用性推荐场景字符串反转最短需手动处理强任意长度Python、脚本类语言数学取模循环中等天然正确强任意长度C、C、Java 等三位拆解格式化最短天然正确走整数弱只适用定长考试手写、快速验证我的建议是第一次做用拆解法务必把%和/的关系想透第二次做换成字符串法逼自己想清楚前导零为什么会冒出来第三次做换成循环法体会“算法与位数无关”带来的通用性。同一道题写三遍比刷三道新题收获大。3. 数位拆解的原理为什么 123 能变成 3213.1 拆位三件套/100、/10%10、%10到底在干什么先说清楚十进制数的结构。一个三位数n 100a 10b c其中a是百位、b是十位、c是个位。计算机不知道什么叫“位”它只知道整除和取余所以我们要用这两个运算把a、b、c反推出来。n % 10得到c因为对 10 取余会把十位及以上全部扔掉只留个位。n / 10得到10a b相当于把个位砍掉、整个数右移一位所以先除再取余n / 10 % 10就能拿到b。n / 100直接把后两位砍光剩下a。这三步的直觉是取余负责“留下低位”整除负责“丢掉低位”。理解了这一层四位数、五位数都是一回事只是多除几次。拿到a、b、c之后逆序的数就是100c 10b a。这就是“按权值重新拼装”。以123为例c3, b2, a1拼出来300 20 1 321。以120为例c0, b2, a1拼出来0 20 1 21。注意这里c是 0权值100c直接归零前导零自动消失了。这就是为什么拆解法走整数路径时天然不需要额外处理前导零——零在数值里本来就不占位。3.2 循环写法把“定长”这件事扔掉拆位三件套的毛病是它写死了三位。想让它通用就得用循环int rev 0; while (n 0) { rev rev * 10 n % 10; n / 10; }这个循环的逻辑值得慢放一遍。初始rev 0。第一轮把原来的个位取出来加到rev的个位位置上第二轮rev先乘 10 左移一位把上一轮的数字推到十位再把新的个位塞进去以此类推。对整个数来说效果就是每读一位就把已经攒下的结果整体左移让新来的数字占据最低位。这个过程和“把一叠牌一张张翻到另一只手”是一样的。拿120走一遍第一轮rev 0*10 0 0n 12第二轮rev 0*10 2 2n 1第三轮rev 2*10 1 21n 0循环结束。结果是21正确。再拿100走一遍第一轮rev 0n 10第二轮rev 0n 1第三轮rev 1n 0。结果1也正确——100逆序是001数值上就是1。注意这个循环的退出条件是n 0。如果输入可能为 0循环一次都不会执行结果就是 0恰好也是对的。但如果输入可能为负n 0会直接不进循环输出 0这就错了。遇到带负号的情形得单独取绝对值、记住符号最后再拼回去。3.3 字符串路线的雷021和21是两个东西用 Python 写一行int(input()[::-1])就能过。这里int()起了两个作用一是把字符串转成整数二是顺手把前导零吃掉——int(021)等于21。很多人正是因为这一行太顺反而没意识到前导零是个问题一换成 C 语言写字符串版本就翻车。C 语言的字符串版本要这么写读进char s[10]从后往前逐字符输出但要跳过末尾也就是反转后的开头的0。而且还要考虑极端情况如果输入是100反转后是001跳过前两个 0 之后还剩1没问题。但如果哪天输入允许是0反转后是0跳过前导零就什么都不剩了得补一个0的特判。这种“边界看着不会发生但换道题就发生了”的情况是字符串路线最大的隐患。还有一点常被忽略用printf(%c)逐字符输出的循环容易忘了最后没有换行导致下一组数据接着上一行。要求输出单个数的题目里看不出来一旦是多组数据就会连成一片。所以我写这类代码时宁可在末尾统一补一个换行也不省那一个字节。3.4 格式化输出法最短的路最窄的适用面还有一条更短的写法把三位拆出来直接按逆序打印。int a n / 100, b n / 10 % 10, c n % 10; printf(%d%d%d\n, c, b, a);等等这样写是错的。printf里的%d是十进制整数格式c为 0 时会输出0120就变成021了。得改成整体计算printf(%d\n, c * 100 b * 10 a);这才是对的。区别在于前一种是把三个数字当字符拼后一种是把它们当权值加起来。这个差别看着微不足道实际是整道题的核心——你到底是在操作“数字的字符串表示”还是在操作“数字本身”。想清楚这一点后面所有变体题都不容易错。4. 手把手实操四语言完整实现与自测记录4.1 C 语言版本从一行scanf开始把细节钉死#include stdio.h int main(void) { int n; if (scanf(%d, n) ! 1) { return 0; } int rev 0; while (n 0) { rev rev * 10 n % 10; n / 10; } printf(%d\n, rev); return 0; }逐行说。第一行包含头文件printf和scanf都在里面。int main(void)里的void写明不带参数是个好习惯。if (scanf(...) ! 1) return 0;这一行很多人会省但我强烈建议保留scanf返回成功读入的项数如果输入是空的或格式不对它返回 0此时n是个未初始化的垃圾值后续循环可能跑出莫名其妙的结果甚至被判题机记为运行时错误。加这一句等于给程序装了个保险丝。循环体就是前面讲的“边取边拼”。最后printf(%d\n, rev)输出结果并换行。整个程序没有一句提示语这是刻意的。编译的时候我习惯加几个开关gcc -stdc99 -Wall -Wextra -O2 -o reverse3 reverse3.c-Wall -Wextra把常见警告全打开-O2开优化。这道题用不上优化但养成开警告的习惯能提前发现“变量未使用”“类型不匹配”这类低级问题。我用-stdc99是因为有些平台用c89的标准//注释和循环内定义变量会出问题写之前确认一下平台的语言标准更稳妥。4.2 C、Python、Java 三个版本的差异点C 版本基本是把 C 的输入输出换掉#include iostream using namespace std; int main() { int n; if (!(cin n)) return 0; int rev 0; while (n 0) { rev rev * 10 n % 10; n / 10; } cout rev endl; return 0; }差异在于cin n失败时表达式的值为假所以写成if (!(cin n))。另外endl会刷新缓冲区题目规模小无所谓数据量大时用\n更省时间。Python 版本可以极简import sys s sys.stdin.read().strip().split()[0] print(int(s[::-1]))用sys.stdin.read()而不用input()是因为有些题目的输入末尾带多余空行input()在某些情况下可能读到空串。split()[0]取第一个 token然后把多余空白全部挡在门外。s[::-1]是切片反转int()顺手吃掉前导零。如果要保守一点也可以写print(int(input().strip()[::-1]))单组数据完全够用。Java 版本的关键是类名import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner sc new Scanner(System.in); if (!sc.hasNextInt()) return; int n sc.nextInt(); int rev 0; while (n 0) { rev rev * 10 n % 10; n / 10; } System.out.println(rev); } }多数判题机要求主类必须叫Main且不能有包声明这一条不知道会白白浪费好几次提交。另外Scanner比较慢这道题无所谓遇到大数据量要换成BufferedReader。4.3 自测用例表六个用例把边界全覆盖写完代码别急着交先在本地跑一遍下面这组用例。这张表是我自己常用的最小集合用例编号输入期望输出覆盖的边界1123321常规情况三位都非零212021个位为零考察前导零31001个位十位都为零最极端4101101逆序后与原数相同5999999上界全 962002中间位为零实测下来第 2、3、6 这三个用例是最容易挂的尤其是第 3 个。我见过有人用%03d输出前五个用例全过第 3 个输出001直接错。也见过有人用字符串拼接第 1 个用例过第 2 个输出021。跑测试的时候建议写个小脚本把输入输出重定向进去对比比手动敲快得多for t in 123 321 120 21 100 1 101 101 999 999 200 2; do in${t% *}; want${t#* } got$(echo $in | ./reverse3) [ $got $want ] echo PASS $in - $got || echo FAIL $in - $got (want $want) done这段脚本我几乎每道基础题都会改一改复用几秒钟跑完全部用例比在网页上点来点去省心。5. 常见问题与排查实录那些让人抓狂的坑5.1 输出类问题多一个字符就是零分多打了提示语。这是新手最高频的错误printf(请输入三位数)一出必挂。判题机不会跳过它它只会认为你的答案开头多了六个汉字。输出前导零。用了%03d或者字符串直接打印120变成021。判断标准很简单题面说“输出逆序后的数”是数就按整数格式输出。多输出空行。有人在printf后面又加了个printf(\n)或者循环里每轮都换行结果末尾多一两个空行。部分平台对此宽容部分不宽容别赌。大小写和标点。这道题不涉及但同类题目常要求输出Yes/No或带句号的句子照抄样例最稳。5.2 判题结果对照速查表判题结果常见原因排查顺序WA答案错误前导零未处理、提示语、结果算错先用120、100自测PE格式错误行末多余空格、多余空行检查所有输出语句RE运行错误数组越界、除零、scanf未校验检查数组长度与输入校验CE编译错误语言选错、类名不是Main、缺头文件看编译信息第一行TLE超时本题不可能除非死循环检查循环条件是否恒真这张表我贴在便签上用了很久。它最大的价值是看到 WA 就别去查编译问题看到 CE 就别去改算法。方向错了越努力越远。5.3 三个翻车现场都是我或身边人真实遇到过的现场一scanf没校验垃圾值进循环。某同学本地测试全过提交后随机挂。查了半天发现是他把n定义在函数里没初始化某次测试环境输入末尾多一个空行scanf失败n是栈上的随机数可能是个七位数循环多跑几轮结果错得离谱。加上if (scanf(...) ! 1) return 0;之后立刻稳定。现场二字符串反转后没去零。用 C 写字符串版本char s[10]; scanf(%s, s);然后倒着printf(%c)。123过、101过、120输出021挂。他自己测试只用了123觉得没问题。后来加了120用例才发现。教训是自测用例一定要包含带零的数。现场三Java 类名写成Reverse。本地 IDE 跑得好好的提交连续 CE 五次以为是代码问题改了半天逻辑。第六次才发现编译信息里写着“找不到主类”。这类错误纯粹是平台约定跟算法一点关系没有但代价是时间和心态。提示每次提交失败先花十秒看判题反馈的具体信息再决定改什么。凭感觉改代码是最浪费时间的排查方式。5.4 一份我自己用的提交前检查清单写完不着急交按这个顺序过一遍能省掉大部分无效提交输入有没有校验返回值异常输入会不会让程序跑飞循环退出条件对不对输入为 0 或 1 时会不会死循环结果的计算是走整数还是走字符有没有意外保留前导零所有输出语句里有没有多余的中文、空格、空行本地跑了几个用例有没有覆盖个位为零、全零结尾、回文这三类提交时选的语言和代码实际语言是否一致这个清单看着啰嗦实际过一遍不到一分钟。我用它挡掉了至少几十次无谓的 WA。6. 从 10 分题往外扩这道题还能怎么玩6.1 五个变体一次把数位操作练透这道题是个很好的跳板稍微改一改就是五道新题。变体一任意位数逆序。把输入范围放开到 1 到 10^9验证你写的是不是循环版本。这是检验“有没有真正理解算法通用性”的最快方式。变体二带负号的逆序。输入-123输出-321。要点是单独处理符号把绝对值送进循环最后再拼回负号。这一改就会发现while (n 0)这个条件在负数下直接失效必须重构。变体三判断回文数。逆序之后和原数比一比相等就是回文。121、1001是120不是。这个变体把“逆序”从终点变成了中间步骤。变体四逆序后是否溢出。如果输入范围放大到接近整型上限逆序后可能超出int的表示范围。这时候要换long long或者提前判断。这是从基础题通往工程思维的一道门。变体五位运算版逆序。把十进制换成二进制做 32 位反转。思路完全一样只是把/10和%10换成移位和掩码。做完这个再回头看十进制版本会有种“原来是一回事”的通透感。6.2 拿它当讲稿怎么把“数位”这个概念讲明白如果你要带新人这道题是讲“数位”概念的绝佳素材。我一般会先问一个问题123里的2凭什么代表二十答案是因为它站在十位上位置决定了权重。然后我会写123 1×100 2×10 3×1让大家看到“位”的本质是“权值”。接着问第二个问题怎么把2单独拿出来这时候引出%和/。123 % 10 3因为对 10 取余就是把 10 的整数倍全部扔掉123 / 10 12因为整除就是把不足 10 的部分扔掉。两个操作一个留低位一个丢低位配合起来就能从右往左一位位剥。最后问第三个问题为什么120逆序之后不是021这时引出“字符串表示”和“数值”的区别。021作为字符串有三个字符作为数值只有一个21。题面说的是“数”所以按数值处理。这一路问下来比直接讲代码有效得多。6.3 训练方法用 10 分题练出工程习惯我的建议是每道这样的基础题写三个版本。第一版叫能过版目标只有一个跑通样例。写完就交看看结果。第二版叫边界版专门针对个位为零、全零结尾、最小值、最大值这几类情况改代码然后自己造用例验证。第三版叫复用版把逆序逻辑抽成一个函数int reverse_int(int n)加上参数校验和注释扔进自己的小工具库。以后遇到回文判断、数字翻转、进制转换直接调用。三个版本写下来同一道题花的时间大概是别人的三倍但收获也差不多是三倍。我自己的工具库里就躺着这么一个reverse_int后来在好几个题目里省了事。基础题的价值不在于它本身有多难而在于它是你反复打磨“手感”的最便宜的材料。我个人的体会是越是被标注为 10 分的题越值得多花十分钟。因为难的题错了你会去查资料、会去调试、会记住简单的题错了你往往只会骂一句“手滑”然后下次继续手滑。真正拉开差距的从来不是那些让人挠头的难题而是这些看着一眼就会、实则处处埋雷的小题。下次再遇到“逆序的三位数”别急着点提交先把120和100敲进输入框里跑一遍。
阅读完成 · 觉得有帮助?