求n以内素数的埃氏筛法与欧拉筛法 详解

假定要求1-maxn范围内的所有素数并保存在数组中,下面是4种方法的分析。

这些方法也可通过预处理来判断素数

这些方法的时间复杂度从上到下越来越低。

方法一:逐个判断(普通)

bool isPrime(int num )
{
    for(int i= 2; i*i <= num ; i++)
      if(num %i== 0)
         return 0 ;
    return 1 ;
}
//若一个数可以进行因数分解,得到的两个数一定是一个小于等于sqrt(n),一个大于等于sqrt(n)。
//据此,上述代码中并不需要遍历到n-1,遍历到sqrt(n)即可。
//因为若sqrt(n)左侧找不到约数,那么右侧也一定找不到约数。
int main(){
    int prime[1000],count=0;//prime数组中能存放1000个素数
    for(int i=2;i<=maxn;i++)//判断范围为[2,maxn]
        if(isPrime(i)) prime[count++]=i;
    return 0;
}

至此方法一能得到<=maxn的所有素数,时间复杂度O(n√n)

方法二:埃氏筛法

埃氏筛法

int main()
{
    static bool isPrime[maxn];//利用static特性将数组值全部置为0 即false
    isPrime[0]=isPrime[1]=ture;//1 0 不是素数标记为ture
    for(int i=2;i<=maxn;i++)//从2开始往后筛  直至maxn
    {
        if(!isprime[i])//如果是素数,则用它来筛出它的倍数(它的倍数一定不是素数)
            for(int j=2*i;j<=maxn;j+=i)//起始值2*j,随后变成3*j 4*j,直至大于maxn结束
                isprime[j]=ture;     //将其倍数标记为ture 不是素数
    }
}

尽管时间复杂度比上面优秀 但还是能发现存在不足: 有数被重复筛选了。如 12=2*6=3*4=4*3

方法三:埃氏筛法改进

我们可以将循环初始值直接设置为int j=i*i,减少筛选次数。原因如下:

首先补充一个数论知识:任何一个大于1的自然数 ,如果N不为素数,都可以唯一分解成有限个素数的乘积数论

为什么不需要遍历(2~i-1)*i部分呢? 因为他们肯定已经被遍历过了 。
对于2~i-1中的素数in,它肯定是小于i的,在i进入循环前就已经通过2*in , 3*in , 4*in , ......i*in 的形式标记过了 in*i ,所以不需要标记。
对于2~i-1中的非素数ik,既然是非素数,可以拆分成素数之积 如20=2*2*5,假设此时ik=20,那么ik*i是不是可以写成 2*(2*5*i)的形式呢,拆分出来的素数肯定小于i,那么它已经通过2*2, 3*2, 4*2,...,(2*5*i)*2的形式被标记了。
至此我们证明了可以直接设置循环初始条件为int j=i*i
时间复杂度相比改进前而言降低了
代码与上面比较只改变了一处

int main()
{
    static bool isPrime[maxn];
    isPrime[0]=isPrime[1]=ture;
    for(int i=2;i<=maxn;i++)
    {
        if(!isprime[i])
            for(int j=i*i;j<=maxn;j+=i)//改变处
                isprime[j]=ture;
    }
}

如102=2*51=3*34=17*6(前一个乘积是素数) ,对于17*6来说,如果将其初始设置为2*17,那么它不可避免会重复筛到102即6*17。改进后初始值设置为17*17,那它不会筛到102,减少了无效筛选次数。当然这只是一点点改进,比如对于3*34来说,就算设置为3*3,它也会筛选到102.还有一种更好的筛法可以来保证每个合数只被筛选一次,那就是下面的欧拉筛法。

方法四:欧拉筛法(线性筛)

基本思想:任何合数都能表示成多个素数的积。所以,任何的合数肯定有一个最小质因子。我们通过这个最小质因子就可以判断什么时候不用继续筛下去了。

代码很短,关键点理解方式我以注释的形式写出来了。

#include<stdio.h>
#include<string.h>
using namespace std;
int main()
{
    int maxn,cnt=0;
    static int prime[1000];//能存1000个素数   static特性能将其全部初始化为0
    scanf("%d", &maxn);
    static bool vis[maxn];//每个数都初始化标签为0 即false
    for(int i = 2; i <= maxn; i++)
    {
        if(!vis[i])//不是目前找到的素数的倍数 (即为素数)
        prime[cnt++] = i;//找到素数 按顺序存入prime数组
        for(int j = 0; j<cnt && i*prime[j]<=n; j++) //从小到大取出数组中的素数
        {       //进入这个for循环的i都是合数哦
            vis[i*prime[j]] = true;//找到的素数的倍数标记,最初j=0,prime[j]是第一个素数2
            if(i % prime[j] == 0) break;//关键!!!!
            //首先我们规定每个合数只能被含有它最小素数因子的等式表达,即i*prime[]_min
            //例子 6=2*3   9=3*3  20=2*10  它们的最小素数因子是2,3 ,2
            //对于6 9 20这三个数,只有i=3,3,10时能被标记,i*合数的最小素数因子=合数
            //这样就保证只被合法的i进入循环时标记一次这个合数
            //素数因子是放在prime里面的,从小到大取出
            //从素数2开始,2乘i肯定是这个合数的含最小素数因子的等式表达,因为最小素数就是2
            //那么接下来判断i%prime[j]==0
            //i%prime[j]==0表示素数prime[j]是i的因子
            //如果是,为什么要break呢?
            //因为如果是,就表示这个i能拆成 prime[j]*N(N为整数) 的形式
            //那么对于prime[j+1],也就是下一个素数,i * prime[j]就不是那个含最小素数因子的表达式了,
            //i*primer[j]=prime[j]*N*prime[j+1] ,这里面还有个小于prime[j+1]素数因子prime[j]可以分出来。
            //由规定,这个i就不合法了,你继续往后prime[j+n]也没用了,总是能分出一个小于它们的prime[j]
            //所以直接break,让下一个i进入。
            //不用担心后面的合数不会被标记到
            //这个公式i*primer[j]=prime[j]*N*prime[j+1]  合法的i=N*prime[j+1]
            //而当前break的i=N*prime[j]  因为prime[j+1]>prime[j]
            //我们要找的i在后面。
            //这是我对于这一行的理解方式
        }
    }
    return 0;
}

欧拉筛法每个数只扫过一遍,所以是线性的,时间复杂度O(n)

猜你喜欢

转载自www.cnblogs.com/linme/p/11868777.html