素数筛选方法
题目来源
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代码仓库(以及部分面试算法题)