在算法竞赛中经常会遇到求质数的问题,这种题目一般都是要求出一定范围内[0,n]所有的质数或者质数的个数。最直接的思路就是根据质数的定义来判定一个数是不是质数(即一个数不能被除1和它本身外的任何数整除),如果我们需要对所有的数都这样进行判断,那么当n非常非常大的时候,这种算法的时间开销就会非常大,大概为O(n^2)。这个时候就可以使用筛法来快速地求出素数,筛法求素数的基本思路就是:除了0、1之外,所有的合数都是可以表示为比它小的一个素数的倍数,如果我们已知一个素数,那么我们可以把问题规定的范围内的所有该素数的倍数全都筛除,如果该范围内的所有的合数都被筛出,剩下的自然就都是素数了,时间复杂度大概为O(n)。
leetcode上面的一道题目:计数质数,一道典型求质数的题目,现在给出筛法求素数的解法:
class Solution {
public:
int countPrimes(int n) {
if(n==0||n==1)return 0;
int count=0;
bool isprime[n];//用来标记是否是素数的数组
memset(isprime,1,sizeof(isprime));//首先假设所有的数都是素数
isprime[0]=isprime[1]=0;//0、1不是素数
for(int i=2;i<n;i++){
if(isprime[i]==1){//如果是素数
count++;//计数加一
for(int j=2*i;j<n;j+=i)//将该质数的倍数标记为合数
isprime[j]=0;
}
}
return count;
}
};
因为我们的算法相当于是从2到n遍历一遍,我们将整个isprime数组都初始化为1,也就是假设有的数开始都是质数,每当访问isprime[i]的时候,如果它是合数,那么在访问它之前它一定已经被比它小的因子素数给筛掉了,所以在这个for(int i=2;i<n;i++)的循环中,每次访问isprime数组的时候都能确定i是不是素数了。
仔细思考一下上面的这种算法:线性筛法虽然大大缩短了求素数的时间,但是实际上还是做了许多重复运算,比如2*3=6,在素数2的时候筛选了一遍,在素数为3时又筛选了一遍。那么我们如何做出优化呢?如果只筛选小于等于素数i的素数与i的乘积,既不会造成重复筛选,又不会遗漏。时间复杂度几乎是线性的。
下面给出优划过后的筛法代码:
class Solution {
public:
int countPrimes(int n) {
if(n==0||n==1)return 0;
int count=0;
bool isprime[n];//用来标记是否是素数的数组
vector<int> su;//用来保存已经确定的素数
memset(isprime,1,sizeof(isprime));//首先假设所有的数都是质数
isprime[0]=isprime[1]=0;//0、1不是素数
for(int i=2;i<n;i++){
if(isprime[i]==1){//如果是素数
count++;//计数加一
su.push_back(i);
}
for(int j=0;j<su.size()&&i*su[j]<n;j++)
isprime[su[j]*i]=0;//小于i的所有的素数与i相乘得到的都是合数
}
return count;
}
};
可以看出优化之后的算法需要额外的空间存放vector<int> su来保存已经确定的素数,所谓空间换时间,这样的优化在刚才的基础上又减少了不少的时间复杂度。