素数筛选方法

素数筛选方法

题目来源

Leet Code204:计数质数

题目描述如下:
给定整数n,返回 所有小于非负整数n的质数的数量 。
题目输入:
n=10
题目输出:
4
解释:
小于10的质数一共有4个,它们是2,3,5,7

对于所有参加过算法竞赛的同学,质数的判断一不会陌生,每次碰见,都是老生常谈。
但在这个官方题解中,有了另外两种判定质数的方法。一起来探索一下质数判定的三种方法(最后一个方法逻辑性很强):

1.平方根枚举

一种非常直观的方法,就是从[2,n-1]逐个遍历,判断是否存在整数,是n的质因子,例如4=2*2,6=2*3,12=2*6,12=3*4等等,发现不满足质数的定义,从而判定为合数。
进一步改进,设n的两个质因子为p,q,不妨设p<=q,则我们可以发现,p<=sqrt(n),如果是从[2,sqrt(n)]检验n的质因子,那么时间复杂度就直接从O( n n n)降低到O( n \sqrt{n} n ),而我最常采用的也是这种方法。

public static int countPrimes(int n) {
    Vector<Integer> primes=new Vector<>();//保存质数的结果
    // 针对[2-n)的每一个数字,使用sqrt(i)的方法,检查是否是质数
    for(int i=2;i<n;i++){
        primes.add(i);
        // 从2开始,一直遍历到sqrt(i),检验是否有多余的质因子
        for(int j=2;j<=Math.sqrt(i);j++){
            if(i%j==0){
                primes.remove(primes.size()-1);
                break;
            }
        }
    }
    return primes.size();
}

解法没错,当放到leet_code中,会发现,系统提示超出时间限制

2.埃氏筛

从上面的方法中,有着明显的弊端,比如4999999这个质数,他一共要运行2236次,才可以判断出是质数,而这对于更大的数字的话,花费的次数更多。在[2,n)的判断过程中,很多2、3的倍数都进行了一次计算,存在资源浪费。
埃氏筛提出了一种方法,减少质数倍数的判断次数,既:

如果i是一个质数,那么i*2,i*3,i*4,…这些数都不是质数,可以直接进行标记

到这里,似乎还可以优化,比如6可以被2*3和3*2进行标记,两次重复的标记是所有程序员都不想看到的,如何减少标记?
最直观的方法,就是由最小的i进行标记,也就是用2标记,而不用3进行标记,这样标记次数直接少一半,而2<3。
所以当质数为i时,i*[2,i-1)都由小于i的整数进行标记,此时,质数i只需要标记i*[i…]这些数字

public static int countPrimes(int n) {
    Vector<Integer> primes=new Vector<>();//保存质数的结果
    boolean[] isPrime=new boolean[n];//标记是否是质数
    Arrays.fill(isPrime, true);//埃氏筛使用来标记和数的,那么没有被标记的都是质数,先填充
    // 查找标签i
    for(int i=2;i<n;i++){
        if(isPrime[i]==true){//找到质数,进行一系列标记
            primes.add(i);//并且添加质数
            // 注意这里需要防止溢出
            if((long)i*i<n){
                for(int j=i;(long)i*j<n;j++){
                    isPrime[i*j]=false;
                }
            }
        }
    }
    return primes.size();
}
3.线性筛

到了这一步,leetcode官方题解,提供了第三种解法,就是线性筛。比如针对45,45=3*15,45=5*9。如果使用埃式筛,则会发现45同时被3和5标记一次,虽然避免了由15和9进行重复标记的尴尬。但是仍然存在多次标记,如何才能够让每个和数只用被标记一次呢?
线性筛维护一primes数组,用来存放质数的结果,如果x是质数,则加入primes数组,关键步骤:

对每个整数x,使用primes集合中的数与x相乘,标记和数,且在x%primesi=0时,终止。

问题在于,为什么这样做就能够减少标记次数,并且不会遗漏?
核心思想:每个和数都有最小质因子,通过最小质因子来筛选,一定可以筛选,而且只用筛选一次
解答:如果x从2开始一直增加直到,能被primesi整除,那么x一定是k倍primesi
当k=1时,如果和数y<=x*primesi中出现比primesi还小的质因子,则会在遍历到x的过程中,被x筛选,比如6=2*3,当x遍历到2时,2为最小质因子,直接进行标记。
当k>1时,primesi是x的最小质因子,比如4=2*2,那么和数4之前已经由x=2标记一次,2为最小质因子,标记8的时候,因为2为最小质因子,用primesi中的2直接标记,减少了使用x=2来标记,这样标记次数就减少一次。
最终:保证每个和数用最小质因子筛选,并且只用筛选一次。

public static int countPrimes(int n) {
    // 使用线性筛
    Vector<Integer> primes=new Vector<>();//保存素数的结果
    boolean[] isPrime=new boolean[n];//记录对应位置的i是否是素数
    Arrays.fill(isPrime, true);
    for(int i=2;i<=n;i++){
        // 先检查一下i是否是素数
        if(isPrime[i]==true){
            primes.add(i);
        }
        // 利用现在已经有的素数结果,判断是否是存在和数i*primes
        for(int j=0;j<primes.size() && i*primes.get(j)<n;j++){
            isPrime[i*primes.get(j)]=false;
            // 如果i超过了素数,且已经是倍数,则停止标记
            if(i%primes.get(j)==0){
                break;
            }
        }
    }
    return primes.size();
}

总结

在这里讨论了素数筛选的三种方法,1.平方根枚举 2.埃氏筛 3.线性筛,其中平方根枚举最常用,埃氏筛理解起来不会很费劲,学完之后很容易掌握,所以可以将埃氏筛作为主要的素数筛选方法,最后的线性筛,逻辑性比较强,不过弄懂后,可以用作备选方案。

Tips:

欢迎一起来肝算法题,当你面对各种杂七杂八的机试题(没有最多,只有更多),不就是代码吗?肝它!!!!!
LeetCode代码仓库(以及部分面试算法题)

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值