小于等于n的素数的个数(埃式筛选法和欧拉筛选)

本文介绍了两种高效计算小于等于n的素数个数的方法:埃式筛选法和欧拉筛选法,并提供了具体实现代码。

问题描述

给定数字n,求出小于等于n的素数的个数,假设n<=1000000

思路

找出数字n之前的所有素数,用数组isprime[i]表示i是否是素数;用num[i]表示小于等于数字i的素数有多少。
那么最重要的就是解决判断一个数是否是素数

方法1

最基础的方法是对每个数从2开始遍历到根号n,判断有没有n的因子,但是显然效率非常低

方法2

埃式筛选法( Eratosthenes筛选)预处理一下
原理:一个合数总是可以分解成若干质数的乘积
初始化isprime数组为1,遍历所有小于题目给定的1000000的数,从1开始判断,如果改数不是素数,那么就将其isprime改成0.
显然1不是素数,num[1]=0;从2开始,如果一个数i是素数(比如2),那么这个数的倍数都不是素数(也就是,4,6,8,10…)这些数至少有一个素数因子了,因此将i的所有倍数的isprime都设为0,而num[i]=num[i-1]+1;

如果一个数i不是素数,那么不用处理,直接num[i]=num[i-1]

代码如下

const int cont = 1000002;
bool isprime[1000002];
int num[1000002];
int main()
{
    int n;
    memset(isprime,1,sizeof(isprime));
    memset(num,0,sizeof(num));
    isprime[1]=0;
    num[1]=0;
    for(int i = 2;i<cont;i++)
    {
        if(isprime[i])//是素数
        {
            num[i]=num[i-1]+1;
            for(int j = 2*i;j<cont;j+=i)//去掉所有倍数
                isprime[j]=0;
        }
        else
            num[i]=num[i-1];
    }
    while(~scanf("%d",&n))
    {
        printf("%d\n",num[n]);
    }
}

分析:
时间复杂度是O(nloglogn)
效率已经比较高,但是做了一些无用功,一个数会被重复筛选很多遍,
比如2和3的倍数中都有6

方法2

欧拉筛选
欧拉筛选的目的就是要不做重复功,筛选过的不再重复筛选,这里的prime[i]=k表示的是第i个素数是k;第一重循环是找素数,第二重还是借助找到的素数去除该数的倍数,只不过去除的方式不同
代码如下:

const int cont = 1000002;;  
int Prime[cont];  
bool vis[cont];  
void prepare()  
{  
    int num = 0;  
    memset(vis,true,sizeof(vis));  
    for(int i = 2; i <= cont; ++i)  
    {  
        if(vis[i])  
            Prime[++num] = i;  
        for(int j = 1; j <= num; ++j)  
        {  
            if (i * Prime[j] > cont)  
                break;  
            vis[i * Prime[j]] = false;  
            if (i % Prime[j] == 0) //表明这个数已经被筛过了 
                break;  
        }  
    }  
}  
<think>我们正在讨论欧拉筛法(也称为线性筛法)在C语言中的实现,用于高效地筛选素数。根据引用[1][2]中的代码,欧拉筛法的核心思想是每个合数只被其最小质因数筛掉一次,从而避免重复筛选,提高效率。 以下是欧拉筛法的基本步骤: 1. 初始化一个布尔数组(或整型数组标记)`flag[]`,用于标记某个数是否为合数(非素数)。初始时,假设所有数都是素数(即`flag`数组初始为0或false)。 2. 创建一个数组`prime[]`用于存储筛选出来的素数。 3. 从2开始遍历到给定的上限n: a. 如果当前数字i未被标记为合数(即`flag[i]`为false),则将它加入素数数组。 b. 遍历已知的素数列表(即`prime`数组中的素数,记为`prime[j]`): 令`x = i * prime[j]`,如果`x`超过n则停止遍历。 将`x`标记为合数(`flag[x]=true`)。 关键点:如果`i`能够被当前素数`prime[j]`整除(即`i % prime[j] == 0`),则停止遍历素数列表。 为什么要在`i % prime[j] == 0`时跳出循环?因为此时`prime[j]`是`i`的最小质因数,那么对于后续的素数`prime[k]`(k>j),`i * prime[k]`的最小质因数应该是`prime[j]`而不是`prime[k]`,为了避免重复标记(这个合数会在后面当`i`变成更大的数,且存在一个更小的质因数`prime[j]`时被标记),所以跳出循环。 现在,我们将根据引用中的代码,整合一个完整的C语言实现,并添加详细注释。 注意:在C语言中,我们需要包含必要的头文件,并且注意数组大小。另外,根据引用[2]提到,C语言中可以使用`<stdbool.h>`来使用布尔类型。 以下是一个完整的C语言实现: ```c #include <stdio.h> #include <stdbool.h> // 为了使用bool类型 #include <string.h> // 为了使用memset int countPrimes(int n) { if (n <= 2) return 0; // 小于2没有素数(01都不是素数) // 创建标记数组素数数组,注意我们只需要0到n-1(因为题目要求小于n的素数) bool flag[n]; // flag[i]为true表示i是合数,为false表示i是素数(或未被标记) int prime[n]; // 存放素数的数组,最多可能有n个素数(实际上远小于n) memset(flag, 0, sizeof(flag)); // 初始化flag数组为0(false) int cnt = 0; // 记录素数个数 // 注意:从2开始,因为我们知道01不是素数 for (int i = 2; i < n; i++) { if (!flag[i]) { // i是素数,加入prime数组 prime[cnt++] = i; } // 遍历当前已有的素数 for (int j = 0; j < cnt && i * prime[j] < n; j++) { flag[i * prime[j]] = true; // 标记合数 // 如果i能被当前素数整除,则跳出内层循环 if (i % prime[j] == 0) { break; } } } return cnt; // 返回小于n的素数个数 } int main() { int n; printf("请输入一个整数n(求小于n的素数个数):"); scanf("%d", &n); int count = countPrimes(n); printf("小于%d的素数有%d个。\n", n, count); return 0; } ``` 注意:上面的程序中,数组`flag``prime`的大小都是`n`,如果`n`很大,可能会占用较多内存。但在一般计算机上,`n`在10^6以内都是可以接受的。 我们再来解释一下欧拉筛法的关键步骤: - 外层循环`i`从2到n-1。 - 内层循环使用已有的素数(`prime[0]`到`prime[cnt-1]`)去筛选合数。 - 当`i`是素数时,它会被加入到素数数组,然后内层循环会用它去标记一些合数(实际上,它会已有的素数相乘来标记合数)。 - 当`i`是合数时,它也能素数相乘来标记合数,但注意在遇到它的最小质因数时跳出循环。 为什么这样能保证每个合数只被标记一次?因为每个合数`x`都可以表示为`x = i * prime[j]`,其中`prime[j]`是`x`的最小质因数。而在外层循环`i`的这次迭代中,当`j`遍历到`x`的最小质因数`prime[j]`时,就会因为`i % prime[j] == 0`而跳出循环,从而保证不会用更大的质数去重复标记`x`。 例如,当`i=4`时,素数数组中有`[2,3]`,首先用4*2=8标记,然后因为4%2==0,跳出循环。这样就不会标记4*3=12。那么12会在什么时候被标记呢?当`i=6`时,用6*2=12标记(因为2是6的最小质因数,所以6%2==0,标记完12后跳出循环,也不会用6*3=18?不对,注意6%2==0,所以内层循环只执行一次(j=0),标记12后跳出,所以18不会被标记?但注意6是合数,它在素数数组中并没有加入,所以素数数组中还是已有的素数[2,3,5,...],所以当`i=6`时,内层循环会取`prime[0]=2`,标记6*2=12,然后因为6%2==0,跳出循环,所以18没有被标记。那么18什么时候被标记?18应该在`i=9`时,用9*2=18标记(因为2是18的最小质因数)?不对,因为18的最小质因数是2,但9是奇数,素数数组中有2,所以9*2=18,然后因为9不能被2整除,所以还会继续?不对,9%2不等于0,所以不会跳出循环,接着用`prime[1]=3`,所以9*3=27,标记27?但18已经被标记了,而27不是18。 这里需要澄清:18会在`i=9`时被标记吗?不会,因为当`i=9`时,内层循环首先用`prime[0]=2`,计算9*2=18,然后标记18,接着判断9%2!=0,所以继续循环;然后用`prime[1]=3`,计算9*3=27,标记27,然后因为9%3==0,跳出循环。所以18在`i=9`时已经被标记了?但注意,18的最小质因数是2,它应该由更小的`i`(即`i=9`?不对,应该由`i=9`乘以2得到?但是`i=9`乘以2时,9%2不为0,所以不会跳出,所以18被标记了。但是,根据欧拉筛法的原理,我们希望每个合数被其最小质因数标记,18的最小质因数是2,那么应该由`i=9`乘以2得到(因为9是18除以最小质因数2得到的?不对,18=2*9,而9不是整数?不对,9是整数。但是,在`i=2`时,我们只能标记4(2*2),因为2%2==0跳出;`i=3`时,标记6(3*2)9(3*3,但3%3==0后跳出,所以9被标记);`i=4`时,标记8(4*2)然后跳出;`i=5`时,标记10(5*2)然后跳出(因为5%2!=0?不对,5不能被2整除,所以会继续标记15(5*3)?不对,5是素数,所以素数数组中会有5,所以5*3=15会被标记,然后5%3!=0,继续?然后5*5=25标记,然后跳出?不对,5%5==0?跳出。所以15被标记了。那么18呢?在`i=6`时,6是合数,所以不会加入素数数组,但内层循环会遍历已有的素数(2,3,5,...),首先6*2=12,标记12,然后6%2==0,跳出循环。所以在`i=6`时标记了12。在`i=9`时,素数数组中有2,3,5,...,所以首先9*2=18,标记18,然后9%2!=0,继续;然后9*3=27,标记27,然后9%3==0,跳出。所以18在`i=9`时被标记。 但是,18的最小质因数是2,而它是由9*2得到的,而9不是偶数,但是9是18的因子(18/2=9)。所以,这个标记过程是符合每个合数由其最小质因数标记的。因为18的最小质因数是2,所以当`i`等于18除以最小质因数的商(即9)时,用最小质因数2去乘,就得到了18。因此,这个算法保证了每个合数只被标记一次。 通过以上代码解释,我们可以实现欧拉筛法来高效求解小于n的所有素数。 相关问题: 1. 欧拉筛法的时间复杂度空间复杂度分别是多少? 2. 欧拉筛法比埃氏筛法效率高在什么地方? 3. 除了求素数个数,如何修改代码来输出所有素数? 4. 如果给定的n很大(例如超过10^8),如何优化欧拉筛法的内存使用? </think>### C语言实现欧拉筛法求素数 欧拉筛法(又称线性筛)是一种高效的素数筛选算法,核心思想是**每个合数只被其最小质因数筛除一次**,时间复杂度为$O(n)$。以下是详细实现原理分析: #### 完整代码实现 ```c #include <stdio.h> #include <stdbool.h> // 布尔类型支持 #include <string.h> // memset函数 int eulerSieve(int n) { if (n <= 2) return 0; // 处理边界情况 bool isComposite[n]; // 标记合数数组 int primes[n]; // 存储素数 memset(isComposite, 0, sizeof(isComposite)); // 初始化为false int count = 0; // 素数计数器 for (int i = 2; i < n; i++) { // 当前数未被标记为合数 → 是素数 if (!isComposite[i]) { primes[count++] = i; // 记录素数 } // 用当前数已知素数筛选合数 for (int j = 0; j < count && i * primes[j] < n; j++) { isComposite[i * primes[j]] = true; // 标记合数 // 关键步骤:遇到最小质因子时终止 if (i % primes[j] == 0) break; } } return count; // 返回素数个数 } int main() { int n; printf("输入上限n: "); scanf("%d", &n); int primeCount = eulerSieve(n); printf("小于%d的素数有%d个\n", n, primeCount); return 0; } ``` #### 核心原理详解 1. **算法流程** - 初始化`isComposite`数组标记合数(默认全为素数) - 遍历$2$到$n-1$的每个数$i$: - 若$i$未被标记 → 添加到素数表 - 用当前$i$与素数表每个元素$p_j$相乘: $$ \text{标记 } i \times p_j \text{ 为合数} $$ $$ \text{当 } i \equiv 0 \pmod{p_j} \text{ 时终止内层循环} $$ 2. **关键优化**(`i % primes[j] == 0`) - 保证每个合数$N$只被其**最小质因数**筛除 - 例如$12=2×6$: - 当$i=6, p_j=2$时标记$12$($6$含质因子$2$) - 避免在$i=4, p_j=3$时重复标记$12$ - 数学证明:若$p_j \mid i$,则对任意$k>j$有: $$ \text{最小质因数}(i \times p_k) = p_j \neq p_k $$ 3. **复杂度分析** - 时间复杂度:$O(n)$(每个数只被筛一次) - 空间复杂度:$O(n)$(需$O(n)$存储空间) - 对比埃氏筛$O(n \log \log n)$的优势:消除重复标记 #### 应用场景 1. 需要快速获取大范围内素数数量(如$n=10^7$) 2. 素数密度相关计算(如素数定理验证) 3. 密码学中快速生成素数 --- ### 相关问题 1. **欧拉筛法在筛选过程中如何确保不重复标记合数?** (关键:最小质因子筛选机制) 2. **当处理超大范围(如$n>10^8$)时,欧拉筛法可能遇到什么性能瓶颈?如何优化?** (考虑内存占用缓存效率) 3. **欧拉筛法与埃氏筛法在时间空间复杂度上有何本质区别?** (数学推导:$O(n)$ vs $O(n \log \log n)$) 4. **如何修改代码使其输出所有筛选出的素数而不仅仅是数量?** (扩展:素数存储与输出格式设计) [^1][^2]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值