有关素数的部分问题的分析与整理

定义:一个大于1的,除了1和它本身外,不能被其他自然数整除的整数,称为质数(prime number)又称素数。合数,指自然数中除了能被1和本身整除外,还能被其他数(0除外)整除的数。与之相对的是质数,而1既不属于质数也不属于合数。

问题一:判断单个数n是否为素数。

思路:如果该数大于或等于2,则遍历2到根号n区间的所有整数a,将n对a取余,如果余数全部不为0则n为质数。将上句话中的“整数a”改为“2与奇数a”、“素数a”,运算的时间越来越短。合数可以分解成若干素数的乘积。大于2的偶数已经是含有乘积项2的合数,n对其取余如果等于0则n对2取余必为0,n对其取余如果不为0则n对2取余也有可能为0,因此对大于2的偶数取余是完全多余的。同理,奇数里面也有合数,也可被分解为若干素数之积。故遍历素数最优,但这会牺牲空间记录素数。故2与奇数a”兼具较好的时间复杂度与空间复杂度。

问题二:统计不大于N的所有素数。

思路:从2到N遍历然后用问题一解法判断,暴力解法不考虑。考虑筛选法:从2到N中筛掉合数,剩下的就是素数。怎么筛,有两种方法。

埃式筛选:

2到N遍历,遇到素数,就筛掉这个素数的所有倍数(做上标记),这里的素数判断不用问题一的方法,凡是没被标记的数就是素数

import java.util.Scanner;
public class Main{
public static void getPrime(int n) {
    if (n <= 1) {
        System.out.println("Error,N should satisfy N>1");
    } else {
        boolean[] flag = new boolean[n + 1];
        int[] prime = new int[n];
        int count = 0;
        for (int i = 2; i <= n; i++) {
            if (!flag[i]) {
                prime[count++] = i;
                for (int j = i * 2; j <= n; j += i) {
                    flag[j] = true;
                }
            }
        }
        System.out.println("不大于"+n+"的所有素数一共有"+count+"个:");
        for (int i=0;i<count;i++) {
            System.out.print(prime[i]+" ");
        }
    }
}

    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        if (scan.hasNext()){
            int N=scan.nextInt();
            Main.getPrime(N);
        }
    }
}

显而易见,空间复杂度为O(N),时间复杂度为O(N log logN)。

分析:对每个素数a,要进行N/a次运算筛掉倍数,则总次数为N*(从 1 到 N 之间的 1/a 之和) (从 1 到 N 之间的 1/a 之和) 约等于 log (对所有 k 从 1 到 N 之间的 1/k 之和),后者括号里面是 log N,所以前者就是 log logN。

该方法存在重复筛的问题,即对某个合数标记了不止一次,因此时间复杂度不是线性的。比如30 =2 * 15 =3 * 10 = 5* 6。

埃式筛法改进:

boolean[] flag = new boolean[n + 1];
        int[] prime = new int[n];
        int count = 0;
        for (int i = 2; i <= n; i++) {
            if (!flag[i]) {
                prime[count++] = i;
                for (int j = i * i; j <= n; j += i) {//从i*i开始稍微优化了一下,但还是存在重复筛
                    flag[j] = true;
                }
            }
        }

欧拉筛选:

这是一种快速线性素数筛法,避免了重复筛的问题,保证每个合数只会被它的最小质因数筛去。

public static void getPrime(int n) {
        if (n <= 1) {
            System.out.println("Error,N should satisfy N>1");
        } else {
            boolean[] flag = new boolean[n + 1];
            int[] prime = new int[n];
            int count = 0;
            for (int i = 2; i <= n; i++) {
                if (!flag[i]) {
                    prime[count++] = i;
                }
                for (int j = 0; j <count  && prime[j] * i <=n; j++) {
                        flag[i * prime[j]] = true;
                        if (i % prime[j] == 0)//关键步骤
                        {break;}
                    }
            }
            System.out.println("不大于"+n+"的所有素数一共有"+count+"个:");
            for (int i=0;i<count;i++) {
                System.out.print(prime[i]+" ");
            }
        }
    }

关键步骤:对每个i不论素数与否都准备筛掉i的 小于等于i的素数 倍的数,若i是某个素数prime[j]的倍数,则i*(大于prime[j]的素数)这个数 不应该由 大于prime[j]的素数 来筛,而是应该由prime[j]来筛,故在这个地方要break,确保了每个合数只会被它的最小质因数筛去。因此该算法时间复杂度和空间复杂度都为O(N)。

问题三:统计不大于N的所有素数及素数幂。(2018春微众银行笔试)

借鉴问题二欧拉筛法的思想,可以得到一种线性时间算法。如下,通过设置三种标志位,确保每个合数只被筛一次。也可采用问题二的欧拉筛法,得到素数序列后,遍历素数取幂 来解决,这种方法就相当于第一轮先筛掉了素数幂,然后第二轮又把素数幂捡起来了。

import java.util.Scanner;//输入一个正整数,统计不大于这个数的所有素数与素数幂

public class Prime {
    public static void getPrimeAndPrimePower(int n){
        if(n<=1){System.out.println("Error,N should satisfy N>1");}else {

            byte[] flag =new byte[n+1]; //没赋值默认为0,flag标志:合数为1,素数为0,素数幂为2
            int[] prime=new int[n];
            int count1=0;
            int count2=n-1;
            for (int i = 2; i <= n; i++) {
                
                if (flag[i]==0) {
                    prime[count1++]=i;
                    for (int j = 0; j <count1-1  && prime[j] * i <=n; j++) {
                        flag[i * prime[j]] = 1; }
                    if(i * i <=n){flag[i*i]=2;}
                }

                if (flag[i]==1) {
                    for (int j = 0; j <count1  && prime[j] * i <=n; j++) {
                        flag[i * prime[j]] = 1;
                        if (i % prime[j] == 0)
                        { break;}
                    }
                }

                if (flag[i]==2) {
                    prime[count2--]=i;
                    for (int j = 0; j <count1  && prime[j] * i <=n; j++) {
                        if (i % prime[j] != 0)
                        {flag[i * prime[j]] = 1;}
                        else
                        { flag[i * prime[j]] = 2;
                        break;}
                }
                }

                /*//3合1
                if (flag[i]==0) { prime[count1++]=i;}
                if (flag[i]==2) { prime[count2--]=i;}
                for (int j = 0; j <count1  && prime[j] * i <=n; j++) {
                    flag[i * prime[j]] = 1;
                    if (i % prime[j] == 0)//这里是精髓
                    { if(flag[i]==2){flag[i * prime[j]] = 2;}
                        break;}
                }
                if(flag[i]==0&&i*i<=n){flag[i*i]=2;}//这里将素数的平方的flag标志重新覆蓋了一次*/

            }

            System.out.println("不大于"+n+"的所有素数与素数幂一共有"+(count1+n-count2-1)+"个:");
            for (int i = 0; i <count1; i++)
            {System.out.print(prime[i]+" ");}
            for (int i = count2+1; i <n; i++)
            {System.out.print(prime[i]+" ");}
        }
    }
    
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        if (scan.hasNext()) {
            int N=scan.nextInt();
            Prime.getPrimeAndPrimePower(N);
        }
    }
        }

点赞