大一时学过的算法,两年之后忘的一干二净,看了网上很多文章终于又弄懂了,尽量用通俗的说法说明白,避免再忘。
题目描述
给定整数 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)。所以欧式筛法也被称为线性筛法。
具体怎么做呢?
- 除了埃式筛法中的isPrime数组,我们还要维护一个集合,里面用来存放已知的质数。
- 埃式筛法中,当前数为质数时,才进行下面的操作。而欧式筛法,对每一个数都进行操作。
- 对于当前数来说,把它依次和集合中的质数相乘,得到的数必为合数,然后筛掉。但是当当前数可以整除当前的质数时,结束循环
先上代码,然后举例。
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)。