埃拉托色尼素数筛法

1.算法原理

埃拉托色尼素数筛法是有古希腊数学家发明的一种快速求解范围内所有的素数的算法
在我们讲解埃拉托色尼素数筛法之前,我们需要了解一下朴素的求素数的算法的工作原理

首先:

对于朴素的求素数的算法我们有过编程基础的人都会知道算法的原理很简单,首先从定义出发,一个数既然是素数那么就说明这个数除了1和本身以外不存在任何一个因子,所以朴素的算法就很直接的遍历一遍整个范围,我们对范围内的所有的数都进行判断,如果该数可以被整除说明不满足素数的条件,这样的话,对整个范围都进行一次比那里我们就可以判断 一个数是不是素数

优化一下我们的朴素的求解思路,对于数来说,如果一个数的有因子的话(至少因子都是成对的),那么我们很显然会知道两个因子至少有一个会是小于等于sqrt(n)的,这一点是显然的,换个说法来看的话,如果一个数我们只要对他的sqrt(n)范围内进行遍历的话,只要在这个范围内是满足我们没有银子的条件,很显然这个数我们就可以认为是素数了,这么做可以减少我们的遍历的循环的次数

好了我们来总结一下,对于朴素的求素数的方法判断每个数是不是素数我们至少需要O(sqrt(n))即O(n)的时间复杂度,但是如果我们要是想要判断一整个范围内的所有的素数或者求范围内的素数的个数的话,我们朴素的方法就至少需要O(sqrt(n)*n)即O(n^2)的时间复杂度来做了,很显然当数据量非常大的时候这么做是有一些低效的

for(int i=2;i*i<N;i++)
{
    if(d%i==0) break;
}

导入:

下面我们来解释一下著名的素数筛法,即 埃拉托色尼素数筛法,本算法由著名的希腊数学家发明,算法的原理也是非常的简单,但是我们要求的不仅仅是初步的优化,之后我会讲解一系列的对埃拉托色尼算法的优化
首先在开始之前,我们需要了解到,埃拉托色尼素数筛法实际上是一种空间换时间的算法优化,对于判断单个数的素数性质来说,相对于朴素的算法没有优化,但是在求解范围素数问题的时候,埃拉托色尼素数筛法可以很快的打印一份范围内的素数表(该思路的时间复杂度我之后讲解)
首先,我们需要来了解一下
埃拉托色尼算法工作原理:
1.假定范围内的所有的数都是素数
2.我们从2开始,只要是2的倍数我们就认为该数不是素数,打标处理
3.直到判断到n为止我们就可以将所有的非素数打上标记,从而确定了所有的非素数

简单证明:反证法
假设应用算法流程之后我们得到了一组序列,如果该序列中存在一个非素数,说明该数必定存在因子d,那么对于在算法流程中我们对d的所有的倍数全部都打标处理了,所以说出现矛盾,埃拉托色尼素数筛法是正确的,可以得到正确的素数序列

附上代码直观一些:
	memset(prime,1,sizeof(prime));   //初始假设所有的数都是素数 
	
	prime[0]=prime[1]=0;   //初始确认0,1不是 
	for(int i=2;i*i<N;i++)
	{
		if(prime[i])
		{
			for(int j=2;i*j<N;j++) prime[i*j]=0;
		}
	}

对于朴素的埃拉托色尼素数筛法的时间复杂度我们来判断一下
n:扫描遍历次数
从2开始直到n我们进行倍数打标处理,每个循环到的数我们记为k
单次扫描的时间复杂度是O(n/k)
那么总的时间复杂度就是
T(n)=n/2+n/3+n/4+.....n/n(因为在算法的过程中我们是不断地筛掉的,所以说实际的时间复杂度是远远要比这个小的)
O(n)<T(n)
对于T(n)的求解,我们应用调和奇数的公式可以得到大致约为Ln(n)
所以说O(n)<O(n*logn)

当然你们可能会觉得算法比朴素的求素数的有点慢,但是注意我们的算法求解出来了整个范围的所有的素数,还算是相对来说比较高效的

优化1:
先陈述我们的优化,在这里我们还是没有必要遍历整个范围,我们只需要遍历到sqrt(n)就可以了
证明,我先说明这个证明确实废了我一些功夫
首先回顾埃拉托色尼素数筛法,我们进行的操作是打标处理,如果我们在sqrt(n)停止了打标处理,会错误吗
反证法:
假设我们操作之后还是存在非素数d没有被打标,那么该素数的sqrt(d)<sqrt(n)显然,那么就说明d还存在一个因子k<sqrt(d)<sqrt(n),但是按照埃拉托色尼算法,这个k的所有的倍数我们全部都打标了,所以说矛盾
证明成功
对于优化1来说我们明显降低了遍历次数

2016/10/29
优化1再优化,今天又得到了一种新的优化思路
因为在埃式筛法中我们都是从2倍开始一次的筛但是仔细注意我们会发现,实际上我们只用从i*i开始筛就好了,因为i*2,i*3....i*i-1都曾经被筛过了,我们只用从i*i开始就好,实际上在压力测试100000000(1亿)的时候我们会发现这样子我们可以优化一些时间,优化1的时间是4951,本次优化的时间压缩到了4321
还是很有用处的
附上代码:
	for(int i=2;i<N;i++)
	{
		if(prime[i])
		{
			long long int j;
			save[++count]=i;
			for(j=pow(i,2);j<N;j+=i) prime[j]=0;
		}
	}




优化2:导入快速线性筛法
从上面的埃拉托色尼算法的流程来看,我们对于某些数其实进行了重复筛选的结果
比如12,我们分别在2,3,的时候重复了筛选,为了优化重复筛选的弊端,我们引入快速线性筛法
为了解释方便首先我们先引入代码段,之哦后我们对代码段进行解释
memset(judge, 1, sizeof(judge));
	judge[1] = judge[0] = 0;
	for (int i=2;i<N;i++)
	{
		if (judge[i]) prime[++countp] = i;   //0
		for (int j=1;j<=countp&&i*prime[j]<N;j++)    //1
		{
			judge[i*prime[j]] = 0;
			if (!(i%prime[j])) break;   //2
		}
	}

线性素数筛法的解释:
对于埃拉托色你素数筛法,我们会发现有的素数我们会重复删除,比如12会被2,3判断两次,这样会大幅度的降低我们算法的时间复杂度,针对一些素数的基本性质和反证法,我来对快速线性筛法做一下简单的我证明和解释

首先:
1.任何一个合数都有唯一的素因子分解式(这也是我们唯一删除一次的应用原理)
对于任何一个合数,始终都在这个范围内
a.合数=素数*素数
b.合数=素数*合数

首先从代码的角度我来解释一下,快速先行筛法的思路如下:
1.从2开始
如果当前的i是素数的话对于1的内层循环我们始终是不会中途跳出的,也就是将当前i和i之前的所有的素数相乘得到的合数全部打标判负(对于合数=素数*素数的情况来看的话这样的删除效果是唯一的,不会重复删除,很容易可以判断出来)
如果当前的i是合数的话对于1的内层循环我们绝对会中途跳出(因为一个合数必定会表示成至少有一个素数参与的分解式)这样子的操作的目的是为了保证该情况下的删除是唯一的,不会重复删除
2.当整个数组遍历完之后,我们就会得到一个完整的素数表(这一点在下面我会用反证法证明)

证明:
1.证明该方案是不会漏筛合数的
反证法:
假设该方案我们会漏掉合数,假设合数是d
显然该合数d可以表示成:
d=d的最小素因子*w(该书可素可和,不考虑)
d的最小素因子必定小于等于w的最小素因子
(该结论应用反证法,如果d的最小素因子大于w最小素因子,那么对于d的最小素因子就不是一开始确定的值,所以说成立)
那么显然按照我们算法的流程来看的话在我们遍历到w的那个时候我们已经将d打标了,所以说和题设相矛盾
说明该算法对于筛素数是完全正确的,是不会漏筛的
2.证明该方案是不会重复删除合数的(也就是证明如果注释2处不跳出是会重复删除的)
假设合数k=p*w(p是素数,w是另一个k的因子)
如果p>w的最小素因子
对于k之后的素数h
我们就也要执行删除操作
因为h=pk*w(pk是p的下一个素数)=pw*ww(pw*ww的式子在ww遍历的时候会h会被打标,ww<w说明h之前被标记了,所以重复标记)
证明完成

综上我们可以总结出快速线性素数筛法在是正确的
因为快速线性素数筛法是不会出现对一个素数重复删除标记的情况,所以说对于埃拉托色尼素数筛法该速发的时间效率更高

为了验证算法的高效性,我对两种算法在压力测试100000000(1亿)的时候的耗时情况进行了大致的测试
实际显示快速先行筛法在大数据量的时候比埃拉托色你筛法要高效很多

附上压力测试代码:
#include"iostream"
#include"cstdio"
#include"cstring"
#include"cstdlib"
#include"time.h"
#define N 100000000

using namespace std;

int prime[N];
bool judge[N];
int countp = 0;

int main()
{
	double w = clock();
	memset(judge, 1, sizeof(judge));
	judge[1] = judge[0] = 0;
	for (int i=2;i<N;i++)
	{
		if (judge[i]) prime[++countp] = i;
		for (int j=1;j<=countp&&i*prime[j]<N;j++)
		{
			judge[i*prime[j]] = 0;
			if (!(i%prime[j])) break;
		}
	}
	//for (int i = 1;i <= countp;i++) printf("%d ", prime[i]);
	printf("耗时:%lf\n%d",(double) (clock() - w),countp);
	return 0;
}
/*
#include"iostream"
#include"cstdio"
#include"cstdlib"
#include"time.h"
#define N 100000000

using namespace std;

bool judge[N];

int main()
{
	int count = 0;
	double w = clock();
	memset(judge, 1, sizeof(judge));
	judge[1] = judge[0] = 0;
	for (int i=2;i*i<N;i++)
	{
		if (judge[i])
		{
			for (int j = 2;i*j < N;j++) judge[i*j] = 0;
		}
	}
	for (int i = 2;i < N;i++) if (judge[i]) count++;
	printf("%lf\n%d\n", clock() - w,count);
	return 0;
}*/

2.Last question

1.对于线性素数筛法还催你在什么好的优化
2.对于欧拉筛法和莫比乌斯筛法的学习算法原理
  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值