关于素数筛法的总结

        我们常见的素数筛有三种,分别为暴力筛法,埃氏筛以及欧拉筛,接下来一一介绍:

一,暴力筛法

暴力筛法是最为简单,容易理解的方法,时间复杂度为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素性测试写一下,在这里先挖个坑

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

殊煜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值