我们常见的素数筛有三种,分别为暴力筛法,埃氏筛以及欧拉筛,接下来一一介绍:
一,暴力筛法
暴力筛法是最为简单,容易理解的方法,时间复杂度为O(n ^ 2),也是三种最慢的,当要求10 ^ 5以为的素数筛时,速度开始变慢,下面是暴力筛的代码
#include <iostream>
using namespace std;
const int MAX = 10010;
int prime[MAX], cnt;
void isprime(int n){
for(int i = 2; i <= n; i++){ //从2开始,默认 0, 1不为素数
int flag = 0; //定义一个变量来判定是否为素数
for(int j = 2; j < i; j++){
if(!(i % j)){
flag = 1;
break;
}
}
if(!flag) prime[cnt++] = i;
}
}
int main(){
int n;
cin >> n;
isprime(n);
cout << cnt << endl;
for(int i = 0; i < cnt; i++) cout << prime[i] << endl;
return 0;
}
下面我们对这个代码进行一个小小的优化:
#include <iostream>
using namespace std;
const int MAX = 10010;
int prime[MAX], cnt;
void isprime(int n){
for(int i = 2; i <= n; i++){
int flag = 0;
for(int j = 2; j <= i / j; j++){ // <--优化的地方
if(!(i % j)){
flag = 1;
break;
}
}
if(!flag) prime[cnt++] = i;
}
}
int main(){
int n;
cin >> n;
isprime(n);
cout << cnt << endl;
for(int i = 0; i < cnt; i++) cout << prime[i] << endl;
return 0;
}
我们可以看到原来 j < i 代码变成 j < i / j;相当于 j * j < i; 因为若 i 在 2 -(根号i)存在因子,则在根号 i – i 也存在因子,所以我们只需要遍历2–根号 i 就可以判断了,那么为什么不写成 j * j < i 和 j < sqrt(i) 呢?
因为写成 j * j < i 当素数的值过大时会有溢出的可能,为了防止溢出,后面的俩种筛法也是这样防止溢出。而 j < sqrt(i) 我们知道需要调用头文件math,然后使用这个函数sqrt,所以速度上会有所影响,该优化后的代码时间复杂度为O(n*根号n)
但是这种暴力筛还有很多优化比如先把偶数筛了等等,这里就不写出来了
二, Eratosthenes(埃氏筛)
埃氏筛是一个埃氏人发明的方法,具体思想是把每个素数的倍数筛掉,比如2,埃氏筛将把2的倍数筛掉,然后是3,将把3的倍数筛掉,接下来是4,但是4在2的时候给我们筛了,所以不行动,继续是5,将5的倍数筛掉,以此类推到目标范围。如下图所示:
下面是具体代码:
#include <iostream>
using namespace std;
const int MAX = 10010;
bool arr[MAX];
int prime[MAX], cnt;
void isprime(int n){
for(int i = 2; i <= n; i++){
if(!arr[i]){ //判断arr[i]是否被筛了
for(int j = 2; j <= n / i; j++){ // n / i 是为了防止 j * i 溢出
arr[i * j] = 1; // 筛掉素数的倍数,会多次筛掉一个数,所以非线性
}
prime[cnt++] = i;
}
/* if(!arr[i]){ //这是第二种埃氏筛的写法
prime[cnt++] = i;
for(int j = i + i; j <= n; j += i){ //不用担心溢出问题
arr[j] = 1;
}
}
*/
}
}
int main(){
int n;
cin >> n;
isprime(n);
cout << cnt << endl;
for(int i = 0; i < cnt; i++) cout << prime[i] << endl;
return 0;
}
这里我们需要注意一个点,我们需要区分埃氏筛的俩种写法,不要将它们混在一起,作者之前就是将这俩种写法混在一起,每次写埃氏筛的时候都非常的痛苦,需要去找哪里有错误,需要各位引以为戒。
埃氏筛的思想是需要我们去学习的,后面许多算法都用到了埃氏筛的思想。
用 bool 数组,不用 int 数组来判断是否为素数可以节省空间,埃氏筛法的时间复杂度为O(n * log (log n)),已经非常接近线性筛了,但是我们可以在图中看来,2筛了10,5也筛了10,像这种重复的筛还有许多,当数变大,重复筛掉一个数就更多,那么有没有一种方法可以防止这种重复筛掉一个数来提高效率,有的,接下来我们来介绍欧拉筛。
三,欧拉筛(线性筛)
我认为欧拉筛是一个很漂亮的筛法,干净利落又不失风度,它的时间复杂度为O( n )很完美,但是理解起来可能比较难,和埃氏筛一样我们定义俩个数组,一个用来判断素数,一个用来储存素数,还需要一个变量来保存素数的个数,下面是具体代码:
#include <iostream>
using namespace std;
const int MAX = 10010;
bool arr[MAX];
int prime[MAX], cnt;
void isprime(int n){
for(int i = 2; i <= n; i++){
if(!arr[i]) prime[cnt++] = i;
for(int j = 0; prime[j] <= n / i; j++){
/*这里的判定条件,你们可能在其他文章看过还需要 j < cnt;
但是思考一下,j在大于 n / i 的时候,j无论如何都不会大于cnt */
arr[i * prime[j]] = 1;
if(!(i % prime[j])) break; //保证每一个数只被筛一次
}
}
}
int main(){
int n;
cin >> n;
isprime(n);
cout << cnt << endl;
for(int i = 0; i < cnt; i++) cout << prime[i] << endl;
return 0;
}
我们可以看到每一个被筛过的数在第二次的来的时候因为这个语句:
if(!(i % prime[j])) break;
而被直接跳过,避免了重复筛掉同一个数,
举个例子还是10, 这个时候我们的prime数组里保存的数有2,3, 5 ,7 四个素数,好当10进来的时候,2 * 10 = 20,把 20 筛了,然后发现 10 % 2 == 0, 这个时候退出循环,而像后面的30可以被 2 * 15 的时候筛掉,所以每个数都只会被筛掉一次。
以上就是三种常用的素数筛法,比较一下欧拉筛和埃氏筛的速度其实基本上差不多,欧拉筛比较完美,但是难理解;埃氏筛比较容易理解,背起模板也比较容易。
有空将素数的判定Miller-Rabin素性测试写一下,在这里先挖个坑