题目地址:
https://leetcode.com/problems/count-primes/
给定 n n n非负,问小于 n n n的素数个数。
法1:埃拉托斯特尼筛法(Sieve of Eratosthenes)。开一个长度为 n n n的boolean数组记录每个数是否是素数。首先全填充为true,将 0 0 0和 1 1 1置为false。接着开始找第一个是true的数,是 2 2 2,接着把 2 2 2的比自己大的倍数全标记为false;接着再向后找是true的数,然后重复上面的操作。这样小于 n n n的所有非素数就全被标记为false了,剩下的就是素数。最后数一下即可。代码如下:
import java.util.Arrays;
public class Solution {
public int countPrimes(int n) {
if (n <= 2) {
return 0;
}
boolean[] isPrime = new boolean[n];
Arrays.fill(isPrime, true);
isPrime[0] = isPrime[1] = false;
for (int i = 2; i <= Math.sqrt(n - 1); i++) {
// 遇到素数就筛掉它的比它大的倍数
if (isPrime[i]) {
for (int j = 2; i * j < n; j++) {
isPrime[i * j] = false;
}
}
}
// 最后数一下还有哪些数没被筛掉
int res = 0;
for (boolean p : isPrime) {
if (p) {
res++;
}
}
return res;
}
}
时间复杂度 O ( n log log n ) O(n\log \log n) O(nloglogn),空间 O ( n ) O(n) O(n)。
时间复杂度证明依赖于一个式子: ∑ p < n 1 p = log log n \sum_{p<n}\frac{1}{p}=\log\log n p<n∑p1=loglogn有了这个式子,时间复杂度是显然的。
法2:欧拉筛。这种筛法时间复杂度是线性的,优于埃拉托斯特尼筛法。它的主要改进在于,让每个合数被筛的时候,只被它的最小素因子筛。这样就能保证每个合数只会被筛一次,而不像埃氏筛法,同一个数可能被多次筛。代码如下:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Solution {
public int countPrimes(int n) {
boolean[] isPrime = new boolean[n + 1];
Arrays.fill(isPrime, true);
// 要把素数存下来
List<Integer> primes = new ArrayList<>();
for (int i = 2; i < n; i++) {
if (isPrime[i]) {
primes.add(i);
}
for (int j = 0; i * primes.get(j) < n; j++) {
isPrime[i * primes.get(j)] = false;
if (i % primes.get(j) == 0) {
break;
}
}
}
return primes.size();
}
}
时空复杂度 O ( n ) O(n) O(n)。
算法正确性证明:
假设有个合数
N
N
N,其最小素因子是
p
p
p,那么由于
p
p
p是素数,所以当外层循环的
i
i
i取
p
p
p的时候,isPrime[i * primes.get(j)] = false;
这句话在之前的所有循环中都没把
p
p
p给筛掉,所以一进循环的时候
p
p
p就被加入了primes的列表中。接下来循环进行到
i
=
N
p
≥
p
i=\frac{N}{p}\ge p
i=pN≥p的时候,由于
i
i
i的最小素因子不会小于
p
p
p,所以不会提前break出来,因此
N
N
N会被筛掉。
算法时间复杂度证明:
只需证明每个合数
N
N
N只会被筛一遍,也就是
i
=
N
p
i=\frac{N}{p}
i=pN的那一遍。首先,在isPrime[i * primes.get(j)] = false;
的时候,
p
j
p_j
pj一定小于等于
i
i
i的所有素因子,否则就会被提前break出去。而这种情况下,
p
j
p_j
pj也是
i
p
j
ip_j
ipj的最小素因子,也就是说,当
N
N
N被筛的时候,一定有
p
j
=
p
p_j=p
pj=p,所以也一定有
i
=
N
p
i=\frac{N}{p}
i=pN,而
i
i
i取
N
p
\frac{N}{p}
pN并且
p
j
p_j
pj取
p
p
p两者一起只会发生一次,所以
N
N
N也只会被筛一次。
C++:
class Solution {
public:
int countPrimes(int n) {
if (!n) return 0;
bool st[n];
memset(st, 0, sizeof st);
int p[n], idx = 0;
for (int i = 2; i < n; i++) {
if (!st[i]) p[idx++] = i;
for (int j = 0; p[j] * i < n; j++) {
st[p[j] * i] = true;
if (i % p[j] == 0) break;
}
}
return idx;
}
};
时空复杂度 O ( n ) O(n) O(n)。