控制流练习 — 10 道必做题
C语言学习教程
控制流练习 — 10 道必做题
🌟 为什么动手练习不可替代?
学编程和学游泳一样——看再多教程,不亲自下水永远学不会。你可能觉得”if 和 for 我都看懂了”,但到了自己写的时候,分号去哪了、边界差一个、无限循环了……这些问题只有在亲自敲代码的过程中才会暴露。
这 10 道题按难度分为三个等级:
- ★☆☆ — 基础题(只用一个知识点)
- ★★☆ — 进阶题(需要组合 if + 循环)
- ★★★ — 挑战题(需要综合运用 + 算法思维)
建议学习方式:每道题先独立尝试 15-30 分钟,实在做不出来再看参考代码。看完参考代码后,关掉它,自己重新写一遍。
📖 解题通用方法
三问法
拿到任何一道编程题,先问自己三个问题:
- 输入是什么?输出是什么?(明确边界)
- 判断条件怎么写?(if 的部分)
- 重复逻辑在哪里?(循环的部分)
用纸笔写下来,然后一行行转换成代码。
测试检查清单
写完代码后,至少用 3 组数据测试:
| 测试类型 | 说明 | 示例 |
|---|---|---|
| 正常值 | 题目给的样例 | 输入 7,期望 odd |
| 边界值 | 刚好卡在边上的 | 输入 0,输入 1,输入 100 |
| 异常值 | 不合理但可能输入的 | 输入 -1,输入 1000 |
第 1 题:判断奇偶 ★☆☆
题目描述
编写一个程序,输入一个整数,判断它是奇数还是偶数,并输出结果。
输入: 7 → 输出: 7 是奇数
输入: 4 → 输出: 4 是偶数
输入: 0 → 输出: 0 是偶数(0 是特殊的偶数)
解题思路
判断奇偶的核心:用 % 取余运算符看是否能被 2 整除。
n % 2 == 0→ 偶数(余数为 0)n % 2 == 1→ 正奇数n % 2 == -1→ 负奇数(注意!负数的余数也是负的)
参考代码
#include <stdio.h>
int main() {
int n;
printf("请输入一个整数: ");
scanf("%d", &n);
// 只判断余数是否为 0,正负数都适用
if (n % 2 == 0) {
printf("%d 是偶数\n", n);
} else {
printf("%d 是奇数\n", n);
}
return 0;
}
细节讨论:为什么不用 n % 2 == 1?
// 用 n % 2 == 1 来判断奇数的问题是:
// 当 n = -7 时,n % 2 == -1(不是 1!)
// 所以用 != 0 或 == 0 更安全
扩展思考
如果不用 % 运算符,怎么做?提示:n & 1(位运算),n / 2 * 2 == n(整除后乘回去判断)。
第 2 题:判断闰年 ★☆☆
题目描述
输入一个年份,判断它是不是闰年,输出”是闰年”或”不是闰年”。
闰年规则
能被 4 整除,但不能被 100 整除 → 闰年
能被 400 整除 → 闰年(修正规则)
其余年份 → 不是闰年
输入: 2000 → 是闰年(能被 400 整除)
输入: 1900 → 不是闰年(能被 100 整除但不能被 400 整除)
输入: 2024 → 是闰年(能被 4 整除但不能被 100 整除)
输入: 2023 → 不是闰年
解题思路
把自然语言翻译成逻辑表达式:
(能被4整除 且 不能被100整除) 或 能被400整除
↓ ↓
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
参考代码
#include <stdio.h>
int main() {
int year;
printf("请输入年份: ");
scanf("%d", &year);
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {
printf("%d 年是闰年\n", year);
} else {
printf("%d 年不是闰年\n", year);
}
return 0;
}
常见错误:运算符优先级
// ❌ 错误:不加括号
if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0)
// 实际按 (year%4==0 && year%100!=0) || (year%400==0) 执行
// 这次刚好没问题,但依赖运算符优先级不安全
// ✅ 推荐:用括号把逻辑分组
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
第 3 题:最大公约数(GCD)★★☆
题目描述
输入两个正整数,计算它们的最大公约数(Greatest Common Divisor)。
输入: 12 18 → 输出: 最大公约数是 6
输入: 17 23 → 输出: 最大公约数是 1(互质)
输入: 48 60 → 输出: 最大公约数是 12
算法:辗转相除法(欧几里得算法)
这是人类最早的算法之一,核心思想很简单:
gcd(a, b) = gcd(b, a % b) ← 递归关系
当 b == 0 时,a 就是答案 ← 终止条件
手算 12 和 18:
gcd(12, 18) → gcd(18, 12%18=12) → gcd(12, 18%12=6) → gcd(6, 12%6=0) → 答案 = 6
参考代码
#include <stdio.h>
int main() {
int a, b;
printf("请输入两个正整数: ");
scanf("%d %d", &a, &b);
// 保存原始值(后面 a, b 会变)
int original_a = a, original_b = b;
// 辗转相除法
while (b != 0) {
int remainder = a % b; // 求余数
a = b; // 把 b 赋给 a
b = remainder; // 把余数赋给 b
}
// 此时 b == 0,a 就是答案
printf("%d 和 %d 的最大公约数是 %d\n", original_a, original_b, a);
// 顺手算一下最小公倍数 LCM
int lcm = (original_a / a) * original_b;
printf("最小公倍数是 %d\n", lcm);
return 0;
}
跟踪执行过程
输入 12 18:
第 1 轮: a=12, b=18, remainder=12%18=12 → a=18, b=12
第 2 轮: a=18, b=12, remainder=18%12=6 → a=12, b=6
第 3 轮: a=12, b=6, remainder=12%6=0 → a=6, b=0
b==0, 退出, 答案 a=6 ✓
第 4 题:打印乘法表 ★☆☆
题目描述
用嵌套循环打印标准的 9×9 乘法表。格式要求对齐。
1×1= 1
1×2= 2 2×2= 4
1×3= 3 2×3= 6 3×3= 9
...
1×9= 9 2×9=18 3×9=27 4×9=36 5×9=45 6×9=54 7×9=63 8×9=72 9×9=81
解题思路
- 外层循环控制行(被乘数 i,1-9)
- 内层循环控制列(乘数 j,1 到 i)
- 格式对齐:
printf("%d×%d=%-3d", j, i, i*j);中的%-3d表示左对齐占 3 位
参考代码
#include <stdio.h>
int main() {
printf("========== 九九乘法表 ==========\n\n");
// 先打印表头
printf(" ");
for (int i = 1; i <= 9; i++) {
printf("%-6d", i);
}
printf("\n");
printf(" ");
for (int i = 1; i <= 9; i++) {
printf("------");
}
printf("\n");
for (int i = 1; i <= 9; i++) {
printf("%2d |", i); // 行号
for (int j = 1; j <= 9; j++) {
if (j <= i) {
printf("%d×%d=%-2d ", j, i, i * j);
} else {
printf(" "); // 空白保持对齐
}
}
printf("\n");
}
return 0;
}
简化版(只打印下三角)
#include <stdio.h>
int main() {
for (int i = 1; i <= 9; i++) {
for (int j = 1; j <= i; j++) {
printf("%d×%d=%-2d ", j, i, i * j);
}
printf("\n");
}
return 0;
}
第 5 题:判断素数 ★★☆
题目描述
输入一个正整数,判断它是不是素数(质数)。
素数:大于 1,且只能被 1 和它自身整除的数。
输入: 17 → 17 是素数
输入: 24 → 24 不是素数
输入: 2 → 2 是素数(最小的素数)
输入: 1 → 1 不是素数(定义上不是)
解题思路
朴素方法:从 2 到 n-1 逐个试除 → 太慢
优化方法:只需要试到 sqrt(n)(n 的平方根)。
为什么?如果 n = a × b,假设 a ≤ b,
那么 a ≤ sqrt(n),b ≥ sqrt(n)。
所以如果 n 有因数,一定能找到不大于 sqrt(n) 的因数。
判断 17:
sqrt(17) ≈ 4.12,只需检查 2, 3, 4
17 % 2 = 1(不能整除)
17 % 3 = 2(不能整除)
17 % 4 = 1(不能整除)
→ 是素数 ✓
参考代码
#include <stdio.h>
#include <math.h> // sqrt 函数
int main() {
int n;
printf("请输入一个正整数: ");
scanf("%d", &n);
// 特殊处理:1 和小于 1 的不是素数
if (n <= 1) {
printf("%d 不是素数(素数必须大于 1)\n", n);
return 0;
}
// 2 是唯一的偶素数
if (n == 2) {
printf("2 是素数\n");
return 0;
}
// 偶数(除了 2)都不是素数,快速排除
if (n % 2 == 0) {
printf("%d 不是素数(能被 2 整除)\n", n);
return 0;
}
int is_prime = 1; // 先假设是素数
int limit = (int)sqrt(n); // 只需要检查到 sqrt(n)
// 从 3 开始,每次 +2(跳过偶数,进一步加速)
for (int i = 3; i <= limit; i += 2) {
if (n % i == 0) {
printf("%d 不是素数(能被 %d 整除)\n", n, i);
is_prime = 0;
break; // 找到一个因数就可以停止了
}
}
if (is_prime) {
printf("%d 是素数\n", n);
}
return 0;
}
逐层优化的价值
方法一:检查 2 到 n-1 → O(n)
方法二:检查 2 到 sqrt(n) → O(√n)
方法三:2 单独处理,奇数跳步 → O(√n/2)
当 n = 1000000 时:
方法一:约 1,000,000 次检查
方法三:约 500 次检查
快了 2000 倍!
第 6 题:水仙花数 ★★☆
题目描述
找出所有三位数中的水仙花数。
水仙花数(Armstrong Number / Narcissistic Number):每一位数字的立方和等于它本身的数。
153 = 1³ + 5³ + 3³ = 1 + 125 + 27 = 153 ✓
370 = 3³ + 7³ + 0³ = 27 + 343 + 0 = 370 ✓
371 = 3³ + 7³ + 1³ = 27 + 343 + 1 = 371 ✓
407 = 4³ + 0³ + 7³ = 64 + 0 + 343 = 407 ✓
解题思路:如何拆分数字的每一位?
假设 n = 153:
个位:153 % 10 = 3 (取最后一位)
十位:153 / 10 % 10 = 15 % 10 = 5 (去掉个位再取最后一位)
百位:153 / 100 = 1 (整除 100)
参考代码
#include <stdio.h>
int main() {
printf("三位数中的水仙花数:\n");
int count = 0;
for (int n = 100; n <= 999; n++) {
int ge = n % 10; // 个位
int shi = n / 10 % 10; // 十位
int bai = n / 100; // 百位
int sum = ge * ge * ge
+ shi * shi * shi
+ bai * bai * bai;
if (sum == n) {
printf("%d = %d³ + %d³ + %d³ = %d + %d + %d\n",
n, bai, shi, ge,
bai*bai*bai, shi*shi*shi, ge*ge*ge);
count++;
}
}
printf("共找到 %d 个水仙花数。\n", count);
return 0;
}
通用解法(任意位数)
// 判断任意正整数是否是水仙花数
#include <stdio.h>
int main() {
int n;
printf("请输入一个正整数: ");
scanf("%d", &n);
int original = n;
int digits = 0;
// 1. 计算位数
int temp = n;
while (temp > 0) {
temp /= 10;
digits++;
}
printf("数字 %d 有 %d 位\n", n, digits);
// 2. 计算各位数字的 digits 次方之和
int sum = 0;
temp = n;
while (temp > 0) {
int digit = temp % 10;
// 计算 digit 的 digits 次方
int power = 1;
for (int i = 0; i < digits; i++) {
power *= digit;
}
sum += power;
printf(" %d^%d = %d\n", digit, digits, power);
temp /= 10;
}
// 3. 判断
printf("各位的 %d 次方之和 = %d\n", digits, sum);
if (sum == original) {
printf("→ %d 是水仙花数!\n", original);
} else {
printf("→ %d 不是水仙花数。\n", original);
}
return 0;
}
第 7 题:斐波那契数列 ★★☆
题目描述
输出前 20 项斐波那契数列。
斐波那契数列:从第 3 项开始,每一项都等于前两项之和。 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …
解题思路
用一个循环,维护三个变量:
初始:a = 1(第1项),b = 1(第2项)
每轮:输出 a,然后 a, b → b, a+b(向后移动一个位置)
第 1 轮:a=1, b=1 → 输出 1 → a=1, b=2
第 2 轮:a=1, b=2 → 输出 1 → a=2, b=3
第 3 轮:a=2, b=3 → 输出 2 → a=3, b=5
第 4 轮:a=3, b=5 → 输出 3 → a=5, b=8
...
参考代码
#include <stdio.h>
int main() {
int n = 20;
long long a = 1, b = 1; // 用 long long 防止溢出
printf("斐波那契数列前 %d 项:\n", n);
for (int i = 1; i <= n; i++) {
printf("%lld ", a);
// 每 10 项换行
if (i % 10 == 0) {
printf("\n");
}
// 核心:计算下一项
long long next = a + b;
a = b;
b = next;
}
printf("\n");
// 验证:第 20 项应该是 6765
printf("第 20 项是 6765 吗?(对照上方输出)\n");
return 0;
}
为什么用 long long?
int 范围:约 ±21 亿(±2×10⁹)
斐波那契第 47 项:约 29 亿 → int 溢出!
long long 范围:约 ±9×10¹⁸ → 可以安全算到第 92 项
第 8 题:数字反转 ★★☆
题目描述
输入一个正整数,输出它的反转形式。
输入: 12345 → 输出: 54321
输入: 1000 → 输出: 1(前导零自动消失)
输入: 7 → 输出: 7
输入: 12300 → 输出: 321
算法思路
每次取最后一位数字,拼到结果上:
12345 反转过程:
n=12345: digit=5, result=0×10+5=5, n=1234
n=1234: digit=4, result=5×10+4=54, n=123
n=123: digit=3, result=54×10+3=543, n=12
n=12: digit=2, result=543×10+2=5432, n=1
n=1: digit=1, result=5432×10+1=54321,n=0
n=0 → 退出
参考代码
#include <stdio.h>
int main() {
int n, original;
printf("请输入一个正整数: ");
scanf("%d", &n);
original = n; // 保存原值
int reversed = 0;
while (n > 0) {
int digit = n % 10; // 取出最后一位
reversed = reversed * 10 + digit; // 拼接到结果
n /= 10; // 去掉最后一位
}
printf("%d 反转后是 %d\n", original, reversed);
return 0;
}
扩展:判断回文数
// 回文数:正着读和反着读一样的数,如 121、12321
// 只需在反转后加一行判断:
if (original == reversed) {
printf("而且 %d 是回文数!\n", original);
}
常见错误:n 被修改后还想用
// ❌ 错误
int n = 12345;
int reversed = 0;
while (n > 0) {
// ... 修改 n
}
printf("%d 反转后是 %d\n", n, reversed); // n 已经是 0 了!
// ✅ 正确:保存原始值
int original = n;
第 9 题:打印菱形 ★★★
题目描述
输入一个奇数 n(如 5),打印一个由 * 组成的菱形。
n = 5:
* ← 2 个空格 + 1 个星号
*** ← 1 个空格 + 3 个星号
***** ← 0 个空格 + 5 个星号
*** ← 1 个空格 + 3 个星号
* ← 2 个空格 + 1 个星号
解题思路:拆成上下两部分
上半部分(包含中间行):
第 1 行(i=1):空格 2 个,星号 1 个 → 空格 = (n-1)/2,星号 = 2i-1
第 2 行(i=2):空格 1 个,星号 3 个 → 空格 = (n-3)/2,星号 = 2i-1
第 3 行(i=3):空格 0 个,星号 5 个 → 空格 = (n-5)/2,星号 = 2i-1
规律:上半部分行数 mid = (n+1)/2
第 i 行:空格 = mid - i,星号 = 2i - 1
下半部分:
是上半部分的镜像(去掉中间行)
第 i 行(i 从 mid-1 到 1):
空格 = mid - i,星号 = 2i - 1
参考代码
#include <stdio.h>
int main() {
int n;
printf("请输入一个奇数: ");
scanf("%d", &n);
if (n % 2 == 0) {
printf("提示:输入偶数也能画,但不对称。请输入奇数。\n");
// 这里我们还是继续执行,把 n 加 1 变成奇数
n = n + 1;
printf("已自动调整为 %d\n", n);
}
int mid = (n + 1) / 2; // 中间行的位置(行数)
// === 上半部分(包括中间行)===
for (int i = 1; i <= mid; i++) {
// 打印空格
for (int j = 1; j <= mid - i; j++) {
printf(" ");
}
// 打印星号
for (int k = 1; k <= 2 * i - 1; k++) {
printf("*");
}
printf("\n");
}
// === 下半部分(中间行以下)===
for (int i = mid - 1; i >= 1; i--) {
// 打印空格
for (int j = 1; j <= mid - i; j++) {
printf(" ");
}
// 打印星号
for (int k = 1; k <= 2 * i - 1; k++) {
printf("*");
}
printf("\n");
}
return 0;
}
逐行跟踪(n=5)
mid = 3
上半部分:
i=1: 空格=3-1=2 个, 星号=2×1-1=1 个 → " *"
i=2: 空格=3-2=1 个, 星号=2×2-1=3 个 → " ***"
i=3: 空格=3-3=0 个, 星号=2×3-1=5 个 → "*****"
下半部分:
i=2: 空格=3-2=1 个, 星号=2×2-1=3 个 → " ***"
i=1: 空格=3-1=2 个, 星号=2×1-1=1 个 → " *"
优化:用绝对值,上下部分统一
// 更简洁的写法(理解难度更高)
for (int i = -mid + 1; i < mid; i++) {
int abs_i = (i < 0) ? -i : i; // 取绝对值
int stars = 2 * (mid - abs_i) - 1;
int spaces = abs_i;
// 打印...
}
第 10 题:猜数字游戏 ★★★
题目描述
程序随机生成一个 1 到 100 之间的整数,让用户来猜。每次猜完后告诉用户”猜大了”还是”猜小了”,直到猜对为止。最后显示猜了多少次。
(程序生成 67)
请输入你的猜测 (1-100): 50
太低了!再试一次。
请输入你的猜测 (1-100): 75
太高了!再试一次。
请输入你的猜测 (1-100): 63
太低了!再试一次。
请输入你的猜测 (1-100): 67
恭喜你猜对了!用了 4 次。
知识点
rand()— 生成伪随机数(<stdlib.h>)srand(time(0))— 用当前时间初始化随机数种子(<time.h>)do-while— 至少猜一次- 二分查找的思想(每次排除一半范围)
参考代码
#include <stdio.h>
#include <stdlib.h> // rand, srand
#include <time.h> // time
int main() {
// 用当前时间作为随机数种子
// 这样每次运行程序生成的数字都不同
srand((unsigned int)time(0));
// 生成 1 到 100 的随机数
int target = rand() % 100 + 1;
int guess;
int tries = 0;
int low = 1, high = 100; // 当前推测范围
printf("╔══════════════════════╗\n");
printf("║ 猜数字游戏 (1-100) ║\n");
printf("╚══════════════════════╝\n\n");
do {
printf("当前范围: [%d, %d]\n", low, high);
printf("请输入你的猜测: ");
scanf("%d", &guess);
tries++;
if (guess < low || guess > high) {
printf("⚠️ 请输入 %d 到 %d 之间的数字!\n", low, high);
tries--; // 无效输入不计入次数
continue;
}
if (guess > target) {
printf("📉 太大了!\n");
if (guess - 1 < high) {
high = guess - 1; // 缩小上界
}
} else if (guess < target) {
printf("📈 太小了!\n");
if (guess + 1 > low) {
low = guess + 1; // 缩小下界
}
} else {
printf("\n🎉 恭喜你猜对了!答案是 %d\n", target);
printf("你一共猜了 %d 次。\n", tries);
// 给个评价
if (tries <= 5) {
printf("评价:天才级别的直觉!\n");
} else if (tries <= 8) {
printf("评价:非常不错!\n");
} else if (tries <= 12) {
printf("评价:还可以,继续加油。\n");
} else {
printf("评价:下次试试二分查找法?\n");
}
}
} while (guess != target);
return 0;
}
二分查找策略(理论最优)
理论上,100 个数字最多只需 ⌈log₂(100)⌉ = 7 次就能猜到!
策略:每次都猜范围的中间值。
范围 [1,100] → 猜 50
如果太低 → 范围 [51,100] → 猜 75
如果太高 → 范围 [1,49] → 猜 25
...
📝 10 题总结
| 题号 | 题目 | 难度 | 核心知识点 | 典型易错点 |
|---|---|---|---|---|
| 1 | 判断奇偶 | ★☆☆ | % 运算符,if-else | 负数取余 |
| 2 | 判断闰年 | ★☆☆ | 复合条件,&& || | 运算符优先级 |
| 3 | 最大公约数 | ★★☆ | while 循环,辗转相除 | 循环条件写反 |
| 4 | 打印乘法表 | ★☆☆ | 嵌套 for,格式控制 | 对齐格式 |
| 5 | 判断素数 | ★★☆ | 算法优化,sqrt | 效率(不必要的检查) |
| 6 | 水仙花数 | ★★☆ | 数字位拆分,幂运算 | 个十百位拆错 |
| 7 | 斐波那契 | ★★☆ | 递推思想,变量交换 | 前两项初始值 |
| 8 | 数字反转 | ★★☆ | while 取位拼接 | n 被修改后再用 |
| 9 | 打印菱形 | ★★★ | 嵌套循环,对称思想 | 空格和星号关系 |
| 10 | 猜数字 | ★★★ | do-while,随机数,二分 | 种子初始化 |
💡 练习方法建议
- 先默写再对照:看完参考代码后,关掉页面,自己从头写一遍。
- 故意改错:把
n /= 10改成n %= 10,看看程序会怎样?理解 bug 比写出正确代码更有价值。 - 加”打印调试”:在循环里加
printf("i=%d, sum=%d\n", i, sum);,亲眼看着变量变化。 - 扩展题目:做完后给自己提新要求。比如”斐波那契那道题,求前 50 项中哪些是素数?”
- 计时练习:第 1-4 题应该在 5-10 分钟内完成。如果做不到,说明基础还不够熟练,需要回看教程。
🔗 相关主题
- 条件判断 — if/else 与 switch — 所有练习题的判断逻辑都依赖这里
- 循环 — for/while 重复执行 — 练习题中大量使用循环
- 数组 — 数据的有序集合 — 学完数组后可以回来改进这些练习(比如把所有水仙花数存到数组里)
- 函数 — 代码复用的基本单元 — 把素数判断、数字反转等功能封装成函数