埃氏筛和线性筛
埃氏筛
我们考虑这样一个事实:如果 x 是质数,那么大于 x 的倍数 2*x, 3*x, …一定不是质数。我们设isPrime[i] 表示数 i 是不是质数,如果是质数则为 1,否则为 0。从小到大遍历每个数,如果这个数为质数,则将其所有的倍数都标记为合数(除了该质数本身),即 0,这样在运行结束的时候我们即能知道质数的个数。这里还可以继续优化,对于一个质数 x,如果按上文说的我们从 2*x 开始标记其实是冗余的,应该直接从 x*x 开始标记,因为 2*x, 3*x, … 这些数一定在 x 之前就被其他数的倍数标记过了,例如 2 的所有倍数,3 的所有倍数等。
class Solution {
public:
int countPrimes(int n) {
vector<int> isPrime(n,1);
int ans=0;
for(int i=2;i<n;++i)
{
if(isPrime[i])
{
ans+=1;
if((long long)i*i<n)
{
for(int j=i*i;j<n;j+=i)
isPrime[j]=0;
}
}
}
return ans;
}
};
该算法由希腊数学家埃拉托斯特尼(Eratosthenes)提出,称为埃拉托斯特尼筛法,简称埃氏筛。时间复杂度是 O(nloglogn)。
线性筛
以上做法仍有优化空间,比如对于 45 这个数,它会同时被 3,5 两个数标记为合数,我们发现这里面似乎会对某些数标记了很多次其为合数。如果能让每个合数都只被标记一次,那么时间复杂度就可以降到 O(n) 了,这就是线性筛的做法。
相较于埃氏筛,我们多维护一个 primes 数组表示当前得到的质数的集合。我们从小到大遍历,如果当前数是质数,就将其加入 primes 集合。另一点与埃氏筛不同的是,标记过程不再仅当 x 为质数时才进行,而是对每个整数 x 都进行。对于整数 x,我们不再标记其所有的倍数 x*x,x*(x+1),...,而是只标记质数集合 primes 中的数与 x 相乘的数,即 x*primes0 ,x*primes1,...,且在发现 x mod primesi = 0 的时候结束当前标记。
核心点在于:如果 x 可以被 primesi 整除,那么对于合数 y = x*primesi+1 而言,它一定在后面遍历到 (x/primesi)*primesi+1 这个数的时候会被标记,其他同理,这保证了每个合数只会被其最小的质因数筛去,即每个合数只被标记一次。
class Solution {
public:
int countPrimes(int n) {
vector<int> primes;
vector<int> isPrime(n, 1);
for (int i = 2; i < n; ++i) {
if (isPrime[i]) {
primes.push_back(i);
}
for (int j = 0; j < primes.size() && i * primes[j] < n; ++j) {
isPrime[i * primes[j]] = 0;
if (i % primes[j] == 0) {
break;
}
}
}
return primes.size();
}
};

浙公网安备 33010602011771号