你一定要会的埃式筛法和欧式筛法求质数

大一时学过的算法,两年之后忘的一干二净,看了网上很多文章终于又弄懂了,尽量用通俗的说法说明白,避免再忘。

原题链接

题目描述

给定整数 n ,返回 所有小于非负整数 n 的质数的数量 。
示例 1:

示例

输入:n = 10
输出:4
解释:小于 10 的质数一共有 4 个, 它们是 2, 3, 5, 7 。
示例 2:

输入:n = 0
输出:0
示例 3:

输入:n = 1
输出:0

解法一:朴素算法

顾名思义,地球人都能想到的算法。
从2开始遍历到n,每个数都判断一下是不是质数,是的话cnt+1,最后返回即可。

class Solution {
    public int countPrimes(int n) {
        int cnt = 0;
        for(int i = 2; i < n; i++){
            if(isPrime(i)){
                cnt++;
            }
        }
        return cnt;
    }
    //判断是不是质数
    private boolean isPrime(int n){
        for(int i = 2; i < n; i++){
            if(n % i == 0){
                return false;
            }
        }
        return true;
    }
}

但是很明显太慢了,复杂度O(n^2)

那么进行优化:我们在判断质数时,也就是n%i == 0这一步时,是不是没有必要从2一直判断到n-1?
因为一个数是由两个数相乘得来的,比如n = a * b,那么a越小,b就越大,相反a越大,b就越小。
那么临界点就是a == b的时候,也就是a = sqrt(n)。
所以我们只需要循环到a <= sqrt(n)就可以,因为后面的数一定已经判断过了。
举个例子:我们判断9是不是质数,sqrt(9) == 3,那么我们只需要循环到3,因为在a == 3的时候,9 % 3 == 0,满足条件,直接退出了。
复杂度o(n*sqsrt(n))

解法二:埃式筛法

开始进入正片。
我们还可以进行优化。
比如2是质数,那么2的倍数一定是合数,比如4,6,8,10…
3是质数,那么3的倍数一定是合数,比如6,9,12,15…
所以只要当前数为质数,我们就可以把他的整数倍都直接pass掉!

注意点下面会讲

class Solution {
    public int countPrimes(int n) {
        int cnt = 0;
        int[] isPrime = new int[n];
        Arrays.fill(isPrime, 1);
        for(int i = 2; i < n; i++){
            if(isPrime[i] == 1){
                cnt++;
                //注意点1
                if((long)i * i < n){
                	//注意点2
                    for(int j = i * i; j < n; j += i){
                        isPrime[j] = 0;
                    }
                }
            }
        }
        return cnt;
    }
}

注意点1:要加一个判断,因为i是int类型,i*i可能会超出int范围,所以用long判断一下。
注意点2:重点!!!为什么j要从i * i开始,而不从i * 2开始呢,因为i的2倍、3倍、4倍…一直到i - 1倍,一定已经判断过了!!
举个例子:目前判断到了5,5是质数,那么我们将5的整数倍都可以判断为合数,也就是10,15,20,25…但是我们没必要从10开始,而是直接从25开始!因为10(2 * 5)早在之前就判断过了,也就是判断2的时候,2的整数倍为4,6,8,10…出现过10,所以没必要重复判断了!
复杂度O(nloglogn)

解法三:欧式筛法

如果上面的你都看懂了,那么已经达到面试的要求了,但是如果你能掌握这种算法,恭喜你,面试官一定会觉得你有点意思。

埃式筛法有一个不足,就是会重复筛,比如30,当判断到3为质数时,30是3的整数倍,会被筛一下;后面判断到5的时候,30也是5的整数倍,也会被筛一下。
所以欧式筛法的核心就是:每个数只被筛一次。
那么就可以达到线性复杂度了!也就是O(n)。所以欧式筛法也被称为线性筛法。

具体怎么做呢?

  1. 除了埃式筛法中的isPrime数组,我们还要维护一个集合,里面用来存放已知的质数。
  2. 埃式筛法中,当前数为质数时,才进行下面的操作。而欧式筛法,对每一个数都进行操作。
  3. 对于当前数来说,把它依次和集合中的质数相乘,得到的数必为合数,然后筛掉。但是当当前数可以整除当前的质数时,结束循环

先上代码,然后举例。

class Solution {
    public int countPrimes(int n) {
        List<Integer> list = new ArrayList();
        int[] isPrime = new int[n];
        Arrays.fill(isPrime, 1);
        int cnt = 0;
        for(int i = 2; i < n; i++){
            if(isPrime[i] == 1){
                list.add(i);
                cnt++;
            }
            for(int j = 0; j < list.size() && i * list.get(j) < n; j++){
                isPrime[i * list.get(j)] = 0;
                //注意点
                if(i % list.get(j) == 0){
                    break;
                }
            }
        }
        return cnt;
    }
}

为什么i % list.get(j) == 0要结束循环呢?
因为我们要保证每个数只被自己最小的质数筛一次。
举个例子:
此时i为10,那么质数集合里现在是[2,3,5,7]
开始遍历集合:
第一个是2,2 * 10 = 20,好,20被筛掉。
然后判断,10 % 2 == 0,结束。
为什么要结束?
如果不结束的话,会这样:
第二个是3,3 * 10 == 30,30被筛掉。看起来很合理对吧?
但是后面在遍历到15时,15 * 2 == 30,也会被筛一次!这就重复了!
所以我们直接结束循环即可,因为30一定在后面会被筛掉!!
也就是遍历到15时,从质数集合开始依次取,先取出2,2 * 15 == 30,30被筛掉,此时是不是利用了最小的质数2进行筛掉的?

结束。复杂度O(n)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值