题目概述:

  1. 请实现一个函数,对于给定的整型参数 n,该函数能够把自然数中, 小于 n 的质数/素数, 从小到大 打印出来。

用C语言实现求质数/素数(第三天:顺序、选择、循环等语句的应用)【每天进步一点点-小白学习笔记】_笔记

示例:

输入:n = 10
输出:2 3 5 7


2.请实现一个函数,对于给定的整型参数 n,该函数能够 从小到大 ,依次打印出自然数中 最小 的 n 个质数/素数。

用C语言实现求质数/素数(第三天:顺序、选择、循环等语句的应用)【每天进步一点点-小白学习笔记】_学习_02

示例:

输入:n = 10
输出:2 3 5 7 11 13 17 19 23 29

提示:

1素数指在正整数范围内,大于1且只能被1和自身整除的数

2采用多种方式求解,尽可能优化算法

31 < n <= 1000

解题思路:

求素数是基本上每个刚接触编程的小白都可能面对的一道经典题,其资历也可以算一位老精英怪了,以至于许多人都会拿其来练练手,当然我也是。刚好下午刷博客的时候看了一篇《素数求解的n种境界》,感觉有些“吃撑了”,正好用自己的理解也尝试打打这位资深精英怪来“消消食”。

素数求解我目前能想到的方法就两种:试除法与筛除法

方法①:试除法

试除法是基本绝大多数人都能想到的一种方法,当然我也是第一时间先想到了这个。

简单来说就是从2开始不断对小于该数的数字取模(求余),若余数一直到试除完所有正整数都不为0,则为质数/素数,并打印输出。因此可以写出代码如下:

// 题1:输入正整数n,求n以内的素数
int count, n;
printf("请输入正整数n:");
scanf("%d", &n);
printf("%d以内的质数有:2 ", n);
count = 1;  // 计入最小素数2
for (int i = 3; i <= n; i++) {
    int j = 0;
    for (j = 2; j < i; j++) {
        if (i % j == 0)
            break;
    }
    if (j == i){
        printf("%d ", i);
        count++;
    }
}
printf("\n共%d个",count);
// 题2:输入正整数n,求n个最小素数
int count, n;
printf("请输入正整数n:");
scanf("%d", &n);
printf("%d个最小质数为:2 ", n);
count = 1;  // 计入最小素数2
for (int i = 3; count < n; i++) {
    int j = 0;
    for (j = 2; j < i; j++) {
        if (i % j == 0)
            break;
    }
    if (j == i){
        printf("%d ", i);
        count++;
    }
}

这个代码已经可以完成题目需求,但这个算法每次都要比较2到n的所有整数,计算效率低、空间占用高,而题目也说需要我们尽可能优化算法。那么有什么办法可以进一步提高这个算法的效率与空间利用率呢?

再次观察素数规律:4 = 2×2或1×4不是素数、100 = 10×10或2×50或4×25或1×100不是素数、25 = 5×5或1×25不是素数……不难发现,每个数如果从小到大逐个取模比较,最大仅仅需要比较到√i(i为原数)的位置即可找到所有约数(用均值不等式、绝对值不等式等数学方法也可进行求证),因此可以进一步优化算法如下:

// 使用sqrt()开方函数一定不能忘了导入头文件
#include<math.h>
// 题1:输入正整数n,求n以内的素数
int count, n;
printf("请输入正整数n:");
scanf("%d", &n);
printf("%d以内的质数有:2 ", n);
count = 1;  // 计入最小素数2
for (int i = 3; i <= n; i++) {
    int j = 0;
    for (j = 2; j <= sqrt(i); j++) {
        if (i % j == 0)
            break;
    }
    if (j > sqrt(i)){
        printf("%d ", i);
        count++;
    }
}
printf("\n共%d个",count);
// 题2:输入正整数n,求n个最小素数
int count, n;
printf("请输入正整数n:");
scanf("%d", &n);
printf("%d个最小质数为:2 ", n);
count = 1;  // 计入最小素数2
for (int i = 3; count < n; i++) {
    int j = 0;
    for (j = 2; j <= sqrt(i); j++) {
        if (i % j == 0)
            break;
    }
    if (j > sqrt(i)){
        printf("%d ", i);
        count++;
    }
}

如此一来,代码效率便大大优化,这里附上试除法完整代码如下:

#define _CRT_SECURE_NO_WARNINGS  
#include<stdio.h>
#include<math.h>
 
 
int main() {
    // 题1:输入正整数n,求n以内的素数
    int count, n;
    // ① 试除法
    // 题1:输入正整数n,求n以内的素数
    printf("方法①:试除法:\n题1:输入正整数n,求n以内的素数\n\n**运行结果**\n请输入正整数n:");
    scanf("%d", &n);
    printf("\n%d以内的质数有:2 ", n);
    count = 1;  // 计入最小素数2
    for (int i = 3; i <= n; i++) {
        int j = 0;
        for (j = 2; j <= sqrt(i); j++) {
            if (i % j == 0)
                break;
        }
        if (j > sqrt(i)) {
            printf("%d ", i);
            count++;
        }
    }
    printf("\n**************\n");
    printf("其中素数共%d个\n**************\n\n--------------------分割线--------------------\n\n", count);


    // 题2:输入正整数n,求n个最小素数
    printf("方法①:试除法:\n题2:输入正整数n,求n个最小素数\n\n**运行结果**\n请输入正整数n:");
    scanf("%d", &n);
    printf("\n%d个最小质数为:2 ", n);
    count = 1;  // 计入最小素数2
    for (int i = 3; count < n; i++) {
        int j = 0;
        for (j = 2; j <= sqrt(i); j++) {
            if (i % j == 0)
                break;
        }
        if (j > sqrt(i)) {
            printf("%d ", i);
            count++;
        }
    }
    printf("\n");
 
    return 0;
}

成功运行如下:

用C语言实现求质数/素数(第三天:顺序、选择、循环等语句的应用)【每天进步一点点-小白学习笔记】_笔记_03

 方法②:筛除法

   其实用完试除法之后,我们已经可以解决上述两个问题了,但事实上当数字大了许多之后,其实整体代码效率还是比较低,这里针对上述代码我尝试添加两行计算循环次数的代码进行检验其运行速率:

int cycount = 0;
 
// 循环体
cyccount++;
// 循环体
 
printf("\n循环次数:%d次\n", cycount);

运行结果:

用C语言实现求质数/素数(第三天:顺序、选择、循环等语句的应用)【每天进步一点点-小白学习笔记】_笔记_04

可以发现,当n = 200时,问题1中循环次数就有600多次,问题2更是到了将近7000次,由此可见,效率仍然比较低,因此仍需继续优化。《素数求解的n种境界》中有一种十分优越的编程思想:筛法思想即尽可能多的筛除某个范围的所有不满足条件且有一定规律的数据,剩余数据即我们所需

在此处,2是公认最小的质数,所以,先把所有2的倍数去掉;然后剩下的那些大于2的数里面,最小的是3,所以3也是质数;然后把所有3的倍数都去掉,剩下的那些大于3的数里面,最小的是5,所以5也是质数……不断筛除合数,最后仅剩质数。那么用代码如何实现呢?

显然,首先需要一个很大的容器来存储这些数,为了尽可能减少内存空间使用,我使用bool类型数组进行存储,内存大小我用之前杨辉三角中所用到的动态分配数组来进行定义,然后用memset函数进行初始化。很快我们便可打出问题①的代码如下:

// 题1:输入正整数n,求n以内的素数
int n = 0;
printf("方法②:筛除法:\n题1:输入正整数n,求n以内的素数\n\n**运行结果**\n请输入正整数n:");
scanf("%d", &n);
printf("\n%d以内的质数有:", n);
int cycount = 0;  // 重置循环次数
int count = 0;  // 重置质数个数
bool* zhishu = (bool*)malloc(sizeof(bool) * (n + 1));
memset(zhishu, true, sizeof(zhishu));  // 设置所有数都可能为质数
// 从2开始判断质数并打印
for (int i = 2; i <= n; i++) {
    if (zhishu[i]) {
        printf("%d ", i);
        count++;
        // 每打印一个质数便将其倍数全部筛除,不再重复循环(埃拉托色尼筛选法)
        for (int j = i * i; j <= n; j += i) {
            zhishu[j] = false;
            cycount++; 
        }
    }
    else
        cycount++;
}
printf("\n**************\n循环次数:%d次\n", cycount);
printf("其中素数共%d个\n**************\n\n", count);

用C语言实现求质数/素数(第三天:顺序、选择、循环等语句的应用)【每天进步一点点-小白学习笔记】_C语言_05

对比之前的试除法可以发现,筛除法的循环次数有了显著的降低,代码效率也提高了不少。于是我特别开心,打算故技重施,再尝试用筛除法解决第二题,然后我的笑容僵住了,再次陷入困境。因为问题2给出的n,表示需要打印的质数的个数,那么这n个质数会分布在多大的范围我根本不知道,所以我很难确定我的bool容器大小,于是就很难再次定义数组了。怎么办?真就只差临门一脚。于是我再次回到资料查阅的路上,然后在一次高等数学复习网课中,我看到了素数定理,又找回了希望。

素数定理是指在大数极限下,素数的分布与自然对数的比值成正比,具体表述为:在区间 [1, x] 中的素数数量大约为用C语言实现求质数/素数(第三天:顺序、选择、循环等语句的应用)【每天进步一点点-小白学习笔记】_C语言_06反过来说n个最小素数的最大值区间为: 用C语言实现求质数/素数(第三天:顺序、选择、循环等语句的应用)【每天进步一点点-小白学习笔记】_C语言_07

   由此可以开始进行编码:

// 题2:输入正整数n,求n个最小素数
printf("方法②:筛除法:\n题2:输入正整数n,求n个最小素数\n\n**运行结果**\n请输入正整数n:");
scanf("%d", &n);
printf("\n%d个最小质数为:", n);
int MAX_2 = n * log(n) + n * log(log(n));
cycount = 0;  // 重置循环次数
count = 0;  // 重置质数个数
bool* zhishu2 = (bool*)malloc(sizeof(bool) * MAX_2);
memset(zhishu2, true, sizeof(zhishu2));  // 设置所有数都可能为质数
// 从2开始判断质数并打印
for (int i = 2; count < n; i++) {
    if (zhishu2[i]) {
        printf("%d ", i);
        count++;
        // 每打印一个质数便将其倍数全部筛除,不再重复循环(埃拉托色尼筛选法)
        for (int j = i * i; j <= MAX_2; j += i) {
            zhishu2[j] = false;
            cycount++;
        }
    }
    else
        cycount++;
}
printf("\n**************\n循环次数:%d次\n**************\n", cycount);

运行结果:

用C语言实现求质数/素数(第三天:顺序、选择、循环等语句的应用)【每天进步一点点-小白学习笔记】_笔记_08

再次对比试除法可以发现效率提升更加显著。再将几次代码放到一起对比大数据结果如下:

#define _CRT_SECURE_NO_WARNINGS  
#include<stdio.h>
#include<math.h>
#include <stdbool.h> 
 
 
int main() {
    int count, n, cycount;
    // ① 试除法
    // 题1:输入正整数n,求n以内的素数
    printf("方法①:试除法:\n题1:输入正整数n,求n以内的素数\n\n**运行结果**\n请输入正整数n:");
    scanf("%d", &n);
    printf("\n%d以内的质数有:2 ", n);
    cycount = 0;  // 重置循环次数
    count = 1;  // 计入最小素数2
    for (int i = 3; i <= n; i++) {
        int j = 0;
        for (j = 2; j <= sqrt(i); j++) {
            cycount++;
            if (i % j == 0)
                break;
        }
        if (j > sqrt(i)) {
            printf("%d ", i);
            count++;
        }
    }
    printf("\n**************\n循环次数:%d次\n", cycount);
    printf("其中素数共%d个\n**************\n\n--------------------分割线--------------------\n\n", count);
    
 
    // 题2:输入正整数n,求n个最小素数
    printf("方法①:试除法:\n题2:输入正整数n,求n个最小素数\n\n**运行结果**\n请输入正整数n:");
    scanf("%d", &n);
    printf("\n%d个最小质数为:2 ", n);
    cycount = 0;  // 重置循环次数
    count = 1;  // 计入最小素数2
    for (int i = 3; count < n; i++) {
        int j = 0;
        for (j = 2; j <= sqrt(i); j++) {
            cycount++;
            if (i % j == 0)
                break;
        }
        if (j > sqrt(i)) {
            printf("%d ", i);
            count++;
        }
    }
    printf("\n**************\n循环次数:%d次\n**************\n\n--------------------分割线--------------------\n\n", cycount);
 
 
    // ② 筛除法
    // 题1:输入正整数n,求n以内的素数
    printf("方法②:筛除法:\n题1:输入正整数n,求n以内的素数\n\n**运行结果**\n请输入正整数n:");
    scanf("%d", &n);
    printf("\n%d以内的质数有:", n);
    cycount = 0;  // 重置循环次数
    count = 0;  // 重置质数个数
    bool* zhishu1 = (bool*)malloc(sizeof(bool) * (n + 1));
    memset(zhishu1, true, sizeof(zhishu1));  // 设置所有数都可能为质数
    // 从2开始判断质数并打印
    for (int i = 2; i <= n; i++) {
        if (zhishu1[i]) {
            printf("%d ", i);
            count++;
            // 每打印一个质数便将其倍数全部筛除,不再重复循环(埃拉托色尼筛选法)
            for (int j = i * i; j <= n; j += i) {
                zhishu1[j] = false;
                cycount++;
            }
        }
        else
            cycount++;
    }
    printf("\n**************\n循环次数:%d次\n", cycount);
    printf("其中素数共%d个\n**************\n\n--------------------分割线--------------------\n\n", count);
 
 
    // 题2:输入正整数n,求n个最小素数
    printf("方法②:筛除法:\n题2:输入正整数n,求n个最小素数\n\n**运行结果**\n请输入正整数n:");
    scanf("%d", &n);
    printf("\n%d个最小质数为:", n);
    int MAX_2 = n * log(n) + n * log(log(n));
    cycount = 0;  // 重置循环次数
    count = 0;  // 重置质数个数
    bool* zhishu2 = (bool*)malloc(sizeof(bool) * MAX_2);
    memset(zhishu2, true, sizeof(zhishu2));  // 设置所有数都可能为质数
    // 从2开始判断质数并打印
    for (int i = 2; count < n; i++) {
        if (zhishu2[i]) {
            printf("%d ", i);
            count++;
            // 每打印一个质数便将其倍数全部筛除,不再重复循环(埃拉托色尼筛选法)
            for (int j = i * i; j <= MAX_2; j += i) {
                zhishu2[j] = false;
                cycount++;
            }
        }
        else
            cycount++;
    }
    printf("\n**************\n循环次数:%d次\n**************\n", cycount);
 
 
    return 0;
}

用C语言实现求质数/素数(第三天:顺序、选择、循环等语句的应用)【每天进步一点点-小白学习笔记】_C语言_09

自此,质数的计算这题便简单解决了,虽然实际上还存在许多隐藏的bug,比如更大的数据容易出现越界与内存分配失败的问题等等,这些只能等到我变得更强了之后再来二刷了。