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()这个函数的,这一点必须要明确(很神奇吧)
- 普通筛选法(埃式筛法)(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 | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | … | N |
isPrime | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | … | 1 |
got = 0 | |||||
---|---|---|---|---|---|
primes |
i为当前需要判断的自然数,由2(最小的素数)开始。由于任何两个大于1的自然数相乘都是合数,所以i的任意大于1的整数倍(i*2,i*3…)都是合数,因此我们将所有小于等于N且为2的倍数的数都筛除,并且将2放进primes中,并让got右移。(为了填表方便姑且当N是偶数)
i | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | … | N |
isPrime | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | … | 0 |
got = 1 | |||||
---|---|---|---|---|---|
primes | 2 |
筛除掉2的倍数之后我们将i右移至下一个没被筛除掉的自然数,也就是3.
i | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | … | N |
isPrime | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | … | 0 |
继续将3入primes栈,并且筛除3的倍数
i | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | … | N |
isPrime | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 1 | 0 | 0 | … | 0 |
got = 2 | |||||
---|---|---|---|---|---|
primes | 2 | 3 |
以此类推。。这样我们在推进i的时候每当遇到isPrime[i]为0的自然数时我们就可以直接跳过,继续将i右移一格(比如上面的表格的下一状态i=5而不是4)。那这样做会不会把合数判定成素数呢?那是不会的。 我们观察primes栈,可以知道求得的素数一定是升序入栈的,并且由于筛除操作,i不是primes栈中任一素数的整数倍。而且我们知道任一合数都可以由有限个素数相乘得到,所以我们可以知道,任何两个小于i的素数(也就是primes中的素数)相乘均不等于i,因此i为继已求得的素数后的下一个最小素数。这也是为什么前面提到的不需要用到判断一个数是否为素数的函数。
但是,这样子问题也很显而易见,观察上面的表格,我们可以发现当i=2和i=3的时候,我们都筛除了合数6,一个数被重复筛除,就会导致性能的下降,所以在这个基础上,我们还可以做进一步的优化,也就是线性筛法。
- 线性筛法(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 | |||||
---|---|---|---|---|---|
primes | 2 | 3 |
则primes[0]*i = 2*3 = 6,筛去6;primes[1]*i = 3*3 = 9,筛去9。
i | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | … | N |
isPrime | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 1 | 1 | 0 | … | 1 |
重点来了,怎么确保a是c的最小质因数?
前面讲到了,a是primes栈中从小到大取出的素数,那a就一定会遍历完当前primes的所有素数吗?答案是否定的。
我们看下面一个例子:
i | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | … | N |
isPrime | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 1 | 1 | 0 | … | 0 |
got = 2 | |||||
---|---|---|---|---|---|
primes | 2 | 3 | |||
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 | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | … | N |
isPrime | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 1 | 0 | 0 | … | 0 |
此时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的所有素数测试结果:
改造成计算前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个素数,基本都能满足需求了。换而言之,线性筛法还是更加适用于求某个自然数区间的所有素数。