C++筛法求素数

假定题目为输出n以内的所有素数

一般方法 

最容易理解的一个方法,从0遍历到根号n判断n是否能被整除。使用时只需要记住判断到根号n就可以了。

但是时间复杂度是o(n*sqrt(n)),效率略低。

代码如下:

#include<iostream>
#include<cmath>			//用sqrt()这个函数需要加的头文件 
using namespace std;
int prime(int n)
{
	for(int i=2;i<sqrt(n);i++)		//不需要到n,到根号n就已经足够 
	{
		if(n%i==0)	return 0;		//不是素数返回0,是素数返回1 
	}
	return 1;
}
int main()
{
	int n;
	cin>>n;
	
	for(int i=1;i<=n;i++)
	if(prime(i))	cout<<i<<" ";	//如果是素数就打印在屏幕上 
	
	return 0;
} 

普通筛——埃拉托斯特尼(Eratosthenes)筛法

基本思路:

定义一个长度为maxn的布尔变量数组prime[maxn],数组元素为0则代表所对应的下标为素数,数组元素为1则代表所对应的下标为合数。(下文中2代表prime[2],2的值为prime[2]的值)

我们先把数组里所有元素的值设为1,代表着目前所有小于n的数都被认为是素数。

假设有一个指针,当他指向2时,2的值为1,表示2为素数,程序把2的倍数4,6,8等的值全部设置成0,这代表他们不被认为是素数。

然后那个指针指向3,3的值为1,表示3为素数,程序把3的倍数6,9,12等的值全部设置成0,这代表他们也不被认为是素数。

指针指向了4,4的值已经被指针指向2时设置成了0。它不是素数,没有必要把它的倍数的值也设置成0,因为4能筛掉的合数,2也都能筛掉。

然后就是指针不断的向后移动,找到素数的值,把它们的倍数的值全部设置成0,直到筛到根号n

因为这种算法是层层筛选素数,所以叫筛法求素数。

函数体代码如下:

const int maxn=10000;						//具体的maxn看题目要求 
bool prime[maxn];
void judge_prime(int n)
{
	memset(prime,1,sizeof(prime));			//把prime数组全部初始化为1 
	for(int i=2;i<n;i++)					//for循环可以实现上面所说的指针的功能 
	{
		if(prime[i]==1)						//找到素数 
		for(int j=i*2;j<=n;j+=i)			//把n以及n以内i的倍数的值全部设为0 
		{
			prime[j]=0;
		}
	}	
}

memset(a,b,c)将a这个地址之后c个字节的值全部替换为b

sizeof(a),返回a总共所占的字节数

第一个循环来找素数,第二个循环来枚举那个素数的倍数

上面讲到的这种方法其实有优化的空间,j刚开始不需要设为2i,可以优化为i*i,指针也和基础方法一样,不用指到n,只需要指到根号n(我也不知道为什么,问数学家去)

优化前的时间复杂度有o(nloglogn),优化后就大概接近o(n)了

优化后的总体代码如下:

#include<iostream>
#include<cmath>
#include<string.h>							//包含memset的头文件,也可以写成cstring 
using namespace std;
const int maxn=10000;						//具体的maxn看题目要求 
bool prime[maxn];
void judge_prime(int n)
{
	memset(prime,1,sizeof(prime));			//把prime数组全部初始化为1 
	prime[0]=0;prime[1]=0;					//加上这两句显得更严谨( 
	for(int i=2;i<n;i++)					//for循环可以实现上面所说的指针的功能 
	{
		if(prime[i]==1)						//找到素数 
		for(int j=i*2;j<=n;j+=i)			//把n以及n以内i的倍数的值全部设为0 
		{
			prime[j]=0;
		}
	}	
}
int main()
{
	int n;
	cin>>n;
	judge_prime(n);
	for(int i=0;i<=n;i++)
	{
		if(prime[i])						//如果prime[i]=1,也就是i被认为是素数时,输出i 
		cout<<i<<" ";	
	} 
	return 0;
} 

线性筛——欧拉Euler筛

基本思路:

你有没有注意到,埃氏筛法在指针指向2和3时都把6的值设为了0,重复了,所以会有些浪费,终究不能达到o(n)的时间复杂度。

通过离散数学中的知识,我们可以知道每个合数都能分割成一个素数a和另一个数i(可以是素数)的乘积。

那么欧拉筛要做的,就是要保证每个合数只会被分割后的最小素数a筛掉,避免重复。

先放代码,方便理解:

const int maxn=1e7;
bool prime[maxn+5];
int sto_prime[maxn+5];
void judge_prime(int n)
{
	int cot=0;
	memset(prime,1,sizeof(prime));
	prime[0]=0;prime[1]=0;
	for(int i=2;i<n;i++)							//外层循环指向不同的数i 
	{
		if(prime[i])	sto_prime[cot++]=i;			//保存素数到int数组里 
		for(int j=0;j<cot;j++)						//内层循环指向不同的素数 
		{
			if(i*sto_prime[j]>n)	break;			//如果要筛的那个数超出了最大值,退出内层循环 
			prime[i*sto_prime[j]]=0;				//筛数 
			if(i%sto_prime[j]==0)	break;			//最重要的一句, 保证只每个合数只筛一次 
		}
	}	
}

具体到实现上,我们除了要像上面一样定义一个bool类型的数组,还要定义一个int类型的数组,专门用来存放筛选出来的素数。

还需要两层循环来枚举素数sto_prime和另一个数i。

内层从小到大枚举 sto_prime[j]。i×sto_rime[j] 是尝试筛掉的某个合数,其中,我们期望 sto_rime[j] 是这个合数的最小质因数 (这是线性复杂度的条件,下面叫做“筛条件”)。它是怎么得到保证的?

j 的循环中,有一句就做到了这一点:

if(i%sto_prime[j]==0)	break;

j 循环到 哪里 就恰好需要停止的理由是:

  • 下面用 s(smaller)表示小于 j 的数,l(larger)表示大于 j 的数。

  • ① i 的最小质因数肯定是 sto_prime[j]。

    (如果 i 的最小质因数是 sto_prime[s] ,那么 sto_prime[s] 更早被枚举到(因为我们从小到大枚举质数),当时就要break)

    既然 i 的最小质因数是 sto_prime[j],那么 i × sto_prime[j] 的最小质因数也是 sto_prime[j]。所以,j 本身是符合“筛条件”的。

  • ② i × sto_prime[s] 的最小质因数确实是 sto_prime[s]。

    (如果是它的最小质因数是更小的质数 sto_prime[s],那么当然 sto_prime[s] 更早被枚举到,当时就要break)

    这说明到 j 之前(用 i × sto_prime[s] 的方式去筛合数,使用的是最小质因数)都符合“筛条件”。

  • ③ i × sto_prime[l] 的最小质因数一定是 sto_prime[l]。

    (因为 i 的最小质因数是 sto_prime[j],所以 i × sto_prime[l]也含有 sto_prime[j] 这个因数(这是因为),所以其最小质因数也是 sto_prime[j](新的质因数 sto_prime[l] 太大了))

    这说明,如果 j 继续递增(将以 i × sto_prime[l] 的方式去筛合数,没有使用最小质因数),是不符合“筛条件”的。

欧拉筛因为每个数都只被筛一次,复杂度只有o(n),是最好的筛选素数的方法。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值