简介:针对C语言初学者与高校编程课程备考生,这份23道循环练习题以docx文档呈现,系统覆盖for、while循环及素数判断、水仙花数、数列求和、最大公约数等经典场景,并延伸至斐波那契判断、数位之和、数根、平均成绩等进阶应用。每道题均给出题目描述、输入输出格式、样例与扩展测试用例,可帮助读者理解循环边界、累加累乘、嵌套控制等关键思维,尤其适合应对计算机二级、专升本机试及日常算法入门训练。资源包共1个文件,类型为Word文档,压缩包约3.85MB,内容可直接编辑打印,便于教师组卷或学生反复练习;题目按难度递进,从最大数、奇偶判断到数位处理与征税计算,由浅入深覆盖循环结构的主要考点,附带的测试输入输出样例有助于自我检验、减少基础语法错误。文档内题干规范、排版清晰,还能快速定位循环条件与边界等关键信息。目前已有4248人学习下载,是快速巩固循环结构、提升编程逻辑的实用材料。
1. 一批能让你真正练熟 C 语言循环的 23 道题:从 do-while 读 0 结束到完数筛选
很多初学者学 C 语言循环,卡在同一个地方:书上的语法看懂了,一到自己写就不知道用 for 还是 while,更不知道循环体里什么时候更新变量、什么时候跳出。这份 C 语言循环练习题(共 23 题)恰好把循环结构里最常见的场景都覆盖了——读 0 结束、遇负数终止、repeat 重复执行、数的拆分、累加累乘、素数判断、最大公约数、数根、完数筛选。它不是一个教学课件,更像是一叠可以直接上手敲的题卡,每道题都带输入样例、输出样例,部分还给了测试输入输出。适合刚学完 for 和 while 语法、想找题目练手的人,也适合带学生做课后训练的人用来当题库。我实际把这 23 道题刷了一遍,其中几道题的“坑”相当典型——比如第 4 题西瓜卖几天,很多人会栽在奇数个西瓜的“卖一半”上;第 21 题计数,最大值初始化选错直接导致答案错。接下来按我的拆解方式,把这 23 题的使用方法、代码骨架和踩坑记录整理出来。
2. 先看懂 23 题的内部结构:按循环模式分类,选题才不会盲目
拿到这 23 题,第一件事不是急着写代码,而是把它们按“循环怎么进、怎么出、循环体里干什么”分类。我刷完后把它们分成四类,每类对应的循环选型和变量维护方式完全不同。
2.1 哨兵值结束型:读 0 结束、遇负数终止、EOF 结束
第 1 题最大数、第 9 题奇数和、第 10 题求和、第 21 题计数都属于这一类。它们的共同点是:不知道循环多少次,只知道某个特定值出现时循环结束。这个特定值在编程里叫哨兵值(sentinel)。
这类题我统一用 while 循环,因为进入循环前就要读一次数据,循环体内根据读到的值决定是否继续。看第 9 题奇数和的标准写法:
#include <stdio.h> int main() { int num, sum = 0; scanf("%d", &num); while (num != 0) { if (num % 2 != 0) { sum += num; } scanf("%d", &num); } printf("%d\n", sum); return 0; }逻辑说明:先读第一个数,进入循环时判断它是不是 0。循环体内先处理当前数,再读下一个数到 num。这样一来,循环体末尾的 scanf 负责“更新条件变量”,下一次循环开始时的 num != 0 判断才会基于新的输入。如果把 scanf 放在循环体开头,会漏处理第一个数,这是最常见的基础错误。
参数说明:num 是当前读入的整数,sum 是奇数累加和。第 10 题求和把条件改成 num < 0 结束,第 1 题最大数在循环体内加一个 if (num > max) max = num; 即可。
这里有个需要特别提醒的点:循环体末尾的 scanf 绝不能漏。我见过不少学生写成 while 循环里只有处理逻辑没有重新读入,结果死循环。调试时第一反应就是检查“循环条件依赖的变量有没有在循环体内被更新”。
2.2 计数型循环:明确知道循环次数,用 for 最直接
第 3 题、第 6 题、第 13 题、第 15 题、第 16 题、第 19 题、第 20 题都属于明确知道要循环多少次的。比如第 6 题计算 1 + 1/2 + ... + 1/n,n 是输入的正整数,循环 n 次即可。
这类题统一用 for 循环。for 的三段式结构天然把“初始化、条件、更新”放在一行,代码可读性最高。看第 3 题的实现:
#include <stdio.h> int main() { int n, i; double sum = 0.0; scanf("%d", &n); for (i = 1; i <= n; i++) { sum += 1.0 / (3 * i - 2); } printf("%.5f\n", sum); return 0; }逻辑说明:第 i 项的分母是 3*i-2,所以循环变量 i 直接从 1 开始,每轮计算一项并累加。注意 1.0 / (3 * i - 2) 这里的 1.0 是浮点字面量,如果写成 1 / (3 * i - 2),在 C 语言里两个整数相除结果还是整数——第 i 项全部是 0,这是整数除法陷阱的经典案例。
参数说明:%.5f 控制输出保留 5 位小数,题目要求的输出格式是 1.56978 这样的 5 位小数。如果把循环变量 i 改成从 0 开始,分母就变成了 3*i+1,一样能算,但可读性差一些。
第 16 题强调一点:它除了循环次数已知,还要求“第一个数为 N,后面接着 N 个数”。这意味着先读 N,再循环 N 次读后续数据,循环次数就是 N。注意它要求输出奇数的平方和与偶数的立方和,需要定义两个累加变量,初始化为 0,千万不能混淆。
2.3 条件递推型:循环次数不固定,但依赖上一轮的结果
第 4 题西瓜卖几天、第 18 题数根属于这一类。它们的特点是:循环体执行一次,问题的规模或状态发生变化,直到满足结束条件。
第 4 题西瓜卖几天是最容易写错的一道。题目描述:n 个西瓜,第一天卖一半多两个,以后每天卖剩下的一半多两个,问几天卖完。奇数个西瓜时“卖一半”为一半的整数——比如 5 个西瓜卖 2 个。这意味着本题不能用浮点数直接除以 2,必须用整数除法。看实现:
#include <stdio.h> int main() { int n, days = 0; scanf("%d", &n); while (n > 0) { int sold = n / 2 + 2; n = n - sold; days++; } printf("%d\n", days); return 0; }逻辑说明:关键在 int sold = n / 2 + 2,C 语言里整数 n / 2 自动向下取整——5 / 2 得到 2,正好对应题目的“卖一半为卖 2 个”。循环条件是 n > 0,每卖一次天数加 1,直到西瓜数不大于 0。这个例子如果你用 double 保存 n,5 / 2 变成 2.5,就会算错。
参数说明:days 是卖完需要的天数,sold 是当天卖出的西瓜数。测试输入 120,按这个逻辑计算结果是 5,与题目给出的测试输出一致。
第 18 题数根是另一种递推:把一个正整数的各位数字相加,如果结果不是一位数就继续相加。比如 8943,8+9+4+3=24,2+4=6,输出 6。这种“结果反馈到下一次计算”的模式,用 while 循环最合适。
2.4 枚举遍历型:从某个范围里筛出满足条件的数
第 7 题水仙花数、第 11 题特殊数值、第 22 题输出区间素数、第 23 题完数都属于枚举遍历型。它们的共同点是:从某个范围(比如 100 到 999、1000 到 9999、2 到 n)逐个检查每个数是否满足条件。
这类题用 for 枚举,内层再做条件判断。但要注意,枚举型的“判断条件”往往是独立的函数或一段较复杂的代码,不适合全部塞进循环体,否则一层套一层可读性极差。后面第 4 章我会专门拆这几道题和一些优化边界。
3. 输入输出格式里的坑:这 5 道题写对逻辑很容易,写对格式很难
23 题里有一批题,算法逻辑本身不算难,但输入输出格式非常讲究。我看到很多人在这些题上翻车,不是不会写循环,而是没理解题目对格式的约定。下面挑最有代表性的几道展开。
3.1 repeat 模式:先读重复次数,再循环处理多组数据
第 5 题成绩判定、第 20 题幂函数都是先输入一个正整数 repeat(0 < repeat < 10),然后做 repeat 次运算。这类题的标准结构是外层循环 repeat 次,内层处理单次业务逻辑。
看第 20 题幂函数的写法:
#include <stdio.h> int main() { int repeat, i; scanf("%d", &repeat); for (i = 0; i < repeat; i++) { double x; int n; scanf("%lf %d", &x, &n); double result = 1.0; for (int j = 0; j < n; j++) { result *= x; } printf("%.0f\n", result); } return 0; }逻辑说明:外层 for 控制 repeat 次处理,内层 for 用连乘计算 x 的 n 次幂。注意内层循环变量声明为 int j = 0 是 C99 标准支持的写法,如果你的编译器较老,需要把 j 的声明提到循环外。
参数说明:scanf("%lf %d", &x, &n) 中 %lf 是 double 的输入格式符,float 才是 %f。很多人在这里写错,导致 x 读取异常。pow 函数当然也能实现,但题目显然在考察循环结构,用连乘才是本题意图。
第 5 题成绩判定在输出格式上有一个细节:每次判断完成绩立即输出 Pass 或 Fail,不需要额外加分隔符,因为每个结果占一行。
3.2 读入一批数据但不提前告诉数量:靠特殊值或负数终止
第 1 题最大数、第 9 题奇数和、第 10 题求和、第 21 题计数都有一个共同特点:不知道输入有多少个数,需要依靠某个特殊值或条件终止。这在实际开发里非常常见——比如从文件里读数据到 EOF。
第 10 题求和的终止条件是“遇到负数则终止输入”。和读 0 结束不一样,负数是不参与求和的。看实现:
#include <stdio.h> int main() { int num, sum = 0; scanf("%d", &num); while (num >= 0) { sum += num; scanf("%d", &num); } printf("%d\n", sum); return 0; }逻辑说明:循环条件是 num >= 0,所以一旦读入负数,循环退出,负数不会计入 sum。注意这里不能改成 while (num > 0),因为正数输入可能包含 0——题目说的是“输入若干正数”,但实际测试数据里 0 有可能出现,用 >= 更稳妥。
参数说明:sum 保存累加和。如果题目要求“输入若干正数,对其求和并输出,遇到负数则终止输入”,那么 0 应被视为正数参与求和,所以条件判断必须包含 0。
我在实际带练的时候发现,很多人把终止条件写成 if (num < 0) break;,然后循环体里继续累加。这样也能运行,但用 while 条件判断作为终止条件,语义更清晰——循环的入口条件本身就说明了“什么情况下继续”。
3.3 数位拆分:循环结合取余和整除
第 8 题数位之和、第 17 题偶数位求和、第 18 题数根,核心都是同一个操作:把一个多位数拆成各个位上的数字。拆位的标准手法是反复 % 10 取个位,再 / 10 去掉个位。看第 8 题的实现:
#include <stdio.h> int main() { int n, sum = 0; scanf("%d", &n); while (n > 0) { sum += n % 10; n = n / 10; } printf("%d\n", sum); return 0; }逻辑说明:n % 10 取出当前个位,n / 10 去掉刚取出的个位。比如 n=1234 时:第一次循环取 4,n 变 123;第二次取 3,n 变 12;第三次取 2,n 变 1;第四次取 1,n 变 0,循环结束,sum=10。
参数说明:当输入 n=0 时,while (n > 0) 不进入循环,sum 保持 0,输出 0——这在逻辑上是对的。但如果你用 do-while 结构,n=0 时会先执行一次循环体,输出 0 也一致,但 n=0 和 n=00 这种边界情况就要想清楚。
第 17 题在取位后加一个 if (digit % 2 == 0) 判断即可。注意它的输入 4321 输出 6(2+4),说明只累加偶数位的数字,不考虑偶数位的位置——每个数字本身是偶数就累加。
3.4 输出格式的差异:空格分隔、换行分隔、行尾是否有空格
第 22 题输出 [2,n] 之间所有的素数及素数个数,输出样例是“2 3 5 7 4”——注意素数和个数之间有两个空格。第 23 题输出完数“6 28 496”,用空格分隔。第 16 题输出“106 512”,平方和与立方和以空格隔开。
这些格式细节直接影响判题结果。我的习惯是:先按题目的输出样例推断分隔符,然后写代码时用一个计数器变量记录“当前是否已经输出过内容”,第一个输出前面不加空格,后续输出前加空格。比如输出完数的代码:
#include <stdio.h> int main() { int n, i, j, sum; int first = 1; scanf("%d", &n); for (i = 2; i <= n; i++) { sum = 0; for (j = 1; j < i; j++) { if (i % j == 0) { sum += j; } } if (sum == i) { if (first) { printf("%d", i); first = 0; } else { printf(" %d", i); } } } printf("\n"); return 0; }逻辑说明:first 变量控制格式——第一个完数输出前不打印空格,之后的完数打印一个空格再打印数字。这样可以确保 6 28 496 而不是 6 28 496(前缀空格)。
参数说明:外层循环从 2 开始是因为完数定义为“除自身外的因子之和等于它本身”,1 的因子和是 0 不等于 1。内层循环 j < i 穷举所有可能的因子,整除时累加。这个写法的时间复杂度是 O(n²),n=1000 时完全没问题,但如果 n 到 100 万,就需要用根号优化的写法了。
3.5 题目示例与测试输入的偏差:拿第 10 题说事
第 10 题的输入样例是“32 33 50 34 65 67 -1”,输出 281,也就是 32+33+50+34+65+67=281。测试输入是“31 33 50 34 65 67 -5”,输出 280。注意两道输入里中间都有空格,不是换行——scanf 对空格和换行一视同仁,用 %d 连续读即可。
第 4 题西瓜卖几天输入 10 输出 2:10 个西瓜第一天卖 10/2+2=7,剩 3;第二天卖 3/2+2=3,剩 0,所以 2 天。这里 3/2 在 C 语言整数除法里等于 1,加上 2 等于 3。如果用浮点数算,3/2=1.5+2=3.5,剩 -0.5,天数反而可能变 3——这也是为什么必须用 int 的原因。
4. 数学规律题和算法题:水仙花、特殊数值、素数、公约数、数根的优化边界
23 题里有一批需要数学推导或算法基础的题,它们不仅考循环,还考“怎么在循环里做判断”。我把这 8 道题放在一起讲,因为它们有一个共同特征:循环内嵌条件判断,但判断逻辑是一套独立的方法,值得单独拎出来分析。
4.1 水仙花数和特殊数值:本质都是拆位后做条件判断
第 7 题水仙花数要求打印所有三位数中,各位数字立方和等于该数本身的数。第 11 题特殊数值要求找出前两位与后两位之和的平方等于该数的四位数。
水仙花数的难点在于怎么分离出百位、十位、个位。常见做法是用除法和取余:
#include <stdio.h> int main() { int n, a, b, c; for (n = 100; n <= 999; n++) { a = n / 100; b = n / 10 % 10; c = n % 10; if (a * a * a + b * b * b + c * c * c == n) { printf("%d ", n); } } printf("\n"); return 0; }逻辑说明:a = n / 100 取百位,b = n / 10 % 10 先整除 10 去掉个位,再取余得到十位,c = n % 10 取个位。三层拆位可以应用到任何固定位数的数字。
参数说明:输出格式上,水仙花数的预期输出是 153 370 371 407。我在实际跑的时候输出带尾部空格,判断系统一般不严查尾部空格,但如果你追求规范,可以用 first 变量控制。
第 11 题特殊数值,先拆前两位和后两位:
#include <stdio.h> int main() { int n, front, back, sum; for (n = 1000; n <= 9999; n++) { front = n / 100; back = n % 100; sum = front + back; if (sum * sum == n) { printf("%d ", n); } } printf("\n"); return 0; }逻辑说明:前两位是 n / 100 的整数部分,后两位是 n % 100。这个来自 3025 的性质——30 + 25 = 55,55² = 3025。运行结果输出 2025 3025 9801,与题目一致。
这类题我一般会建议学生先手算一个数验证自己的拆位逻辑,再写代码。比如 2025,前两位 20,后两位 25,45²=2025,说明算法正确。
4.2 素数判断的两种写法:完整遍历和根号优化
第 2 题判断 n 是否为素数,第 22 题输出 [2,n] 之间的所有素数及个数。素数的定义是只能被 1 和本身整除的正数(1 除外)。
第 2 题判断单个素数,用 for 循环从 2 遍历到 n-1:
#include <stdio.h> int main() { int n, i; int is_prime = 1; scanf("%d", &n); if (n <= 1) { is_prime = 0; } for (i = 2; i < n; i++) { if (n % i == 0) { is_prime = 0; break; } } printf("%d\n", is_prime); return 0; }逻辑说明:is_prime 初始化为 1(假设是素数),一旦发现 n 能被某个 i 整除,立即置 0 并 break 跳出循环。注意 n <= 1 必须单独处理,因为 1 不是素数。
参数说明:这里判断到 n-1 是完整遍历。数学上有一个优化:只需要判断到 sqrt(n) 即可。因为如果 n 有大于 sqrt(n) 的因数,必然对应一个小于 sqrt(n) 的因数。第 22 题输出 [2,n] 之间所有素数时,n 如果很大,用 sqrt 优化能显著减少计算量。
第 22 题我建议把“判断是否是素数”单独写成函数,因为输出素数和计数的循环里,每遍历一个数都要判断一次,函数化之后主逻辑更清爽:
#include <stdio.h> #include <math.h> int is_prime(int n) { int i; if (n <= 1) return 0; for (i = 2; i <= sqrt(n); i++) { if (n % i == 0) return 0; } return 1; } int main() { int n, i, count = 0, first = 1; scanf("%d", &n); for (i = 2; i <= n; i++) { if (is_prime(i)) { if (first) { printf("%d", i); first = 0; } else { printf(" %d", i); } count++; } } printf(" %d\n", count); return 0; }逻辑说明:注意输出格式——素数和素数个数之间有两个空格,代码里 printf(" %d\n", count) 两个空格,贴合题目输出样例“2 3 5 7 4”。
参数说明:sqrt 函数在 math.h 里,编译时需要加 -lm 链接数学库(如 gcc prime.c -lm)。循环里用 i <= sqrt(n),sqrt(n) 每次循环都会重复计算,更好的写法是在循环前 int limit = sqrt(n); 然后 for (i = 2; i <= limit; i++)。
这里有个常见的性能陷阱:如果你统计 [2, 100000] 的素数个数,每次判断都调用 sqrt 函数的开销不小。工程上的做法是提前算好 limit,循环变量只用整数比较,避免每次迭代都做浮点计算。
4.3 最大公约数:辗转相除法的循环写法
第 14 题最大公约数,裁剪两个正整数 m、n,输出它们的最大公约数。最经典的做法是欧几里得算法(辗转相除法),用 while 循环实现:
#include <stdio.h> int main() { int m, n, r; scanf("%d %d", &m, &n); while (n != 0) { r = m % n; m = n; n = r; } printf("%d\n", m); return 0; }逻辑说明:辗转相除法的核心是 gcd(m, n) = gcd(n, m % n),不断用较小的数和余数替换原数,直到余数为 0,此时的 m 就是最大公约数。比如 m=20, n=15:r=5,m=15,n=5;再算 r=0,m=5,n=0,循环结束输出 5。
参数说明:这里有一个隐藏细节——如果 m < n,第一轮循环后 m 和 n 会交换。比如 m=15, n=20,r=15,m=20,n=15,第二轮到正确状态。所以不需要额外判断哪个数大。
测试输入 32 80:32 % 80 = 32,m=80,n=32;80 % 32 = 16,m=32,n=16;32 % 16 = 0,m=16,n=0,输出 16,正确。
4.4 数根:两种实现方式,一种循环拆位、一种数学公式
第 18 题数根,除了反复拆位累加,其实有一个数学结论:一个数的数根等于它对 9 取余,如果结果是 0 则数根是 9。但这个结论在 C 语言里要小心处理 n=0 的情况。我更多时候会鼓励学生用循环实现,因为这才是循环题的目的。看循环实现:
#include <stdio.h> int main() { int n, sum; scanf("%d", &n); while (n >= 10) { sum = 0; while (n > 0) { sum += n % 10; n /= 10; } n = sum; } printf("%d\n", n); return 0; }逻辑说明:外层 while 判断当前数是否还是一位数,内层 while 拆位累加。n 每轮被替换成各位数字之和,直到 n < 10。比如 8943:第一轮 sum=24,n=24;第二轮 sum=6,n=6,循环结束输出 6。
参数说明:注意内层 while 结束后 n 被除到 0,所以要把 sum 赋给 n,而不是把 n 和 sum 混淆。也可以在进入内层前用一个临时变量 t = n,循环里操作 t,最后 n = sum——很多学生会在这里翻车:内层循环把 n 改成了 0,外层下一次判断直接跳出。
用取余 9 的一行实现是 n % 9 == 0 ? 9 : n % 9,但这样对 0 还要单独判断,而且完全没练到循环。练习阶段我更建议用循环实现。
4.5 因数个数和完数:穷举范围的边界控制
第 13 题因数个数,第 23 题完数都涉及穷举因数。因数个数的朴素做法是从 1 遍历到 n,每个能整除的都计数:
#include <stdio.h> int main() { int n, i, count = 0; scanf("%d", &n); for (i = 1; i <= n; i++) { if (n % i == 0) { count++; } } printf("%d\n", count); return 0; }逻辑说明:n % i == 0 意味着 i 是 n 的因数。输入 12,因数为 1、2、3、4、6、12,count=6。测试输入 30,因数为 1、2、3、5、6、10、15、30,count=8。
参数说明:这个循环可以优化到 sqrt(n)。因为因数成对出现,i 和 n/i 同时是因数,只需遍历到 sqrt(n) 然后 count+=2。但要注意完全平方数(如 16,因子 4 被数了两次),需要单独判断 i*i == n 时 count 只加 1。练习阶段先把朴素写法写对,再谈优化。
完数判断本质是“求一个数的所有真因子之和”,同样用穷举。第 4.3 节里我已经给了完数的完整代码,这里补充一点:完数很少——1000 以内只有 6、28、496,10000 以内也只有 4 个。如果你看到自己的程序输出大量数字,通常是“除自身的因子之和”写漏了“除自身”,把 j <= i 当成了 j < i。
5. 常见问题排查:这 7 个坑我刷题时几乎每个都踩过一轮
把 23 题完整过一遍之后,我把最常见、最容易让人反复调试的问题汇总成 7 条。每一条都按“现象 → 原因 → 解决”的格式梳理,基本覆盖了循环练习里 80% 的翻车点。
5.1 循环体末尾没有重新读入数据,导致死循环
现象:程序运行后光标一直闪烁,输入数据后没有输出,按 Ctrl+C 才能停止。
原因:while 循环的条件依赖一个变量,但循环体内没有修改这个变量的语句。比如第 9 题奇数和,如果读完第一个数后循环体内忘写 scanf,num 永远是初始值,条件 num != 0 永远为真。
解决:检查循环体内是否有更新条件变量的语句。哨兵值结束型的题目,循环体末尾必须有 scanf 读入下一个值。更稳妥的做法是用 do-while 结构,先执行循环体再判断条件,但 do-while 同样需要在循环体内更新条件变量。
5.2 整数除法把小数部分直接丢掉,数列求和全是 0
现象:第 3 题数列求和,输入 5,输出 0.00000,而不是 1.56978。
原因:1 / (3 * i - 2) 在 C 语言里是整数除法,1 除以任何大于 1 的整数结果都是 0,sum 累加的全是 0。
解决:把分子或分母转成浮点。最直接的是写 1.0 / (3 * i - 2),或者 (double)1 / (3 * i - 2)。注意 3 * i - 2 本身是整数,只要分子是 1.0,整个表达式会自动转成 double 运算。
5.3 scanf 读 double 用了 %f 而不是 %lf
现象:第 20 题幂函数,输入 2 5,输出结果异常大或者结果不对,调试时发现 x 的值是 0 或一个奇怪的小数。
原因:scanf 中 %f 对应 float*,%lf 对应 double*。如果变量声明为 double x,但格式写 %f,内存布局不一致,读入的值是垃圾数据。
解决:double 类型变量的格式化输入用 %lf,输出用 %f 或 %lf 都可以(printf 中 float 会自动转 double,%f 对两者都适用)。养成习惯:scanf 里 double 一律 %lf。
5.4 水仙花数拆位时弄错 b 的计算顺序
现象:第 7 题输出结果不是 153 370 371 407,可能输出一堆不对的数字,或者只输出了 153。
原因:十位拆位写成 b = n / 10 / 10 或者 b = n / 100 % 10,逻辑混乱。n=153 时,n / 10 = 15,15 % 10 = 5,这是对的;但如果写成 n / 100 % 10,得到的是百位(1),不是十位。
解决:记住口诀:除以 10 的幂去掉低位,取余 10 的幂得到低位。十位 = n / 10 % 10,百位 = n / 100 % 10。任何固定位数的拆位都按这个思路推。
5.5 最大数初始值设成 0,输入全是负数时结果错误
现象:第 1 题最大数,按“输入一批正数,输入 0 时结束”来做,如果测试数据里有一个数比 0 小或全部是负数,输出 0 而不是实际最大值。
原因:max 初始化为 0,第一个数进来时 if (num > max) 判断,负数小于 0 不会更新 max。
解决:针对题目“输入一批正数”的场景,max 初始化为 0 没问题。但更通用的写法是用第一个输入的值初始化 max。可以先读第一个数,确保 max = 第一个数,再进入循环。第 21 题计数也是同样的问题——最大值初始化为 0 在输入全为负数时会错。
5.6 素数判断忘记处理 n=1 的情况
现象:输入 1,输出 1(判定为素数),但 1 不是素数。
原因:循环 for (i = 2; i < n; i++) 在 n=1 时条件不成立,循环体不执行,is_prime 保持初始值 1。
解决:在循环之前加 if (n <= 1) 直接置 is_prime = 0。更优雅的做法是在函数开头统一判断 n <= 1 返回 0。第 22 题输出区间素数时,直接从 2 开始遍历,天然避开了 1,但如果单独判断单个 n 就必须处理。
5.7 完数判断把自身当成因子累加
现象:第 23 题输出 6 28 496 之外还输出了一些别的数字,比如 1、2、3 等,或者输出 6 28 496 1 这种结果。
原因:内层循环条件写成 j <= i,把 i 本身也算进因子累加,导致 sum = i + 1 + 2 + 3 这种结果。比如 i=6,因子和变成 1+2+3+6=12,不等于 6,输出反而少了;但有些数可能刚好“假完数”被输出。
解决:内层循环条件是 j < i,不是 j <= i。完数定义“除自身外的因子之和”,自身必须排除。
这些坑在 23 题里反复出现——一旦你发现自己的答案和样例对不上,首先检查这几个位置:条件变量有没有更新、整数除法有没有幸存者、初始化值是否合理、边界值是否单独处理。我自己的习惯是每道题写完先用题目给的输入样例跑一遍,再自己造两个边界数据(比如 0、1、最大数)跑一遍,两遍都对了才看下一题。
6. 刷完 23 题后的自查手段:把“能跑”变成“经得起测”
23 题全部做完之后,不建议直接开始下一章内容,而是花一点时间做交叉验证。我用的方法是:把所有题按“输入输出特征”重新归档成三类,然后给每类设计一套通用测试用例,批量跑一遍看有没有遗漏。
第一类是哨兵值结束型,测试用例必须包含:哨兵值出现在开头(比如第一个就是 0)、哨兵值出现在中间、连续多个哨兵值、只有哨兵值没有其他数据。第 1 题最大数如果测试数据是“0”,应该无输出或输出默认值而不是报错。
第二类是计数型循环,重点测 n=1 和 n=边界上限。第 6 题 n=1 时输出 1,第 20 题 n=50 时 x 的 50 次幂可能溢出,如果 x=2,2 的 50 次幂是 1125899906842624,double 能装下但 float 会变成无穷大——所以这类题我一般把累乘变量声明为 double。
第三类是数学规律题,重点做反向验证。水仙花数算完后,手动验证 153:1³+5³+3³=1+125+27=153;完数算完后,手动验证 496:因子 1、2、4、8、16、31、62、124、248,和正好 496。这种验证不需要代码,但能快速发现自己是不是把条件写反了。
还有一个容易被忽略的自查点:输出格式的“尾部空格”。很多判题系统忽略尾部空格,但有些格式严格的系统不忽略。我在写完水仙花数这类需要空格分隔输出的题之后,会用 first 变量控制统一处理,而不是在循环体里 printf("%d ", n) 留尾部空格。
从那以后,我每次刷完一组循环题都强制走一遍“边界输入 + 反向验证 + 格式检查”三个动作,再开始下一个知识点。这个习惯帮我避免了很多重复调试的耗时。希望帮到你。
本文还有配套的精品资源,点击获取