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,则有

本文介绍使用C++实现线性筛法求前N个素数的方法,包括普通筛选法(埃式筛法)及线性筛法的原理与代码实现,通过实例对比不同方法的效率。

1522

被折叠的 条评论
为什么被折叠?



