前言
埃氏筛法与欧拉筛(线性筛)是快速找出n
以内素数的算法,其中埃氏筛法的时间复杂度是O(nlogn)
,而欧拉筛(线性筛)的时间复杂度接近O(n)
相关题目:https://leetcode-cn.com/problems/count-primes/
一、埃氏筛法
- 所谓筛法,就是在
1-n
的序列中,筛掉不符合的数,最终留下符合条件的数。 - 利用一条定律:合数都是素数的倍数。
- 从小到大,利用已有的素数
num
,将其的k
倍,即k*num
筛除掉。
1、示例
初始化一个序列,其中红色标记的代表素数:
- | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
从素数2出发,将2的倍数(4,6,8,10,12,14,16,18,20)删除掉:
- | 2 | 3 | - | 5 | - | 7 | - | 9 | - |
---|---|---|---|---|---|---|---|---|---|
11 | - | 13 | - | 15 | - | 17 | - | 19 | - |
然后将序列中升序中首个未标记的数字3标记为素数
- | 2 | 3 | - | 5 | - | 7 | - | 9 | - |
---|---|---|---|---|---|---|---|---|---|
11 | - | 13 | - | 15 | - | 17 | - | 19 | - |
从素数3出发,将3的倍数(6,9,12,15,18)删除掉:
- | 2 | 3 | - | 5 | - | 7 | - | - | - |
---|---|---|---|---|---|---|---|---|---|
11 | - | 13 | - | - | - | 17 | - | 19 | - |
然后将序列中升序中首个未标记的数字5标记为素数
- | 2 | 3 | - | 5 | - | 7 | - | - | - |
---|---|---|---|---|---|---|---|---|---|
11 | - | 13 | - | - | - | 17 | - | 19 | - |
从素数5出发,将3的倍数(10,15,20)删除掉:
- | 2 | 3 | - | 5 | - | 7 | - | - | - |
---|---|---|---|---|---|---|---|---|---|
11 | - | 13 | - | - | - | 17 | - | 19 | - |
以此类推,直到最终序列中所有数字被标记:
- | 2 | 3 | - | 5 | - | 7 | - | - | - |
---|---|---|---|---|---|---|---|---|---|
11 | - | 13 | - | - | - | 17 | - | 19 | - |
2、代码实现
int countPrimes(int n) {
vector<bool>iscut(n+1); //筛选标记,true-被筛掉,false-未筛除
vector<int>prime; //存储素数的容器
for (int i = 2; i <= n; i++)
{
if (!iscut[i]) { //i=true,即i未被筛掉,加入素数
prime.push_back(i);
for (int k = 2; k < n; k++) //把i的倍数筛掉
{
if (k * i > n) //防止越界
break;
iscut[k * i] = true;
}
}
}
return prime.size(); //返回素数个数
}
3、一些改进的点
1.对于把i
的倍数筛掉这里,倍数k
无需从2
开始,因为当k<i
时,i*k
已经被筛掉过一遍了。例:i=7,k=3,num=7*3=21
,在i=3,k=7,num=3*7=21
时就筛选过了。所以k
应当从k=i
开始。
2.当i*i>SIZE
时,就可以结束判断,将序列中剩余未被筛除掉的数加入到素数序列中。
4、改进后代码
int countPrimes(int n) {
vector<bool>iscut(n+1); //筛选标记,true-被筛掉,false-未筛除
vector<int>prime; //存储素数的容器
for (int i = 2; i < n+1; i++)
{
if (!iscut[i]) { //iscut[i]=false,即i未被筛掉,加入素数
prime.push_back(i);
for (int k = i; k < n; k++){//把i的倍数筛掉
if (k * i > n){ //防止越界
if (k == i){ //是i的平方越界,直接把序列中剩余数加入到素数
for (int j = i+1; j <= n; j++){
if(!iscut[j])
prime.push_back(j);
}
i = n + 1; //添加素数完毕后将最外层循环跳出;
}
break;
}
iscut[k * i] = true;
}
}
}
return prime.size(); //返回素数个数
}
改了之后测试发现性能几乎没有区别。。。。
5、缺点
会有一些数被重复地筛除掉,造成了不必要的计算量。例如数字16
,被2*8
和4*4
筛除。
二、欧拉筛(线性筛)
欧拉筛相对埃氏筛而言就是减少了重复的筛除,从而降低复杂度,达到O(n)
。
1、原理
- 欧拉筛与埃氏筛使用的原理有所区别。欧拉筛是
合数=最小质因数*合数(质数)
。以上组合唯一。 - 欧拉筛中不是从素数序列中出发,而是直接从数字序列依次遍历。对序列中的每一个数字
i
,将i
依次与素数序列成员相乘得到num=i*prime[j]
,然后将num
筛除掉,当num
超出统计范围或者i%prime[j]==0
(原因后面会讲)时退出循环。
2、代码实现
int countPrimes(int n) {
vector<bool>iscut(n + 1); //筛选标记,true-被筛掉,false-未筛除
vector<int>prime; //存储素数的容器
for (int i = 2; i <= n; i++)
{
if (!iscut[i]) //iscut[i]=false,即i未被筛掉,加入素数
prime.push_back(i);
for (int j = 0; j < prime.size(); j++)
{
if (i * prime[j] > n) //防越界
break;
iscut[i * prime[j]] = true; //把i的质数倍筛除
if (i % prime[j] == 0) //*判断最小质因数,防止重复筛除
break;
}
}
return prime.size(); //返回素数个数
注解:整个欧拉筛中的关键判断就是i % prime[j] == 0
,当为true
时,i
可以被prime[j]
整除,有k=i/prime[j]
。如果此时未跳出循环,则下一个被筛除的数为num=i*prime[j+1]
,而num
又可以写为num=prime[j] * k * prime[j+1]
,即当i=k*prime[j+1]
时,会再对num
进行筛除。故跳出循环可以避免重复的筛除操作。
例:当i=10
时,素数序列中有素数2,3,5,7
。
①不判断最小质因数时,被筛除的数有20,30,50,70
。而30
会在i=15
时再被筛除一次,因为30=15*2
。
②判断最小质因数时,被筛除的数有20
,因为筛除完20
后就因为10%2==0
跳出循环,而30,50,70
等就留到后面的数进行筛除,这样就有效避免了重复筛选除的操作。
总结
总的来说,两种筛法使用的原理有所不同,埃氏筛法更为直观,欧拉筛则需要理解一下。两者的性能虽然有所区别,但实际上区别并不大。