🛡️

身份验证

答对题目即可进入,验证你是谁

剩余尝试次数:3

控制流练习 — 10 道必做题

C语言学习教程

控制流练习 — 10 道必做题

🌟 为什么动手练习不可替代?

学编程和学游泳一样——看再多教程,不亲自下水永远学不会。你可能觉得”if 和 for 我都看懂了”,但到了自己写的时候,分号去哪了、边界差一个、无限循环了……这些问题只有在亲自敲代码的过程中才会暴露。

这 10 道题按难度分为三个等级:

建议学习方式:每道题先独立尝试 15-30 分钟,实在做不出来再看参考代码。看完参考代码后,关掉它,自己重新写一遍。


📖 解题通用方法

三问法

拿到任何一道编程题,先问自己三个问题:

  1. 输入是什么?输出是什么?(明确边界)
  2. 判断条件怎么写?(if 的部分)
  3. 重复逻辑在哪里?(循环的部分)

用纸笔写下来,然后一行行转换成代码。

测试检查清单

写完代码后,至少用 3 组数据测试:

测试类型说明示例
正常值题目给的样例输入 7,期望 odd
边界值刚好卡在边上的输入 0,输入 1,输入 100
异常值不合理但可能输入的输入 -1,输入 1000

第 1 题:判断奇偶 ★☆☆

题目描述

编写一个程序,输入一个整数,判断它是奇数还是偶数,并输出结果。

输入: 7   →  输出: 7 是奇数
输入: 4   →  输出: 4 是偶数
输入: 0   →  输出: 0 是偶数(0 是特殊的偶数)

解题思路

判断奇偶的核心:% 取余运算符看是否能被 2 整除

参考代码

#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

解题思路

参考代码

#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 次。

知识点

参考代码

#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,随机数,二分种子初始化

💡 练习方法建议

  1. 先默写再对照:看完参考代码后,关掉页面,自己从头写一遍。
  2. 故意改错:把 n /= 10 改成 n %= 10,看看程序会怎样?理解 bug 比写出正确代码更有价值。
  3. 加”打印调试”:在循环里加 printf("i=%d, sum=%d\n", i, sum);,亲眼看着变量变化。
  4. 扩展题目:做完后给自己提新要求。比如”斐波那契那道题,求前 50 项中哪些是素数?”
  5. 计时练习:第 1-4 题应该在 5-10 分钟内完成。如果做不到,说明基础还不够熟练,需要回看教程。

🔗 相关主题