C++求前N个素数——线性筛法

C++求前N个素数——线性筛法

素数

又称质数,是指在大于1的自然数中,除了1和它本身以外不再有其他因数的自然数。

判断素数
一般而言,判断一个数n是不是素数,只需要用n分别除以[2,n-1]看是不是能整除,这样的话时间复杂度为O(n^2)。
但是实际上我们其实只需要用n分别除以[2,floor(sqrt(n-1))]就可以判断出n是否是素数了,但是为什么呢,我也不知道,反正大家都这么干 这样下来的时间开销是√1+√2+√3+…+√n,这个式子目前好像还没有公式可以表示。

//判断一个数是否是素数
bool isPrime(int num) {
	if (num < 2)return false;
	int stop = sqrt(num);
	for (int i = 2; i <= stop; i++) {
		if (num % i == 0) return false;
	}
	return true;
}

**

问题的拓展:求2,3,4,5…N中所有素数**

  • 基本解法

    既然我们手上有个判断素数的函数了,就很容易拿它去判断2,3,4,5…N中的每一个数并且将素数记录下来。但是这样干时间开销是很大的,假如isPrime()这个函数是没有做过优化的那么这一整个算法的时间复杂度∈O(N^3),所以我们肯定得找条别的路。

  • 筛选

    筛选是啥,就是将合数筛选出去,留下素数,这样我们就能够得到某一个自然数区间内所有的素数了。
    需要特别注意的是,下面的筛选算法中是不会用到isPrime()这个函数的,这一点必须要明确(很神奇吧)

  1. 普通筛选法(埃式筛法)(O(nlognlogn))

要求2,3,4,5…N中所有素数,考虑用这样一个类型为bool的isPrime数组,大小为[N+1],isPrime[i]用来标记自然数i是否为素数。我们把这个数组除isPrime[0]和isPrime[1]外全初始化为1(其实可有可无,因为i是从2开始的),即先认为2,3,4,5…N都是素数。primes数组用来存放已经求得的素数(相当于一个栈),got表示已经求得的素数的数量。如下表所示

i
index0123456789N
isPrime00111111111
got = 0
primes

i为当前需要判断的自然数,由2(最小的素数)开始。由于任何两个大于1的自然数相乘都是合数,所以i的任意大于1的整数倍(i*2,i*3…)都是合数,因此我们将所有小于等于N且为2的倍数的数都筛除,并且将2放进primes中,并让got右移。(为了填表方便姑且当N是偶数)

i
index0123456789N
isPrime00110101010
got = 1
primes2

筛除掉2的倍数之后我们将i右移至下一个没被筛除掉的自然数,也就是3.

i
index0123456789N
isPrime00110101010

继续将3入primes栈,并且筛除3的倍数

i
index0123456789N
isPrime00110101000
got = 2
primes23

以此类推。。这样我们在推进i的时候每当遇到isPrime[i]为0的自然数时我们就可以直接跳过,继续将i右移一格(比如上面的表格的下一状态i=5而不是4)。那这样做会不会把合数判定成素数呢?那是不会的。 我们观察primes栈,可以知道求得的素数一定是升序入栈的,并且由于筛除操作,i不是primes栈中任一素数的整数倍。而且我们知道任一合数都可以由有限个素数相乘得到,所以我们可以知道,任何两个小于i的素数(也就是primes中的素数)相乘均不等于i,因此i为继已求得的素数后的下一个最小素数。这也是为什么前面提到的不需要用到判断一个数是否为素数的函数。

但是,这样子问题也很显而易见,观察上面的表格,我们可以发现当i=2和i=3的时候,我们都筛除了合数6,一个数被重复筛除,就会导致性能的下降,所以在这个基础上,我们还可以做进一步的优化,也就是线性筛法。

  1. 线性筛法(O(n))

在了解线性筛法之前,先看这样一个式子(均为大于1的自然数):

	因数a * 因数i = 合数c

很明显,这不废话吗,一个合数肯定可以分解为两个因数,但分解的结果可能不是唯一的,比如24可以分解为3*8, 4*6, 2*12。那我们怎么去避免重复筛除呢?虽然一个合数的分解结果不唯一,但是所有分解式中总有唯一一个式子是包含着这个合数c的最小质因数的,如上例的2*12,2就是24的最小质因数。因此,如果我们在筛除合数过程中能够知道当前这个因数是否是相乘后的积的最小质因数,就能够避免掉重复的筛除动作。因此上式可以改写为:

	c的最小质因数a * 因数i = 合数c

这里还有另一个之前讲到的比较重要的一点,就是primes栈中的所有素数都是升序入栈的,这有啥用呢,注意上面的式子,我们需要的是最小质因数a,因此筛除的时候,我们不像埃式筛法那样根据i的整数倍去筛除,而是从primes中由小到大依次取出已求得的素数与i相乘进行筛除,这样我们能尽可能使这个a为c的最小质因数(但不一定就是,后面会讲)。比如当i=3,isPrime[3]=1,就将3入primes栈,此时栈为

got = 2
primes23

则primes[0]*i = 2*3 = 6,筛去6;primes[1]*i = 3*3 = 9,筛去9。

i
index0123456789N
isPrime00110101101

重点来了,怎么确保a是c的最小质因数?

前面讲到了,a是primes栈中从小到大取出的素数,那a就一定会遍历完当前primes的所有素数吗?答案是否定的。
我们看下面一个例子:

i
index0123456789N
isPrime00110101100
got = 2
primes23
j

此时i=4,虽然isPrime[4]=0,不将4进栈,但我们依然要根据栈中的2,3与4相乘去筛除合数。a的值为primes[j],j的初始值为0(j是下标),a*i = primes[j]*i = 2*4 = 8,因此将isPrime[8]置为0.

i
index0123456789N
isPrime00110101000

此时j应该右移吗?也就是我们需要把isPrime[primes[++j]*i]也就是isPrime[12]置为0吗?我们要确保的是令a为c的最小素因数,再看一手等式:

a * i = c

假如当前a=a0,乘积c=c0,即 a0 * i = c0;且 i%a0=0,则有

a02*(i/a0) = c0

i = a0*(i/a0)

倘若我们将j右移继续进行筛除,则有

a1*i = c1(a1为primes中a0的下一个素数)

代入i,得

a1*a0*(i/a0)= c1

式中a1>a0,这样就不满足a1是c1的最小素因数了!后续的a2,a3,…亦同理。
因此我们可以根据i是否能被a整除从而判断j是否需要继续右移。

#include<iostream>
#define  uint unsigned int 
using namespace std;

int main() {
	uint N, got = 0;
	cin >> N;
	bool* isPrime = new bool[N + 1];
	//初始化
	for (int i = 0; i < N + 1; i++) isPrime[i] = 1;
	uint* primes = new uint[N + 1];
	for (uint i = 2; i <= N; i++) {
		//如果是素数则入栈
		if (isPrime[i]) primes[got++] = i;
		//筛除,并且并且下标大于N以后的自然数不需要标记
		for (uint j = 0; j < got&&primes[j]*i<=N; j++) {
			isPrime[primes[j] * i] = 0;
			//确保a是c的最小素因数
			if (i % primes[j] == 0) break;
		}
	}
	int k=0;
	//每10个打印一行
	for (int i = 0; i < got; i++) {
		cout << primes[i];
		k=(k+1)%10;
		if(k) cout<<"\t";
		else cout<<endl;
	}
	delete[] isPrime,primes;

}

求2~100的所有素数测试结果:
求2~100的所有素数

改造成计算前N个素数

要求前N个素数,只需要把终止条件改为got==N即可。不过需要注意的是,因为我们不知道后面的a*i会是多少,所以每当isPrime的空间大小<=i*i的时候(因为a有可能等于i,所以要当做i*i处理)我们就需要将isPrime的空间扩充到i*i+1的大小。

#include<iostream>
#include<time.h>
#define  uint unsigned int 
using namespace std;
bool _isPrime(int num) {
	if (num < 2)return false;
	int stop = sqrt(num);
	for (int i = 2; i <= stop; i++) {
		if (num % i == 0) return false;
	}
	return true;
}
int main() {
	uint N, got = 0, i = 2, mark_space = 10;
	cin >> N;
	//bool* isPrime = new bool[N + 1];
	bool* isPrime = new bool[mark_space];
	//初始化
	for (int i = 0; i < mark_space; i++) isPrime[i] = 1;
	//uint* primes = new uint[N + 1];
	uint* primes = new uint[N];
	//for (uint i = 2; i <= N; i++) {
	//	//如果是素数则入栈
	//	if (isPrime[i]) primes[got++] = i;
	//	//筛除,并且并且下标大于N以后的自然数不需要标记
	//	for (uint j = 0; j < got&&primes[j]*i<=N; j++) {
	//		isPrime[primes[j] * i] = 0;
	//		//确保a是c的最小素因数
	//		if (i % primes[j] == 0) break;
	//	}
	//}
	while (got < N) {
		if (isPrime[i])primes[got++] = i;
		//判断是否需要扩充空间
		if (i * i >= mark_space) {
			uint new_space = i * i + 1;
			bool* t;
			do {
				t = new bool[new_space];
			} while (t == NULL);
			for (int j = 0; j < new_space; j++) {
				if (j < mark_space) t[j] = isPrime[j];
				else t[j] = 1;
			}
			delete[] isPrime;
			isPrime = t;
			mark_space = new_space;
		}
		for (int j = 0; j < got; j++) {
			isPrime[primes[j] * i] = 0;
			if (i % primes[j] == 0) break;
		}
		i++;
	}

	int k = 0;
	for (int i = 0; i < got; i++) {
		cout << primes[i];
		k = (k + 1) % 10;
		if (k&&i!=got-1) cout << " ";
		else cout << endl;
	}

	delete[] isPrime,primes;

}

求前100个素数测试结果:
在这里插入图片描述
但是其实这样做的缺点很明显,也很致命,因为在这个过程中我们需要不断的扩充isPrime数组的空间,不断new,delete,并且拷贝,这些操作的开销其实早就远大于算法本身了,以至于这种做法的耗时甚至还远大于逐个自然数去判断是否是素数的做法,测试如下:

#include<iostream>
#include<time.h>
#define  uint unsigned int 
using namespace std;
bool _isPrime(int num) {
	if (num < 2)return false;
	int stop = sqrt(num);
	for (int i = 2; i <= stop; i++) {
		if (num % i == 0) return false;
	}
	return true;
}
int main() {
	uint N, got = 0, i = 2, mark_space = 10;
	cin >> N;
	//bool* isPrime = new bool[N + 1];
	bool* isPrime = new bool[mark_space];
	//初始化
	for (int i = 0; i < mark_space; i++) isPrime[i] = 1;
	//uint* primes = new uint[N + 1];
	uint* primes = new uint[N];
	//for (uint i = 2; i <= N; i++) {
	//	//如果是素数则入栈
	//	if (isPrime[i]) primes[got++] = i;
	//	//筛除,并且并且下标大于N以后的自然数不需要标记
	//	for (uint j = 0; j < got&&primes[j]*i<=N; j++) {
	//		isPrime[primes[j] * i] = 0;
	//		//确保a是c的最小素因数
	//		if (i % primes[j] == 0) break;
	//	}
	//}
	time_t s = clock();
	while (got < N) {
		if (isPrime[i])primes[got++] = i;
		if (i * i >= mark_space) {
			uint new_space = i * i + 1;
			bool* t;
			do {
				t = new bool[new_space];
			} while (t == NULL);
			for (int j = 0; j < new_space; j++) {
				if (j < mark_space) t[j] = isPrime[j];
				else t[j] = 1;
			}
			delete[] isPrime;
			isPrime = t;
			mark_space = new_space;
		}
		for (int j = 0; j < got; j++) {
			isPrime[primes[j] * i] = 0;
			if (i % primes[j] == 0) break;
		}
		i++;
	}
	time_t e = clock();

	int k = 0;

	cout << "耗时" << difftime(e, s) << "ms";
	got = 0;
	i = 2;
	s = clock();
	while (got < N) {
		if (_isPrime(i++)) primes[got++] = i - 1;
	}
	e = clock();

	cout << "耗时" << difftime(e, s) << "ms";
	delete[] isPrime,primes;

}

测试结果:
在这里插入图片描述
所以这个方法还是只有理论意义。。除非你能判断出第N个素数的大小至多是多少,这样就能够预先决定开辟多少空间给isPrime。
因此为了做个正确的对比,我用函数法先求出第1000000个素数为15485863,因此给isPrime的大小为15485864,然后注释掉扩充空间的那部分逻辑,修改完代码如下:

#include<iostream>
#include<time.h>
#define  uint unsigned int 
using namespace std;
bool _isPrime(int num) {
	if (num < 2)return false;
	int stop = sqrt(num);
	for (int i = 2; i <= stop; i++) {
		if (num % i == 0) return false;
	}
	return true;
}
int main() {
	uint N, got = 0, i = 2, mark_space = 15485864;
	cin >> N;
	//bool* isPrime = new bool[N + 1];
	bool* isPrime = new bool[mark_space];
	//初始化
	for (int i = 0; i < mark_space; i++) isPrime[i] = 1;
	//uint* primes = new uint[N + 1];
	uint* primes = new uint[N];
	//for (uint i = 2; i <= N; i++) {
	//	//如果是素数则入栈
	//	if (isPrime[i]) primes[got++] = i;
	//	//筛除,并且并且下标大于N以后的自然数不需要标记
	//	for (uint j = 0; j < got&&primes[j]*i<=N; j++) {
	//		isPrime[primes[j] * i] = 0;
	//		//确保a是c的最小素因数
	//		if (i % primes[j] == 0) break;
	//	}
	//}
	time_t s = clock();
	while (got < N) {
		if (isPrime[i])primes[got++] = i;
		/*if (i * i >= mark_space) {
			uint new_space = i * i + 1;
			bool* t;
			do {
				t = new bool[new_space];
			} while (t == NULL);
			for (int j = 0; j < new_space; j++) {
				if (j < mark_space) t[j] = isPrime[j];
				else t[j] = 1;
			}
			delete[] isPrime;
			isPrime = t;
			mark_space = new_space;
		}*/
		for (int j = 0; j < got&& primes[j] * i<mark_space; j++) {
			isPrime[primes[j] * i] = 0;
			if (i % primes[j] == 0) break;
		}
		i++;
	}
	time_t e = clock();
	cout << "筛选法耗时" << difftime(e, s) << "ms"<<endl;
	got = 0;
	i = 2;
	s = clock();
	while (got < N) {
		if (_isPrime(i++)) primes[got++] = i - 1;
	}
	e = clock();
	cout << "函数判断法耗时" << difftime(e, s) << "ms";
	delete[] isPrime,primes;

}

运行结果:
在这里插入图片描述
————这才像样

虽然比较正统的做法不具有实践意义,但是只要我们稍微变通一下,把isPrime的空间定为15485864,就代表我们可以实现快速求少于一百万个的前N个素数,基本都能满足需求了。换而言之,线性筛法还是更加适用于求某个自然数区间的所有素数。

  • 8
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
### 回答1: 线性是一种常用的素数的方,它的思想简单且高效。下面是使用Java语言实现线性素数的代码: ```java import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class LinearSieve { public static List<Integer> getPrimes(int n) { boolean[] isComposite = new boolean[n + 1]; List<Integer> primes = new ArrayList<>(); for (int i = 2; i <= n; i++) { if (!isComposite[i]) { primes.add(i); } for (int j = 0; j < primes.size() && i * primes.get(j) <= n; j++) { isComposite[i * primes.get(j)] = true; if (i % primes.get(j) == 0) { break; } } } return primes; } public static void main(String[] args) { int n = 100; List<Integer> primes = getPrimes(n); System.out.println("从 2 到 " + n + " 的素数为:"); for (int prime : primes) { System.out.print(prime + " "); } } } ``` 以上代码中,我们使用了一个布尔数组`isComposite`来标记是否为合数。初始时,将所有数都标记为非合数。然后从2开始,遍历到n,如果某个数i是合数,则跳过;如果是素数,则将其加入到素数列表中,并标记它的倍数为合数。遍历结束后,我们得到了从2到n的所有素数。 在main函数中,我们设置n为100,调用`getPrimes`函数获取从2到100的素数,并打印出来。 运行结果为:从 2 到 100 的素数为:2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97。 ### 回答2: 线性是一种高效地选出一定范围内的素数的算。下面是用Java实现线性素数的代码: ```java import java.util.*; public class LinearSieve { public static List<Integer> sieve(int n) { boolean[] isPrime = new boolean[n + 1]; Arrays.fill(isPrime, true); // 将所有数初始化为素数 List<Integer> primes = new ArrayList<>(); for (int i = 2; i <= n; ++i) { if (isPrime[i]) { primes.add(i); // 将素数加入结果列表 } for (int j = 0; j < primes.size() && i * primes.get(j) <= n; ++j) { isPrime[i * primes.get(j)] = false; // 将当素数倍数标记为非素数 if (i % primes.get(j) == 0) { break; // 若当数为素数倍数,跳出内层循环 } } } return primes; } public static void main(String[] args) { int n = 100; // 范围上限 List<Integer> primes = sieve(n); System.out.println("范围[2, " + n + "]内的素数有:"); for (int prime : primes) { System.out.print(prime + " "); } } } ``` 通过线性,我们首先将所有数初始化为素数,然后从2开始,将每个素数的倍数标记为非素数,直到选结束。最后,将选出的素数存入结果列表中。在上述代码中,我们以100为例,调用`sieve`方解范围内的素数,并输出结果。 当我们运行上述代码时,将会得到范围[2, 100]内的素数列表: ``` 范围[2, 100]内的素数有: 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 ``` 以上就是使用Java实现线性素数的代码及结果。 ### 回答3: 线性是一种用于素数的算,可以高效地找出某一个范围内的所有素数。下面是使用Java语言实现线性素数的代码: ```java import java.util.ArrayList; import java.util.List; public class PrimeNumbers { public static List<Integer> getPrimeNumbers(int n) { List<Integer> primeNumbers = new ArrayList<>(); boolean[] isComposite = new boolean[n + 1]; for (int i = 2; i <= n; i++) { if (!isComposite[i]) { primeNumbers.add(i); } for (int j = 0; j < primeNumbers.size() && i * primeNumbers.get(j) <= n; j++) { isComposite[i * primeNumbers.get(j)] = true; if (i % primeNumbers.get(j) == 0) { break; } } } return primeNumbers; } public static void main(String[] args) { int n = 100; List<Integer> primeNumbers = getPrimeNumbers(n); System.out.println("在[2, " + n + "]范围内的素数有:"); for (int number : primeNumbers) { System.out.println(number); } } } ``` 这段代码使用了一个布尔数组isComposite来记录某个数是否为合数(非素数),初始时假设所有数都是质数,然后从2开始遍历到n,如果某个数i没有被标记为合数,就将其添加到素数列表中,并将i与已有的质数依次相乘,将其标记为合数。 运行以上代码,可以解出2到100之间的所有素数。输出结果如下: ``` 在[2, 100]范围内的素数有: 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 ``` 通过线性,我们可以高效地找到某个范围内的素数,而不需要遍历所有的数进行判断。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值