素数筛和线性筛
1 素数筛
1.1 素数的定义
素数,又称质数。若一个大于等于2的正整数,除了1和它本身不再有其他约数,则称这个数为素数。相对的,除了1和自身外还有其他约数的数,则称之为合数。素数筛,也叫埃氏筛法,顾名思义,就是用于从自然数中筛选出素数的算法。
1.2 素数筛流程
在介绍素数筛之前,我们首先要明确的一点是,任何一个合数,都可以由多个素数相乘得到。因此,如果我们可以通过素数去标记所有的合数。
我们知道最小的素数为2,因此从2开始的自然数序列如下。
2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , ⋯ ⋯ 2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,\cdots \cdots 2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,⋯⋯
首先我取出素数2,存入素数数组。接着我们将对剩余的自然数序列中,所有2的倍数的数标记为合数。为方便显示,我们对素数2加粗,并用删除符号删除2的倍数,这些一定是合数,如下:
2
,
3
,
4
,
5
,
6
,
7
,
8
,
9
,
1
0
,
11
,
1
2
,
13
,
1
4
,
15
,
1
6
,
⋯
⋯
\bm2,3,\sout4,5,\sout6,7,\sout8,9,\sout1\sout0\sout,11,\sout1\sout2,13,\sout1\sout4,15,\sout1\sout6,\cdots \cdots
2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,⋯⋯接着我们取出该序列中下一个值3,该值不是2的倍数,因此没有被删除,表明它也是素数。因此我们接下来通过3来标记合数,对自然数序列中所有3倍数的值进行标记。
2
,
3
,
4
,
5
,
6
,
7
,
8
,
9
,
1
0
,
11
,
1
2
,
13
,
1
4
,
1
5
,
1
6
,
⋯
⋯
\bm2,\bm3,\sout4,5,\sout6,7,\sout8,\sout9,\sout1\sout0\sout,11,\sout1\sout2,13,\sout1\sout4,\sout1\sout5,\sout1\sout6,\cdots \cdots
2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,⋯⋯如此循环下去我们就能在一定范围内筛选出所有的素数。
1.3 素数筛代码
1.3.1 c语言代码
// num[]为一个长度max_n的数组,如果i为合数,则将num[i] = 1, 素数则为num[i] = 1
int num[max_n] ; // 初值为0
int prime[max_n] ; //用于存储质数
void prime_filter(int *num, int *prime, int n) {
int k = 0;
for (int i = 2; i <= n; i++) {
if (!num[i]) continue;
prime[++k] = i;
for (j = i * 2; j <= n; j += i) {
num[j] = 1;
}
}
return ;
}
1.3.2 python生成器的方案
这边多提一嘴python的实现,以上c语言实现的时候,在堆区开辟了一个长度为max_n的静态数组,需要一定的存储。在Python中,这种一边循环一边计算的机制,称为生成器,他可以将某一种推算规则存储起来,比如斐波那契数列,知晓前两个数字后便可以推算出下一个,不需要把每一个数字都存储下来。在生成器中,只要每次调用next()便可以获取下一个推算结果,具体可以看廖雪峰老师的python教程(c语言包括c++等高级语言应该也是有类似的功能的,但是学艺不精暂时还不清楚就不多提了,=。=)
2 线性筛
2.1 线性筛原理
素数筛通过素数去标记掉合数来筛选素数,但是由于一个合数是由多个素数相乘得到,因此势必会被标记多次。这就会使得时间复杂度在一定程度上有所增加。而线性筛的目的就是避免多次标记合数,以达到时间复杂度的提高。
线性筛通过某一个合数的最大因子去标记这个合数,一个合数的最大因子总是唯一的,因此就可以避免多次标记的可能。
通常的,一个最大因子对应了好几个合数,也就是说,好几个合数的最大因子是相同的。例如,我们取最大因子25,那么最大因子为25的合数有50、75和125。
那么如何通过最大因子去确定这个合数?
首先,我们知道合数可以由多个素数相乘得到,假设一个合数由n个素数相乘得到,那么它的最大因子一定是由其中最大的n-1个素数相乘得到,而最小因子就是剩余的那个素数。因此当我们知道这个最大因子之后,我们只需要知道剩余的那个最小因子便可以获取该最大因子对应的合数。很显然,剩余的最小因子是一个小于等于最大因子中最小因子的素数。
例如上面讲到的最大因子25,可以表示为5*5,由两个素数5相乘得到,因此25的最小因子是5。小于等于5的素数分别有2、3、5。而25作为最大因子所对应的合数中, 50 = 5 ∗ 5 ∗ 2 50=5*5*2 50=5∗5∗2,2是合数50的最小因子; 75 = 5 ∗ 5 ∗ 3 75=5*5*3 75=5∗5∗3,3是合数75的最小因子; 125 = 5 ∗ 5 ∗ 5 125=5*5*5 125=5∗5∗5中,5是合数125的最小因子。可见所有被标记的合数的最小因子都是小于等于合数最大因子中最小因子的值。
再例如最大因子是35,可以表示为7*5,因此35的小因子是5,而使得35成为最大因子的合数的最小因子的可能就有2、3、5。因此通过35可以标记的合数有 75 = 35 ∗ 2 75=35*2 75=35∗2、 105 = 35 ∗ 3 105=35*3 105=35∗3、 175 = 35 ∗ 5 175=35*5 175=35∗5
2.2 线性筛流程
对于2开始的自然数序列:
2
,
3
,
4
,
5
,
6
,
7
,
8
,
9
,
10
,
11
,
12
,
13
,
14
,
15
,
16
,
⋯
⋯
2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,\cdots \cdots
2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,⋯⋯我们首先取到2并将其加入素数数组进行储存,接着我们将2当做最大因子去标记合数,最大因子2的最小因子也是2,因此2能标记的合数的最小因子也只能是2,如果大于2,2就不是这个合数的最大因子了,与我们的假设违背。因此我们通过最大因子2和最小因子2标记掉了合数
2
∗
2
=
4
2*2=4
2∗2=4
2
,
3
,
4
,
5
,
6
,
7
,
8
,
9
,
10
,
11
,
12
,
13
,
14
,
15
,
16
,
⋯
⋯
\bm2,3,\sout4,5,6,7,8,9,10,11,12,13,14,15,16,\cdots \cdots
2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,⋯⋯
接着我们取下一个数字3。将其作为最大因子去标记合数,3的最小因子是3,小于等于3的素数有2和3,因此我们将3最为被标记合数的最大因子,2和3作为被标记合数的最小因子,可以标记掉
3
∗
2
=
6
3*2=6
3∗2=6和
3
∗
3
=
9
3*3=9
3∗3=9
2
,
3
,
4
,
5
,
6
,
7
,
8
,
9
,
10
,
11
,
12
,
13
,
14
,
15
,
16
,
⋯
⋯
\bm2,\bm3,\sout4,5,\sout6,7,8,\sout9,10,11,12,13,14,15,16,\cdots \cdots
2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,⋯⋯接着我们通过5可以标记掉
5
∗
2
=
10
、
5
∗
3
=
10
、
5
∗
5
=
25
5*2=10、5*3=10、5*5=25
5∗2=10、5∗3=10、5∗5=25,以此递推下去便可以标记掉所有合数,且每个合数只被标记掉一次。
2.3 c语言代码
// num[]为一个长度max_n的数组,如果i为合数,则将num[i] = 1, 素数则为计入num数组中,得到num[x] = i
int num[max_n] ; // 初值为0
int prime[max_n] ; //用于存储质数
void linear_filter((int *num, int *prime, int n) {
int k = 0;
for (int i = 2; i <= n; i++) { // i 为最大因子
if (!num[i]) prime[k++] = i; //k 记录素数个数
for (int j = 0; j < k; j++) {
if (i * prime[j] > n) break; // 超出标记范围
num[i * prime[j]] = 1;
if (i % prime[j] == 0) break; //当最小因子等于最大因子中的最小因子时,不在增加
}
}
return ;
}
最后:介绍可能有些繁琐了,慢慢来做些删减调整逻辑。