C语言OJ刷题精讲:1031-1040题实战与编程思维训练

📅 2026/7/24 16:17:10
C语言OJ刷题精讲:1031-1040题实战与编程思维训练
1. 项目概述从OJ平台到编程思维的跨越最近在整理自己带学生做编程练习的资料翻到了当年在ZZNUOJ上刷题的记录特别是1031到1040这十道题。这十道题表面上看是C/C的基础练习但在我看来它们更像是一把钥匙一把能帮你打开“编程思维”这扇大门的钥匙。很多新手朋友一上来就想搞大项目学框架结果往往在基础语法和逻辑上栽跟头写出来的代码要么跑不通要么逻辑混乱。这十道题恰恰就是帮你夯实基础、理顺逻辑的绝佳材料。它们覆盖了循环控制、数组应用、字符处理、简单数学运算等核心知识点每一道题都像是一个精心设计的小关卡让你在解决问题的过程中不知不觉地掌握那些教科书上枯燥的概念。无论你是刚接触C语言的大一新生还是想巩固基础的转行开发者静下心来把这十道题吃透比你漫无目的地看十个小时视频教程要管用得多。2. 核心需求解析为什么是这十道题在开始逐题拆解之前我们得先弄明白练习这十道题到底是为了满足什么需求或者说通过它们我们能获得哪些在其他地方难以系统训练的能力我总结下来主要有以下三点核心需求。2.1 需求一建立清晰的程序执行流概念对于初学者而言最大的障碍往往不是语法而是对“程序是如何一步步跑起来的”缺乏直观感受。比如什么时候该用for循环什么时候用whileif-else的嵌套到底是怎么判断的这十道题尤其是涉及多重循环和条件判断的题目强迫你去梳理每一步的执行顺序。你在脑子里或者纸上画出的流程图就是你对程序执行流理解的外化。这个过程是培养计算思维的第一步也是最重要的一步。2.2 需求二掌握基础数据结构的初步应用这里的“数据结构”不是指链表、树那些高级货而是最基础的整型、浮点型、字符和数组。很多同学知道int a可以存一个整数但面对“用数组存储并处理10个学生的成绩”这样的问题时就不知道如何下手了。这十道题里你会反复和数组打交道学习如何遍历数组元素、如何通过下标访问和修改数据。这是你今后学习更复杂数据结构如字符串、向量、链表的基石。把一维数组用熟了后面学二维数组、字符数组字符串就会轻松很多。2.3 需求三训练将实际问题抽象为代码的能力这是编程的终极目标。题目描述的是一个现实问题或数学问题你需要用代码语言来描述解决步骤。例如“判断一个数是否为素数”就是一个典型的抽象过程。你需要将素数的定义除了1和自身没有其他因数转化为循环判断的逻辑。这十道题提供了不同复杂度的问题模型让你由浅入深地练习这种“翻译”能力。一开始可能很吃力需要反复调试但当你独立完成几道后会发现这种思维模式慢慢就形成了。3. 环境准备与工具链选择工欲善其事必先利其器。虽然OJ平台在线判题系统会帮你编译和运行代码但在本地有一个顺手的开发环境对于练习、调试和理解错误至关重要。这里我分享一套经过多年教学验证的、对新手极其友好的工具组合。3.1 编辑器与编译器告别臃肿拥抱轻量我不建议初学者一上来就安装完整的Visual Studio或其它大型IDE。它们功能强大但过于复杂容易让人分散注意力。我的推荐组合是VSCode MinGW-w64。VSCode微软出品的免费开源编辑器轻量、插件丰富、颜值高。通过安装C/C插件它能提供代码高亮、智能提示IntelliSense、格式化等基础功能完全够用。MinGW-w64一个Windows下的GCC编译器套件。GCC是行业标准的C/C编译器在Linux/macOS上原生存在用MinGW-w64可以让Windows用户获得接近Linux的开发体验。最重要的是它体积小配置简单。注意网络上有很多所谓的“C盘清理工具”或“CMD清理命令”声称能释放空间。对于开发环境绝对不要随意删除C:\Users\[你的用户名]\.vscode或C:\Users\[你的用户名]\AppData\Local\Temp下的文件这可能导致插件配置丢失或编译临时文件被误删引发奇怪的问题。清理空间应该针对的是浏览器缓存、下载目录或系统更新备份开发环境相关目录请谨慎操作。配置步骤简述下载并安装MinGW-w64记得将bin目录例如C:\mingw64\bin添加到系统的PATH环境变量中。安装VSCode并在扩展商店搜索安装“C/C”扩展由Microsoft发布。打开一个文件夹作为工作区VSCode会自动提示你配置tasks.json构建任务和launch.json调试配置。对于简单练习你甚至可以直接在终端里用gcc -o program program.c和./program来编译运行。3.2 调试技巧从“猜错”到“知错”只会写代码不会调试就像开车不看仪表盘。VSCode配合GCC可以很方便地进行调试。编译时加入调试信息使用gcc -g -o program program.c命令编译-g参数会在可执行文件中加入源代码信息这样调试器才能将机器指令和你写的代码对应起来。善用printf大法这是最朴素也是最有效的调试手段。在你觉得可能出问题的变量变化处、循环内部、函数调用前后打印出关键变量的值。这能让你清晰地看到程序的执行轨迹和数据流。使用调试器在VSCode中设置断点点击行号左侧然后按F5启动调试。你可以看到所有变量的当前值可以单步执行F10步入函数F11这对于理解复杂的循环和函数调用栈非常有帮助。很多同学遇到“段错误Segmentation Fault”或“运行时错误”就懵了。其实大部分情况都是数组越界访问了a[10]但数组大小是10有效下标是0-9、访问空指针或者整数除零造成的。通过调试你可以快速定位到出错的那一行代码。4. 题目1031-1040逐题精讲与避坑指南接下来我们进入正题对这十道题进行逐一拆解。我不会仅仅给出答案而是重点分析解题思路、常见的错误写法以及如何写出健壮、易读的代码。4.1 1031题求整数的位数及其逆序数题目核心输入一个整数输出其位数并输出其逆序数。思路拆解求位数在C语言中对于整数n我们可以通过不断除以10直到商为0的方法来计算位数。每次除法位数加一。这里要特别注意处理n0的情况0的位数是1。求逆序数在求位数的循环过程中我们可以同步构造逆序数。公式为reverse reverse * 10 n % 10。每次取出n的个位n%10加到已有的逆序数后面。常见坑点与代码实现#include stdio.h int main() { int n, original, digit 0, reverse 0; scanf(%d, n); original n; // 保存原始值因为n在循环中会被改变 // 处理负数先转为正数处理最后在输出或计算时考虑负号 if (n 0) n -n; // 处理0的情况 if (n 0) { digit 1; reverse 0; } else { while (n 0) { digit; reverse reverse * 10 n % 10; n / 10; } } printf(位数: %d\n, digit); printf(逆序数: %d\n, original 0 ? -reverse : reverse); // 如果原数是负数逆序数也为负 return 0; }实操心得scanf读取整数后如果直接修改n原始数据就丢失了。对于后续可能需要用到原始值的场景比如这里判断正负一定要先存到一个临时变量里。这是一个非常好的编程习惯。4.2 1032题判断素数题目核心输入一个正整数判断是否为素数。思路拆解素数的定义是大于1的自然数且除了1和自身外没有其他因数。最直接的判断方法是从2遍历到这个数的平方根sqrt(n)。因为如果n有一个大于sqrt(n)的因数a那么必定有一个小于sqrt(n)的因数ba*bn所以检查到平方根就足够了。算法优化与边界处理#include stdio.h #include math.h // 使用sqrt函数需要包含此头文件 int main() { int n, i, isPrime 1; // 先假设是素数 scanf(%d, n); if (n 1) { // 1和负数、0都不是素数 isPrime 0; } else { // 循环上限取sqrt(n)效率更高。注意sqrt返回double需要强转为int并且为了保险循环条件用 i*i n for (i 2; i * i n; i) { if (n % i 0) { isPrime 0; break; // 找到一个因数就足以证明不是素数立即跳出循环 } } } if (isPrime) { printf(是素数\n); } else { printf(不是素数\n); } return 0; }避坑指南很多新手会写for(i2; in; i)对于小的n没问题但当n很大时比如10^9这个循环次数是灾难性的。使用i*i n可以将循环次数从O(n)降到O(sqrt(n))这是算法中非常重要的优化思想。另外务必处理好n1的边界情况。4.3 1033题斐波那契数列第N项题目核心输出斐波那契数列的第N项通常N不会太大。思路拆解斐波那契数列定义为F(1)1, F(2)1, F(n)F(n-1)F(n-2) (n3)。这是一个经典的递归或迭代问题。迭代法与递归法的抉择#include stdio.h // 方法一迭代法推荐效率高 long long fibonacci_iterative(int n) { if (n 2) return 1; long long a 1, b 1, c; for (int i 3; i n; i) { c a b; a b; b c; } return b; } // 方法二递归法直观但效率低仅用于理解概念 long long fibonacci_recursive(int n) { if (n 2) return 1; return fibonacci_recursive(n-1) fibonacci_recursive(n-2); } int main() { int n; scanf(%d, n); // 使用迭代法 printf(第%d项是%lld\n, n, fibonacci_iterative(n)); return 0; }核心要点绝对不要在OJ题目中用递归法求较大的斐波那契数比如N40。递归会产生大量重复计算时间复杂度是O(2^n)极其缓慢。迭代法的时间复杂度是O(n)空间复杂度是O(1)是此类问题的标准解法。同时注意使用long long类型因为斐波那契数增长很快int类型可能溢出。4.4 1034题数字菱形图案题目核心根据输入的数字n打印一个数字菱形。思路拆解这类图形题是理解循环控制的绝佳练习。通常将菱形分为上下两部分上半部分包括中间行。上半部分行号i从1到n。每行先打印空格数量为n-i然后打印数字数字从1递增到i再递减回1。下半部分行号i从n-1到1。逻辑与上半部分对称。代码实现与格式化技巧#include stdio.h int main() { int n, i, j; scanf(%d, n); // 上半部分包括中间行 for (i 1; i n; i) { // 打印空格 for (j 1; j n - i; j) { printf( ); } // 打印递增数字 for (j 1; j i; j) { printf(%d, j); } // 打印递减数字 for (j i - 1; j 1; j--) { printf(%d, j); } printf(\n); // 换行 } // 下半部分 for (i n - 1; i 1; i--) { for (j 1; j n - i; j) { printf( ); } for (j 1; j i; j) { printf(%d, j); } for (j i - 1; j 1; j--) { printf(%d, j); } printf(\n); } return 0; }调试技巧如果图形不对可以先注释掉打印数字的部分只打印空格和换行看看空格的布局是否正确。图形题的调试核心就是分离关注点先确保框架空格正确再填充内容数字。4.5 1035题数组元素逆置题目核心输入N个整数存入数组将其逆序存放后再输出。思路拆解这是数组操作的基本功。逆置的核心是交换。定义两个下标一个从头部i0开始一个从尾部jN-1开始交换它们指向的元素然后ij--直到i j。原地交换与双指针思想#include stdio.h int main() { int n, i, temp; scanf(%d, n); int arr[100]; // 假设N不超过100根据题目要求调整 // 读入数组 for (i 0; i n; i) { scanf(%d, arr[i]); } // 双指针法逆置 int left 0, right n - 1; while (left right) { // 交换arr[left]和arr[right] temp arr[left]; arr[left] arr[right]; arr[right] temp; left; right--; } // 输出逆置后的数组 for (i 0; i n; i) { printf(%d , arr[i]); } printf(\n); return 0; }注意事项while循环的条件是left right而不是left right。当元素个数为偶数时两者相遇前就交换完毕当为奇数时最中间的元素不需要和自己交换。这是双指针操作中的经典边界条件。4.6 1036题矩阵转置题目核心输入一个MN的矩阵输出其转置矩阵NM。思路拆解转置操作就是将原矩阵a[i][j]的元素放到新矩阵b[j][i]的位置上。关键在于理解二维数组的下标含义第一个下标是行号第二个下标是列号。二维数组的输入输出与转置逻辑#include stdio.h int main() { int m, n, i, j; scanf(%d %d, m, n); int matrix[10][10]; // 根据题目最大范围定义 int transpose[10][10]; // 读入原矩阵 for (i 0; i m; i) { for (j 0; j n; j) { scanf(%d, matrix[i][j]); } } // 转置原矩阵的(i,j)元素 - 新矩阵的(j,i)元素 for (i 0; i m; i) { for (j 0; j n; j) { transpose[j][i] matrix[i][j]; } } // 输出转置矩阵 (n行 m列) for (i 0; i n; i) { for (j 0; j m; j) { printf(%d , transpose[i][j]); } printf(\n); // 每行输出后换行 } return 0; }内存布局理解在C语言中二维数组在内存中是按行连续存放的。matrix[0][0]后面紧跟着matrix[0][1]而不是matrix[1][0]。理解这一点对于后续学习指针和动态内存分配非常重要。转置操作改变了这种访问顺序。4.7 1037题字符串统计字母、数字、其他字符题目核心输入一行字符可能包含空格统计其中英文字母、数字、空格和其他字符的个数。思路拆解C语言中字符串以字符数组形式存储以\0结尾。我们需要逐个读取字符直到换行符\n并对每个字符进行分类。分类依据是字符的ASCII码范围或使用C标准库函数isalpha(),isdigit(),isspace()需要#include ctype.h。两种实现方式的对比#include stdio.h #include ctype.h // 使用字符分类函数 int main() { char str[1000]; int letters 0, digits 0, spaces 0, others 0; int i 0; // 方法一使用fgets读取整行包括空格 fgets(str, sizeof(str), stdin); // 遍历字符串直到遇到字符串结束符\0 while (str[i] ! \0) { if (isalpha(str[i])) { letters; } else if (isdigit(str[i])) { digits; } else if (isspace(str[i])) { // isspace 包括空格、\t, \n等 spaces; } else { others; } i; } // 注意fgets会把换行符\n也读进来如果不需要可以在这里减去 if (i 0 str[i-1] \n) { spaces--; // 将换行符从spaces计数中扣除根据题目要求调整 } printf(字母:%d 数字:%d 空格:%d 其他:%d\n, letters, digits, spaces, others); return 0; }关键细节scanf(“%s”, str)无法读取带空格的字符串遇到空格就停止了。所以处理包含空格的字符串必须使用fgets。另外fgets会读取并存储换行符\n在统计时需要考虑是否将其计入“空格”或其他类别根据题目要求做调整。使用ctype.h中的函数比手动判断ASCII码范围更安全、可读性更好。4.8 1038题求最大公约数与最小公倍数题目核心输入两个正整数求其最大公约数GCD和最小公倍数LCM。思路拆解最大公约数常用辗转相除法欧几里得算法。原理是gcd(a, b) gcd(b, a % b)直到余数为0此时的除数就是最大公约数。最小公倍数利用公式lcm(a, b) a * b / gcd(a, b)。注意先算乘法可能导致溢出可以先除后乘a / gcd(a, b) * b。代码实现与溢出防范#include stdio.h // 辗转相除法求最大公约数 int gcd(int a, int b) { while (b ! 0) { int temp a % b; a b; b temp; } return a; } int main() { int a, b; scanf(%d %d, a, b); int g gcd(a, b); // 计算最小公倍数先除后乘防止中间结果溢出 long long l (long long)a / g * b; printf(最大公约数: %d\n, g); printf(最小公倍数: %lld\n, l); return 0; }算法理解辗转相除法的效率非常高。理解其数学原理比记住代码更重要。最小公倍数的计算中(long long)a / g * b这个顺序很重要它保证了除法先进行结果仍是整数再乘以b避免了a*b可能超出int范围的问题。这是处理潜在整数溢出的常用技巧。4.9 1039题猴子吃桃问题题目核心猴子第一天摘下若干桃子每天吃一半加一个第N天早上发现只剩1个。求第一天共摘了多少个。思路拆解这是一个典型的**逆推递归/迭代**问题。已知第N天剩1个那么第N-1天没吃之前的桃子数就是(第N天的桃子数 1) * 2。以此类推一直推到第一天。逆推的循环实现#include stdio.h int main() { int n, i; int peaches 1; // 第n天早上剩1个 scanf(%d, n); // 从第n天逆推到第1天 for (i n; i 1; i--) { // 循环n-1次 peaches (peaches 1) * 2; } printf(第一天共摘了%d个桃子。\n, peaches); return 0; }思维训练这道题锻炼的是逆向思维。很多编程问题尤其是数列、递推类问题正着想困难时不妨试试反着推。在循环中变量peaches的含义随着循环进行在变化开始时它代表第n天的桃子数第一次循环后代表第n-1天的桃子数以此类推。理解变量在循环中的状态变化是掌握循环的关键。4.10 1040题完数判断题目核心输入一个数判断其是否为完数。完数定义一个数恰好等于它的所有真因子即除了自身以外的约数之和。思路拆解找出所有真因子遍历从1到n-1实际上到n/2即可因为大于n/2的数不可能是n的因子判断是否能整除n。累加因子将所有能整除n的数累加起来。判断如果累加和等于n本身则是完数。优化与输出因子#include stdio.h int main() { int n, i, sum 0; scanf(%d, n); // 寻找因子并累加 for (i 1; i n / 2; i) { // 优化因子最大不超过n/2 if (n % i 0) { sum i; } } if (sum n) { printf(%d是完数。\n, n); // 如果需要输出因子可以再循环一次或者在上次循环中存储因子 printf(其因子为); for (i 1; i n / 2; i) { if (n % i 0) { printf(%d , i); } } printf(\n); } else { printf(%d不是完数。\n, n); } return 0; }性能与功能扩展循环条件用i n/2代替i n是简单的性能优化。如果题目要求输出所有因子有两种做法一是像上面代码那样再循环一次二是在第一次循环时用一个数组把因子存起来。后者用空间换时间当n很大且需要多次使用因子列表时更优。这引入了“空间换时间”的基本算法思想。5. 从练习到精通通用解题框架与思维提升刷完这十道题如果你只是照抄了代码那收获可能只有50%。更重要的是提炼出一套解决同类基础问题的通用方法和思维模式。我把它总结为以下四步法几乎适用于所有OJ基础题。5.1 第一步问题抽象与输入输出建模拿到题目不要急着写代码。首先用笔在纸上回答几个问题输入是什么有几个变量是什么类型整数、浮点数、字符串输入格式如何空格分隔、换行分隔输出是什么要输出单个值、多个值、还是格式化的图形末尾是否需要换行核心转换逻辑是什么输入和输出之间需要经过怎样的计算或处理能用一句或几句话描述清楚吗例如1031题输入是一个整数n输出是两个整数n的位数、n的逆序数。核心逻辑是通过除10取余循环得到位数并构建逆序数。把这个过程想清楚代码就是对这个过程的翻译。5.2 第二步选择合适的数据结构与算法对于基础题数据结构通常就是变量、数组、字符串。算法的核心是循环和条件判断。需要存储多个同类型数据吗- 考虑使用数组。需要处理字符序列吗- 考虑使用字符数组字符串。操作是重复性的吗- 考虑使用for或while循环。有不同的情况分支吗- 考虑使用if-else或switch。有重复的计算逻辑吗- 考虑封装成函数。以1037题字符串统计为例输入是一串字符自然选择字符数组字符串来存储。处理逻辑是对每个字符进行判断这明显是一个遍历数组的循环循环体内是多个if-else判断。5.3 第三步编写代码与防御性编程动手写代码时要养成“防御性编程”的习惯即预见可能出错的地方并加以处理。数组越界访问数组前确保下标在[0, 数组大小-1]范围内。输入验证如果题目说输入是正整数你的代码是否处理了负数或零的情况如1032题判断素数。除法与取模除数是否可能为零边界条件循环的起始和结束值是否正确特别是号该不该取变量初始化累加器sum、计数器count等变量在使用前是否初始化为0在纸上或脑子里模拟一遍代码执行用一组简单的数据包括边界数据如0、1、负数测试一下往往能提前发现很多逻辑错误。5.4 第四步测试、调试与优化代码通过编译不代表正确。必须用多种数据进行测试。常规测试用题目给的样例输入看输出是否一致。边界测试输入最大值、最小值、0、1、空字符串等。特殊测试输入一些看似奇怪但合法的数据比如1038题输入两个互质的数。调试如果结果不对回到第三步使用printf或调试器查看程序运行到每一步时关键变量的值是否符合预期。优化通常在正确性之后考虑。对于基础题优化主要是避免明显的低效操作比如在循环里调用耗时的函数或者使用O(n^2)算法代替O(n)算法如1032题的素数判断优化。6. 常见错误汇总与排查手册根据多年观察初学者在这类题目上犯的错误高度相似。我整理了一个“错误速查表”当你遇到编译错误、运行错误或答案错误时可以按图索骥。错误现象可能原因排查方法编译错误 (Compilation Error)undefined reference to sqrt使用了数学函数如sqrt,pow但未链接数学库。在编译命令最后加上-lm如gcc test.c -o test -lm。expected ; before ...语句末尾缺少分号。检查错误提示行及上一行的末尾是否漏了;。array size missing in arr声明数组时未指定大小如int arr[];。给数组一个确定的大小如int arr[100];。运行时错误 (Runtime Error)段错误 (Segmentation Fault)1. 数组越界访问。2. 访问了未初始化或已释放的指针NULL指针。3. 修改了字符串常量。1. 检查所有数组下标是否在有效范围内。2. 检查指针是否被正确赋值。3. 避免使用char *p hello; p[0]H;这样的写法。浮点错误 (Floating Point Exception)整数除以零。检查所有除法运算特别是取模运算%确保除数不为零。答案错误 (Wrong Answer)样例通过但提交错误1. 边界条件未处理如n0, n1。2. 多输出或少输出空格、换行。3. 数据类型范围溢出如阶乘、斐波那契数未用long long。4. 逻辑错误算法在特定数据下出错。1. 系统测试数据会涵盖边界情况仔细检查你的代码逻辑是否覆盖所有可能输入。2. 严格按照题目要求的格式输出可以复制样例输出对比。3. 估算结果大小选择合适的数据类型。4. 设计更多测试数据尤其是角落情况(corner case)。时间超限 (Time Limit Exceeded)算法效率太低存在死循环。1. 检查循环条件是否能正常结束。2. 对于嵌套循环评估其时间复杂度。如果n很大O(n^2)的算法很可能超时需要优化如1032题找素数只需到sqrt(n)。内存超限 (Memory Limit Exceeded)在循环内申请了大量内存或定义了超大数组。检查数组大小是否远超需求。避免在循环内定义大数组或频繁malloc/new。7. 延伸学习与资源推荐把这十道题吃透你的C语言基础就算过关了。但编程之路还很长接下来可以朝着以下几个方向深入方向一深入C语言特性指针这是C语言的灵魂。理解指针与数组的关系、指针运算、动态内存分配malloc/free。结构体学习如何用结构体组织复杂数据。文件操作学习如何从文件读取数据以及将结果写入文件。预处理器了解#define宏定义和#include的本质。方向二接触C面向对象如果你对C感兴趣在掌握C基础后可以平滑过渡到C。从cin/cout开始体验C更便捷的输入输出。学习string和vector告别原始的字符数组和固定大小数组使用更安全的C标准库容器。理解类与对象这是面向对象编程的核心。尝试用类来封装一些功能比如一个“分数类”或“学生类”。了解STL标准模板库提供了大量现成的数据结构如map,set和算法如sort,find能极大提升编码效率。方向三系统学习数据结构与算法这是编程能力的分水岭。建议找一本经典的教材如《算法导论》或更易懂的《大话数据结构》配合LeetCode、AcWing等平台进行练习。从简单的线性表、栈、队列到树、图再到排序、查找、递归、动态规划等算法。资源推荐书籍《C Primer Plus》经典全面、《C和指针》深入指针、《C Primer》C权威入门。在线练习平台除了ZZNUOJ可以尝试洛谷题目分类清晰社区活跃、力扣LeetCode面向求职算法题为主、Codeforces竞赛向挑战性强。调试工具熟练掌握VSCode调试器或GDB命令行调试器。理解Valgrind等工具用于检测内存泄漏。编程是一门实践性极强的技能没有捷径。最好的学习方法就是“动手-遇到问题-解决问题-总结”。这十道题是一个完美的起点它们涵盖的思维模式和调试技巧将会在你未来解决更复杂问题时反复用到。别怕出错每一个“Wrong Answer”和“Segmentation Fault”都是你进步的阶梯。