C 练习实例36 – 求100之内的素数
在学习 C 语言的过程中,我们常常会遇到一些经典的编程练习题,它们看似简单,却能帮助我们夯实基础、理解逻辑。今天要讲的这个题目,就是 C 练习实例36 – 求100之内的素数。它不仅是初学者掌握循环与判断的绝佳实践,也是一次对算法效率的初步探索。
素数,也叫质数,是指大于 1 且只能被 1 和自身整除的自然数。比如 2、3、5、7、11 都是素数,而 4、6、8、9 显然不是,因为它们能被其他数整除。我们今天的目标,就是用 C 语言找出从 2 到 100 之间所有的素数。
这个题目虽然看起来不大,但它背后涉及了三个核心知识点:循环控制、条件判断、以及算法优化。如果你是初学者,别担心,我会一步步带你理解,就像拆解一个积木玩具一样,把复杂逻辑拆成简单步骤。
什么是素数?从生活中的例子说起
想象一下你在分糖果。如果有 7 个糖果,你只能平均分给 1 个人或 7 个人,不能分成 2 份、3 份、4 份……这就说明 7 是一个“不可再分”的数,它就是素数。
反过来,如果 8 个糖果,你可以分给 2 个人,每人 4 个,也可以分给 4 个人,每人 2 个。这说明 8 可以被 2 和 4 整除,它就不是素数。
所以,判断一个数是不是素数,关键就是看它能不能被小于它的数整除。如果都不能,那就是素数。
基础思路:暴力枚举法
最直观的方法,就是对每个数从 2 到 n-1 逐一尝试能否整除。如果没有任何一个数能整除它,那它就是素数。
我们来写一个简单的程序,遍历 2 到 100 的每一个数,然后判断它是否为素数。
#include <stdio.h>
int main() {
// 外层循环:遍历 2 到 100 的每一个数
for (int i = 2; i <= 100; i++) {
int is_prime = 1; // 假设当前数是素数,用标志位标记
// 内层循环:尝试用 2 到 i-1 的数去整除 i
for (int j = 2; j < i; j++) {
if (i % j == 0) {
is_prime = 0; // 如果能整除,说明不是素数
break; // 一旦找到一个因数,立刻跳出循环
}
}
// 如果标志位仍为 1,说明没有找到因数,是素数
if (is_prime == 1) {
printf("%d ", i);
}
}
printf("\n");
return 0;
}
代码注释详解:
for (int i = 2; i <= 100; i++):外层循环从 2 开始,一直到 100,我们检查每一个数。int is_prime = 1;:设置一个标志变量,初始为 1,代表“目前认为是素数”。for (int j = 2; j < i; j++):内层循环尝试用 2 到 i-1 的数去整除 i。if (i % j == 0):判断是否能整除,如果能,说明 i 有因数,不是素数。is_prime = 0; break;:一旦发现一个因数,就标记为非素数,并跳出内层循环,避免多余计算。if (is_prime == 1):如果整个循环都没找到因数,说明是素数,打印输出。
这个方法虽然简单,但效率不高。比如判断 97 时,我们要从 2 试到 96,其实大可不必。
优化思路:减少判断范围
你有没有想过,如果一个数能被 6 整除,那它肯定也能被 2 或 3 整除。所以,我们不需要检查所有小于 i 的数,只需要检查到 √i 就够了。
为什么?因为如果 i 有一个大于 √i 的因数,那它必然有一个小于 √i 的因数。比如 100,它的因数有 10 和 10,而 √100 = 10。如果一个因数大于 10,另一个就一定小于 10。
所以,我们只需要检查从 2 到 √i 的数即可。
#include <stdio.h>
#include <math.h> // 用于 sqrt 函数
int main() {
printf("100 之内的素数有:\n");
for (int i = 2; i <= 100; i++) {
int is_prime = 1;
int limit = (int)sqrt(i); // 计算 √i,只检查到这个值
for (int j = 2; j <= limit; j++) {
if (i % j == 0) {
is_prime = 0;
break;
}
}
if (is_prime == 1) {
printf("%d ", i);
}
}
printf("\n");
return 0;
}
优化亮点说明:
#include <math.h>:引入数学库,使用sqrt函数。int limit = (int)sqrt(i);:计算 i 的平方根,并转为整数。j <= limit:循环只到 √i,大大减少了内层循环次数。
比如判断 97 时,我们只需检查 2 到 9 的数,而不是 2 到 96。效率提升了几十倍。
进阶技巧:使用数组标记法(埃拉托斯特尼筛法)
如果你觉得上面的方法还行,那我再告诉你一个更高效的方法——埃拉托斯特尼筛法(Sieve of Eratosthenes)。它就像用筛子过滤杂质,先把所有数都当成素数,然后逐步筛掉非素数。
算法步骤:
- 创建一个布尔数组,大小为 101(索引 0 到 100),初始全部设为 true。
- 从 2 开始,把 2 的所有倍数(4, 6, 8, ...)标记为 false。
- 找到下一个未被标记的数(比如 3),把它的倍数也标记为 false。
- 重复这个过程,直到处理完 √100 = 10。
最终,所有仍为 true 的索引,就是素数。
#include <stdio.h>
#include <string.h> // 用于 memset
int main() {
// 定义一个布尔数组,记录每个数是否为素数
int is_prime[101]; // 索引 0 到 100
memset(is_prime, 1, sizeof(is_prime)); // 初始全部设为 true
// 0 和 1 不是素数
is_prime[0] = 0;
is_prime[1] = 0;
// 埃拉托斯特尼筛法
for (int i = 2; i * i <= 100; i++) {
if (is_prime[i]) { // 如果 i 是素数
// 将 i 的所有倍数标记为非素数
for (int j = i * i; j <= 100; j += i) {
is_prime[j] = 0;
}
}
}
// 输出所有素数
printf("100 之内的素数有:\n");
for (int i = 2; i <= 100; i++) {
if (is_prime[i]) {
printf("%d ", i);
}
}
printf("\n");
return 0;
}
筛法核心逻辑解析:
memset(is_prime, 1, sizeof(is_prime));:将数组初始化为 1,表示“默认是素数”。i * i <= 100:只需要从 2 到 10,因为大于 10 的数的平方已超过 100。j = i * i:从 i² 开始标记,因为更小的倍数已经被更小的素数筛过了。j += i:每次加 i,标记下一个倍数。
这个方法的时间复杂度是 O(n log log n),远优于前两种方法的 O(n√n)。
结果对比:三种方法效率差异
我们来整理一下三种方法的优缺点:
| 方法 | 时间复杂度 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 暴力枚举法 | O(n²) | 小范围练习 | 逻辑清晰,易理解 | 效率低,不适合大数 |
| 优化枚举法 | O(n√n) | 中小范围 | 代码简洁,效率提升明显 | 仍需对每个数独立判断 |
| 埃拉托斯特尼筛法 | O(n log log n) | 大范围批量求素数 | 效率最高,适合批量处理 | 需要额外数组空间 |
在实际开发中,如果你要频繁查询 100 以内的素数,筛法是最优选择。如果你只是临时判断一个数是否为素数,优化枚举法就足够了。
实际应用:为什么素数很重要?
别小看素数,它在计算机领域应用广泛。比如:
- 加密算法(如 RSA)依赖大素数生成密钥。
- 哈希函数中,使用素数作为模数可以减少冲突。
- 游戏开发中,有时用素数作为随机种子的偏移量。
所以,掌握“C 练习实例36 – 求100之内的素数”不仅是学习 C 语言的里程碑,更是理解算法思维的起点。
总结与建议
今天我们从最简单的暴力法,一步步优化到高效的筛法,完整走了一遍求素数的全过程。你可能会问:我到底该用哪个?
答案是:理解优先,选择其次。
- 初学者:先用暴力法,理解逻辑。
- 中级开发者:尝试优化法,体会性能差异。
- 进阶者:掌握筛法,为复杂问题打基础。
编程不是背代码,而是培养思维。每一个练习题,都是一次思维的锻炼。
希望这篇文章能让你对“C 练习实例36 – 求100之内的素数”有更深入的理解。下次你再看到素数,不只是数字,而是一段段逻辑、一次次优化、一个个思维的跃迁。
动手写一写,跑一跑,改一改,你会发现,编程的乐趣,就藏在这些小小的练习里。