
1. 从NOJ前100题说起这套题到底在练什么如果你在西工大读工科大概率绕不开NOJ这个平台。C程序设计这门课很多专业大一就开而NOJ上的前100题基本就是配合课堂进度的一套“手速思维”训练集。我带过几届学弟学妹刷这套题发现一个很普遍的现象很多人把题做完提交通过就完事了结果期末上机考试换个壳子就不会写。问题出在——他们只把NOJ当成了“交作业的地方”没把它当成“练基本功的道场”。这套前100题表面上看是100道零散的编程练习实际上它的编排是有内在逻辑的。从最基础的输入输出、四则运算到分支判断、循环嵌套再到数组、字符串、函数、指针、结构体最后收在递归和简单算法上。它几乎覆盖了C语言程序设计的全部核心语法点而且每道题都对应一个具体的“应用场景”——比如求圆的面积、判断闰年、打印乘法口诀表、统计字符个数、矩阵转置、冒泡排序等等。这些场景不是随便编的它们都是后续专业课数据结构、算法、操作系统里反复出现的基本操作单元。所以刷这套题的正确姿势不是“刷完”而是“刷透”。什么叫刷透就是每做完一道题你能说清楚三件事这道题考的是哪个语法点我用的解法是不是最合理的如果题目条件变一下我还能不能写出来举个例子NOJ上有一道经典的“计算分段函数”的题很多人用if-else堆出来就交了。但如果你多想一步这个分段函数能不能用条件运算符写得更简洁如果分段点从两个变成五个我的代码结构还撑得住吗这种思考才是这套题真正的价值所在。另外要提醒一点NOJ的评测机制是黑盒测试它只看你的输出结果对不对不看你代码写得漂不漂亮。这就导致很多人养成了“能过就行”的坏习惯——变量名用a、b、c缩进乱七八糟注释一句不写。短期看没问题但到了期末上机考或者后续做课程设计的时候这种代码风格会让你吃大亏。我的建议是从第一题开始就按规范来变量名要有意义该缩进就缩进关键逻辑加一行注释。这个习惯一旦养成后面受益无穷。2. 前30题输入输出与表达式里的隐藏考点2.1 为什么“格式错误”比“答案错误”更让人抓狂NOJ前30题里绝大多数是输入输出和简单表达式计算。很多新手第一次遇到“Presentation Error”或者“Wrong Answer”的时候第一反应是“我算的对啊”。没错你算的是对的但输出的格式不对。比如题目要求输出“sum 15”你只输出了“15”或者多了一个空格、少了一个换行评测机都会判错。我见过最典型的一个坑题目要求“输出保留两位小数”有人用了printf(%f, x)结果输出的是6位小数直接WA。还有人用了printf(%.2f, x)但题目要求的是“四舍五入到两位”而C语言的%.2f默认是“四舍六入五成双”的银行家舍入规则在某些边界值上会和题目预期不一致。这种细节你不踩一次坑根本不会注意到。提示NOJ的评测机对输出格式极其敏感。提交之前务必把题目里的“输出格式”那一行逐字读一遍包括标点符号、空格数量、换行位置。一个实用的技巧是把题目要求的输出样例复制到记事本里用“显示不可见字符”功能看一眼确认空格和换行的位置。2.2 整数除法和浮点数除法的经典陷阱前30题里有一类题专门考“除法”。比如“输入两个整数输出它们的商”。很多人随手写a / b然后发现结果不对——因为C语言里两个整数相除结果会自动截断成整数。比如5 / 2得到的是2不是2.5。要得到浮点结果必须写成(double)a / b或者a * 1.0 / b。这个知识点看起来简单但每年都有大量的人在NOJ上栽跟头。更隐蔽的是“整数溢出”问题。比如题目要求计算“1到100的和”你用int类型没问题。但如果题目要求计算“1到100000的和”int就可能溢出需要用long long。NOJ上有一道题是“计算阶乘”输入20输出20的阶乘。20的阶乘是2432902008176640000这个数已经超过了int的范围约21亿必须用long long才能存下。很多人不知道这一点结果输出一个负数百思不得其解。2.3 字符输入里的“回车陷阱”前30题里还有一类题涉及字符输入。比如“输入一个字符输出它的ASCII码”。很多人用scanf(%c, c)读字符然后紧接着用scanf(%c, d)读下一个字符结果发现第二个字符读进来的是回车符。这是因为第一次输入后按了回车回车符留在了输入缓冲区里被第二次scanf读走了。解决这个问题有几种办法。第一种是在两次scanf之间加一个getchar()把回车吃掉。第二种是用scanf( %c, c)注意%c前面加一个空格这个空格会跳过所有的空白字符包括回车、空格、制表符。第三种是干脆用gets()或者fgets()读一整行然后自己解析。我个人推荐第二种写起来最简洁也不容易出错。注意gets()函数因为不安全不检查缓冲区长度已经在C11标准里被移除了NOJ的编译器可能还支持但强烈建议不要用。用fgets()替代虽然要多写一个参数但安全得多。3. 中间40题循环、数组与字符串的硬骨头3.1 循环边界从“差一错误”到“死循环”从第31题开始NOJ的难度明显上来了循环结构成为主角。循环里最容易犯的错误就是“差一错误”off-by-one error。比如题目要求“输出1到100之间所有能被3整除的数”你写for (i 1; i 100; i)结果漏掉了100。或者写for (i 1; i 100; i)但循环体里又写了个i导致跳过了某些数。还有一种更隐蔽的差一错误出现在数组遍历里。C语言的数组下标从0开始所以一个长度为n的数组有效下标是0到n-1。很多人写for (i 0; i n; i)访问了a[n]这是越界访问。在NOJ上越界访问可能不会立刻报错但会读到垃圾值导致结果莫名其妙地不对。更严重的是如果越界写入了数据可能会覆盖其他变量的值造成“灵异现象”——比如你明明没改变量x但x的值自己变了。死循环是另一个高频问题。最常见的是while循环里忘了更新循环变量。比如“输入若干个数求平均值直到输入-1为止”有人写while (x ! -1) { sum x; count; }但忘了在循环体里再读一次x结果x永远是第一个数循环永远不结束。NOJ对死循环的处理是“超时”Time Limit Exceeded你会看到评测结果里显示TLE。3.2 数组从一维到二维的思维跃迁数组是C语言里第一个真正意义上的“数据结构”。NOJ中间部分的题目很多都在训练“用数组批量处理数据”的思维。比如“输入10个数逆序输出”你需要先把10个数存进数组然后从后往前遍历输出。再比如“输入一个矩阵求它的转置”你需要用二维数组然后交换a[i][j]和a[j][i]。这里有一个很关键的思维转变以前你处理数据是一个一个来的现在你要把数据“存起来”然后“批量操作”。这个转变对后续学习数据结构至关重要。我建议在做这类题的时候刻意练习“先存后算”的模式——不管题目要不要你存你都先存进数组然后再想怎么算。这样做的好处是你的代码结构会更清晰也更容易调试。二维数组的初始化是个容易踩坑的地方。比如int a[3][4] {{1,2},{3,4}}你以为所有元素都初始化了实际上只有前两行的前两个元素被初始化了其余元素都是0。如果你想要全部初始化为0可以写int a[3][4] {0}这样所有元素都会是0。这个技巧在NOJ的矩阵类题目里非常实用。3.3 字符串C语言里最“反直觉”的部分字符串是C语言里最让人头疼的部分没有之一。NOJ上关于字符串的题目错误率普遍偏高。核心原因在于C语言没有原生的字符串类型字符串就是一个以\0结尾的字符数组。这个\0是很多问题的根源。比如“输入一个字符串统计其中字母的个数”。你用scanf(%s, str)读入字符串然后遍历。但scanf(%s)遇到空格就会停止读取所以如果输入是“hello world”你只能读到“hello”。要读一整行需要用fgets(str, sizeof(str), stdin)。但fgets会把换行符也读进来所以字符串末尾可能是\n而不是\0你需要手动把\n替换成\0。再比如字符串比较。很多人用if (str1 str2)来判断两个字符串是否相等这是错的。str1和str2是数组名它们代表的是数组的首地址比较的是地址而不是内容。正确的做法是用strcmp(str1, str2)返回0表示相等。这个坑几乎每个人都会踩一次。还有一个经典问题是“字符串拼接”。有人写str1 str1 str2编译器直接报错。因为数组名不是左值不能被赋值。正确的做法是用strcat(str1, str2)但前提是str1必须有足够的空间容纳拼接后的结果。如果str1定义的是char str1[10]而str2有20个字符strcat会导致缓冲区溢出程序可能崩溃或者产生不可预期的结果。提示处理字符串时永远要问自己三个问题这个字符串有多长存储它的数组够不够大它有没有以\0结尾这三个问题能帮你避开80%的字符串相关bug。4. 后30题函数、指针与递归的进阶修炼4.1 函数从“一坨代码”到“模块化思维”NOJ后30题开始要求你把代码拆成函数。比如“写一个函数判断一个数是不是素数然后在主函数里调用它输出1到100之间的所有素数”。这种题目的价值不在于语法而在于“模块化思维”——把一个大问题拆成若干个小问题每个小问题用一个函数解决。很多人写函数的时候喜欢把所有的逻辑都塞进main里然后复制粘贴。这样做短期看省事但一旦题目变复杂代码就会变得不可维护。我建议从后30题开始强制自己“一个功能一个函数”。比如判断素数写一个isPrime求最大公约数写一个gcd交换两个变量写一个swap。这样做的好处是你的main函数会变得非常简洁一眼就能看出程序的逻辑流程。函数还有一个容易踩的坑是“值传递”和“地址传递”的区别。C语言默认是值传递也就是说你把一个变量传给函数函数里修改的是这个变量的副本不会影响原来的变量。如果你想让函数修改原来的变量必须传地址也就是指针。比如经典的swap函数void swap(int *a, int *b) { int temp *a; *a *b; *b temp; }调用的时候要写swap(x, y)。如果你写成swap(x, y)编译器可能不报错取决于参数类型但交换不会生效。这个坑在NOJ的排序类题目里特别常见。4.2 指针C语言的“分水岭”指针是C语言里最让人望而生畏的部分也是NOJ后30题的重点。很多人学到这里就放弃了觉得指针太抽象。但其实指针的核心概念只有一个它存储的是地址。你只要记住“指针就是地址”很多问题就迎刃而解了。比如“输入两个字符串用指针交换它们的内容”。如果你用数组来交换需要逐个字符复制很麻烦。但用指针就很简单定义两个指针分别指向两个字符串然后交换指针的值。这样两个指针就互换了指向也就相当于交换了字符串的内容。这个技巧在NOJ的字符串排序题里非常有用。指针和数组的关系也是必须搞清楚的。在C语言里数组名在大多数情况下会“退化”成指向数组第一个元素的指针。所以a[i]等价于*(a i)a[i]等价于a i。这个等价关系在NOJ的题目里经常用到。比如“用指针遍历数组并输出”你可以写int *p; for (p a; p a n; p) { printf(%d , *p); }这种写法比用下标更“C语言”也更高效虽然现代编译器优化后差别不大。4.3 递归从“自己调用自己”到“分而治之”递归是NOJ后30题的另一个重点。经典的递归题目包括“求阶乘”“求斐波那契数列”“汉诺塔”“全排列”等。递归的核心思想是“分而治之”——把一个大问题拆成一个或多个小问题小问题的解法和大问题一样只是规模更小。写递归函数的关键是找到两个东西递归基什么时候停止和递归式怎么把问题变小。比如求阶乘int factorial(int n) { if (n 0 || n 1) return 1; // 递归基 return n * factorial(n - 1); // 递归式 }递归基是n 0 || n 1递归式是n * factorial(n - 1)。如果你忘了写递归基函数会无限调用自己直到栈溢出Stack Overflow程序崩溃。递归的另一个坑是“重复计算”。比如斐波那契数列的朴素递归写法int fib(int n) { if (n 1) return n; return fib(n - 1) fib(n - 2); }这个写法的时间复杂度是指数级的算到fib(40)就要好几秒算到fib(50)可能要几分钟。NOJ上如果遇到这种题用朴素递归大概率会超时。解决办法是用“记忆化搜索”——用一个数组把已经算过的结果存起来下次需要的时候直接取不再重复计算。这个技巧在NOJ的递归类题目里非常实用。5. 刷题之外那些NOJ不会教你的实战经验5.1 调试技巧从“printf大法”到“断点调试”NOJ上做题最痛苦的事情莫过于“答案不对但不知道错在哪”。很多人只会用printf在代码里到处打印中间变量这种方法在代码短的时候还行代码一长就变成了“打印地狱”。我建议尽早学会用调试器。如果你用的是Dev-C可以在代码里设置断点点击行号旁边的空白区域然后按F5进入调试模式按F7单步执行观察变量的值。如果你用的是VS Code配置好C/C调试环境后体验会更好。调试器的好处是你可以“暂停”程序的执行查看当前所有变量的值而不是靠猜。当然printf大法也不是完全没用。在NOJ这种在线评测环境里你没法用调试器只能靠printf。我的经验是把printf语句写得有辨识度比如printf(DEBUG: i%d, sum%d\n, i, sum)这样一眼就能看出是调试信息提交之前也容易删干净。5.2 代码风格从“能跑就行”到“可读可维护”前面提到过NOJ只看结果不看代码风格。但我想强调的是代码风格是你自己的事不是给评测机看的。好的代码风格能让你在调试的时候更快找到问题在复习的时候更快回忆起思路在给别人看的时候更快传达意图。具体来说我建议做到以下几点变量名用英文单词或缩写不要用a、b、c缩进用4个空格或者一个Tab保持一致每个函数前面写一行注释说明功能复杂的逻辑块前面写一行注释说明意图不要在一行里写太多东西适当换行。这些习惯看起来微不足道但坚持一个学期你的代码质量会有质的飞跃。5.3 从NOJ到真实项目还差什么NOJ的题目都是“单文件、单函数、标准输入输出”的模式。但真实的C语言项目要复杂得多多个源文件、头文件、Makefile、版本控制、单元测试、内存管理、错误处理等等。NOJ不会教你这些但你可以自己补。比如你可以尝试把NOJ上做过的题目整理成一个“题库项目”每个题目一个.c文件然后写一个Makefile来批量编译。再比如你可以尝试用git来管理你的代码每次做完一道题就提交一次这样你就能看到自己的进步轨迹。这些额外的练习会让你在后续的课程设计和毕业设计中游刃有余。提示NOJ上有一道题是“大数相加”要求计算两个超过long long范围的整数的和。这道题的标准解法是用字符串模拟手工加法。这个思路在真实项目里非常有用——当你遇到语言原生类型不够用的时候就需要自己设计数据结构来解决问题。这种“造轮子”的能力是区分普通程序员和优秀程序员的关键。6. 关于“更新中”这件事如何持续跟进和利用这套题6.1 题目会变知识点不会变标题里写着“更新中”说明这套题可能还在不断补充。但不管题目怎么变C语言的核心知识点是固定的数据类型、运算符、控制流、函数、数组、指针、结构体、文件操作。你只要把这些核心知识点吃透了不管题目怎么变你都能应对。我的建议是不要追求“刷完100题”而是追求“每做一题都有收获”。做完一道题后花几分钟想一想这道题考了什么知识点我用的方法是不是最优的有没有更简洁的写法如果题目条件变一下我还能不能做出来这种“复盘”的习惯比刷题数量重要得多。6.2 建立自己的“错题本”NOJ上做错的题不要改完就扔。建一个文档把错题的题目描述、你的错误代码、错误原因、正确代码、知识点总结都记下来。每隔一段时间翻一翻你会发现自己的薄弱环节在哪里。比如你可能发现自己在“字符串处理”上错得最多那就专门找字符串的题目来练。这个错题本还有一个好处期末复习的时候你不需要把100题重新做一遍只需要看错题本就行了。我当年就是用这个方法期末上机考试前只花了两个小时复习就拿到了满分。6.3 从“抄答案”到“读答案”网上有很多NOJ的答案很多人遇到不会的题就直接搜答案复制粘贴。这样做短期看省时间但长期看是害了自己。我的建议是遇到不会的题先自己想15分钟。如果实在想不出来再看答案。但看答案的时候不要只看代码要看思路——它是怎么分析问题的用了什么数据结构为什么这么设计看完之后把答案关掉自己重新写一遍。如果能写出来说明你真的懂了如果写不出来说明你只是“看懂了”并没有“学会”。注意NOJ的题目每年可能会有微调网上搜到的答案不一定完全适用。而且直接抄答案有被查重系统标记的风险。自己写哪怕写得丑一点也是自己的东西。6.4 组队刷题一个人走得快一群人走得远如果你觉得一个人刷题太枯燥可以找几个同学组队。每个人负责不同的题目做完之后互相讲解。讲解的过程就是最好的复习——你要把思路说清楚别人才能听懂。而且不同的人有不同的解法互相交流能开阔思路。比如一道排序题你可能用冒泡排序别人可能用选择排序还有人可能用qsort函数。这些不同的解法都是宝贵的学习资源。我在带学弟学妹的时候经常组织“NOJ专题讨论”一次讨论一个主题比如“字符串处理专题”“递归专题”“指针专题”。每次讨论完大家都觉得收获很大。这种学习方式比一个人闷头刷题效率高得多。7. 一些具体的题目解析与代码示范7.1 经典题判断素数并输出这道题在NOJ上出现在函数部分。题目要求写一个函数isPrime判断一个数是不是素数然后在main里输出1到100之间的所有素数。#include stdio.h #include math.h int isPrime(int n) { if (n 1) return 0; for (int i 2; i sqrt(n); i) { if (n % i 0) return 0; } return 1; } int main() { for (int i 1; i 100; i) { if (isPrime(i)) { printf(%d , i); } } printf(\n); return 0; }这道题的关键点有两个一是isPrime函数里循环的上界是sqrt(n)而不是n这样可以把时间复杂度从O(n)降到O(sqrt(n))二是main函数里只负责调用和输出逻辑非常清晰。很多人把判断素数的逻辑直接写在main里代码就会显得很乱。7.2 经典题字符串逆序题目要求输入一个字符串将其逆序输出。#include stdio.h #include string.h int main() { char str[100]; fgets(str, sizeof(str), stdin); int len strlen(str); // 去掉fgets读入的换行符 if (len 0 str[len - 1] \n) { str[len - 1] \0; len--; } for (int i len - 1; i 0; i--) { printf(%c, str[i]); } printf(\n); return 0; }这道题的坑在于fgets会把换行符也读进来。如果你不处理逆序输出的第一个字符就是换行符结果就会多一个空行。处理方法是检查最后一个字符是不是\n如果是就把它替换成\0同时把长度减1。7.3 经典题递归求斐波那契数列题目要求用递归求斐波那契数列的第n项。#include stdio.h long long memo[100] {0}; long long fib(int n) { if (n 1) return n; if (memo[n] ! 0) return memo[n]; memo[n] fib(n - 1) fib(n - 2); return memo[n]; } int main() { int n; scanf(%d, n); printf(%lld\n, fib(n)); return 0; }这道题的关键是“记忆化搜索”。如果不加memo数组算到fib(50)就会超时。加了memo之后每个fib(n)只算一次时间复杂度降到O(n)。这个技巧在NOJ的递归类题目里非常实用建议熟练掌握。8. 最后聊几句实在的刷NOJ这套题最忌讳的就是“为了刷而刷”。我见过太多人题目通过之后连自己写了什么都不记得。这样的刷题除了浪费时间没有任何意义。真正有效的刷题是每做一道题都有思考、有总结、有收获。另外不要被“100题”这个数字吓到。你不需要一次性刷完也不需要按顺序刷。可以根据自己的薄弱环节有针对性地选择题目来练。比如你觉得指针不熟就专门找指针的题目你觉得递归太难就专门找递归的题目。集中突破比漫无目的地刷题效率高得多。还有一点很重要不要怕犯错。NOJ的好处就是你可以随便提交错了就改改了再交没有任何成本。这种“试错”的机会在真实项目里是没有的。所以大胆地写大胆地交从错误中学习这才是NOJ最大的价值。我在实际带人的过程中发现那些最终学得好的同学往往不是最聪明的而是最愿意“折腾”的。他们会尝试不同的解法会去查资料搞懂每一个细节会把自己的代码反复重构。这种“折腾”的精神才是学好编程的关键。NOJ只是一个工具怎么用好这个工具取决于你自己。