素数筛(埃氏筛和欧拉筛)

我们以leetcode204题计数质数来讲解素数筛问题

题目描述:

给定整数 n ,返回 所有小于非负整数 n 的质数的数量 。

示例 1:

输入:n = 10
输出:4
解释:小于 10 的质数一共有 4 个, 它们是 2, 3, 5, 7 。
示例 2:

输入:n = 0
输出:0
示例 3:

输入:n = 1
输出:0

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/count-primes

首先我们给出常规方法:

CountPrime函数用于计算小于n的非负整数的质数数量,我们用一个循环来遍历从2开始到n的所有数字,并判断该数字是否是质数,如果是则sum++。IsPrime用来判断一个数是否为素数,我们从2

开始遍历到n,判断这个数是否是n的因子,如果是则证明该数不是质数,遍历结束没有找到因子则返回真。

下面给出代码:

#include<iostream>
#define ll long long
using namespace std;

ll sum = 0;
bool IsPrime(ll n)
{
	for (ll i = 2; i < n; i++)
	{
		if (n % i == 0)
			return false;
	}
	return true;
}
void CountPrime(ll n)
{
	for (ll i = 2; i < n; i++)
	{
		if (IsPrime(i))
		{
			sum++;
		}
	}
}

int main(void)
{
	ll num = 1e6;
	time_t first, second;
	first = time(NULL);
	CountPrime(num);
	second = time(NULL);
	cout << "共" << sum << "个质数,用时" << second - first <<"s" << endl;

	return 0;
	
}

我们给出数据大小为1e6,得出结果

 用时303s可以看出这个时间复杂度是相当的高的为O(n)

我们对上面的算法可以进行一些改进,我们在判断素数的时候遍历到n,但实际情况两个数相乘x * y = n,那么x,y必须有一个大于\sqrt{n}一个小于\sqrt{n},所以我们只需要遍历到\sqrt{n}即可,实际复杂度O(\sqrt{n})

下面给出更改后的代码,其中i < n,是为了排除n=2时的影响,读者可以自行思考一下

bool IsPrime(ll n)
{
	for (ll i = 2; i < sqrt(n) + 1 && i < n; i++)
	{
		if (n % i == 0)
			return false;
	}
	return true;
}

下面给出运行结果: 

 

 可以看出这次用时1s比未改进前快了300倍。

下面介绍进阶方法埃氏筛:

什么是埃氏筛法?我们用一个数组isPrime来维护从1到n所有数的状态,其中0表示素数,1表示合数。我们遍历到其中一个数i的时候,把i的所有倍数全部记录为合数以此来减少循环的次数。那么为什么这个方法可以筛选出所有的质数,假设我们需要判断的数字为n,那么在判断之前,我们已经遍历了从[2,n-1]内的所有值以及把它们的倍数都标记为合数,所以n的因子是否在[2,n-1]内的情况已经清楚。

#include<iostream>
#include<vector>
#define ll long long
using namespace std;

ll sum = 0;

void CountPrime(ll n)
{
	vector<int> isPrime(n);
	for (ll i = 2; i < n; i++)
	{
		if (isPrime[i] == 0)
			sum++;
		for (ll j = 2 * i; j < n; j += i)
		{
			isPrime[j] = 1;
		}
	}
}

int main(void)
{
	ll num = 1e6;
	time_t first, second;
	first = time(NULL);
	CountPrime(num);
	second = time(NULL);
	cout << "共" << sum << "个质数,用时" << second - first << "s" << endl;

	return 0;

}

下面给出运行结果:

 可以看出用时为0s速度相当的快,此算法的时间复杂度为O(nloglogn),所以为了便于后面算法效率的比较,我们将数据增加为1e9

下面给出运行结果:

这就是埃氏算法的最快情况吗,显然我们还可以再进行优化,我们注意每次更新标记是每次都是从j = 2 * i开始,我们假设i  = 10,那么我们就是要标记20,30,40...为合数,但是其中2 * 10是不是在i=2的时候已经标记过一次,因为我们在i = 2时需要标记4,6,8,10...10 * 2。所以我们可以令j = i * i,这样就排除了[2,i-1]中重复计算。用数学语言解释就是,假设x * i = y,x = zi',则y的质因数中有z,y的判断在z遍历时已经完成。

下面给出优化代码:

void CountPrime(ll n)
{
	vector<int> isPrime(n);
	for (ll i = 2; i < n; i++)
	{
		if (isPrime[i] == 0)
			sum++;
		for (ll j = i * i; j < n; j += i)
		{
			isPrime[j] = 1;
		}
	}
}

下面给出运行结果:

 可以看出用时为103秒,比改进前快了2倍左右

下面我们再来介绍埃氏筛的进阶改法,欧拉筛法:
我们以12为例子,12 = 2 * 6,12 = 3 * 4,那么12在2的时候标记了一次,在3的时候又被标记了一次,我们怎么解决这个重复的问题呢,这时我们引入一个定理:

正整数的唯一分解定理,即:每个大于1的自然数均可写为质数的积,而且这些素因子按大小排列之后,写法仅有一种方式。

也即是i = a * b * c * ...,我们假设a是最小质因数。我们简化为i = a * m,其中m为b * c * ...,a为最小质因数,我们用一个数组prime来存储目前的素数集合,我们设目前遍历到的数字为i,由正整数的唯一分解定理可知,i = a * m,假设a为最小质因数,那么在遍历到i之前已经确定了[2,i-1]内所有质数的倍数了,比如说i = 12,那么在i = 6,prime[j] = 2时已经标识过一次。所以,我们可以通过i % prime[j] 来判断是否当前i分解后的质数是否在已经遍历的质数序列中,如果有则直接可以退出循环。

下面给出优化代码:

void CountPrime(ll n)
{
	vector<int> isPrime(n);
	vector<int> prime;
	for (ll i = 2; i < n; i++)
	{
		if (isPrime[i] == 0)
		{
			sum++;
			prime.push_back(i);
		}
		for (ll j = 0; j < prime.size() && i * prime[j] < n; j++)
		{
			isPrime[i * prime[j]] = 1;
			if (i % prime[j] == 0)
				break;
		}
	}
}

运行结果为:

  可以看出用时为50秒,比改进前快了2倍左右

猜你喜欢

转载自blog.csdn.net/m0_53377876/article/details/130500101