素数筛选,从O(n^2)到O(n),从暴力到线性筛,从小白到欧几里得

1暴力筛选素数

从2开始:4 6 8 10 12 14 16 18 20 …都是合数
从3开始:6 9 12 15 18 21 24 …都是合数
从4开始:8 12 16 20 24 28 …都是合数


从2开始依次向后开始遍历(i),每次遍历过程中,都乘以2,乘以3,乘以4标记(这里写成每次都加上i,也就是相乘的意思)

#include <stdio.h>
#define MAX_N 100

int prime[MAX_N + 5];
void init(){
    for(int i = 2; i <= MAX_N; i++){
        for(int j = 2 ; j <= MAX_N; j += i){
            prime[j] = 1;
        }
    }
    return ;
}
2少重复一点

每次遍历i的时候,比i小的数可以不用判断了,
比如:
从2开始:4 6 8 10 12 14 16 18 20 …都是合数
从3开始:6 9 12 15 18 21 24 …都是合数
从4开始:8 12 16 20 24 28 …都是合数


3不用从32开始
4不用从4
2,4*3开始
因为前面肯定访问过了。
修改j = i * i
改进之后:
从2开始:4 6 8 10 12 14 16 18 20 …都是合数
从3开始:9 12 15 18 21 24 …都是合数
从4开始:16 20 24 28 …都是合数

#include <stdio.h>
#define MAX_N 100

int prime[MAX_N + 5];
void init(){
    for(int i = 2; i <= MAX_N; i++){
        for(int j = i * i ; j <= MAX_N; j += i){
            prime[j] = 1;
        }
    }
    return ;
}
3再少重复一点

每次遍历到合数的时候也不需要判断了
比如:
从2开始:4 6 8 10 12 14 16 18 20 …都是合数
从3开始:9 12 15 18 21 24 …都是合数
从4开始:16 20 24 28 …都是合数


4的所有标记肯定都被比他小的一个数标记了,因为它是合数,肯定能被分解。
所以,我们只需要对每个素数i标记就行合数肯定会被之前标记过
改进:
增加一句if(prime[i]) continue;
从2开始:4 6 8 10 12 14 16 18 20 …都是合数
从3开始:9 12 15 18 21 24 …都是合数
从4开始:16 20 24 28 …都是合数
从5开始:25 30 35 40 45 50…都是合数
从6开始:36 42 48 54 60 …都是合数
从7开始:49 56 63 70 77 …都是合数

#include <stdio.h>
#define MAX_N 100

int prime[MAX_N + 5];
void init(){
    for(int i = 2; i <= MAX_N; i++){
    	if(prime[i]) continue;
        for(int j = i * i ; j <= MAX_N; j += i){
            prime[j] = 1;
        }
    }
    return ;
}
4一个都不允许重复

观察:
从2开始:4 6 8 10 12 14 16 18 20 22 24 26…都是合数
从3开始:9 12 15 18 21 24 27 30…都是合数
从5开始:25 30 35 40 45 50…都是合数
从7开始:49 56 63 70 77 …都是合数


还是存在一定数字是被重复标记的,比如24、30等等
能不能再优化一下呢?
思考…


24被第一个碰到的素数2标记之后,就不应该被3这个素数标记了
30被第一个碰到的素数2标记之后,就不应该被3这个素数标记了
30被第一个碰到的素数2标记之后,就不应该被5这个素数标记了


如果按照上面这种说法(狗屎理解气死人,大多数都是这么解释的)那就错了。
**24被第一个碰到的素数2标记之后,就不应该被3这个素数标记了
30被第一个碰到的素数2标记之后,就不应该被3这个素数标记了
30被第一个碰到的素数2标记之后,就不应该被5这个素数标记了


应该变为
24被第一个碰到的素数2标记之后,就不应该 只能被3这个素数标记
30被第一个碰到的素数2标记之后,就不应该 只能被3这个素数标记
30被第一个碰到的素数2标记之后,就不应该 只能被5这个素数标记


**
这么说是不是更蒙了?这也就是大多数人讲不清楚的地方,我们利用素数标记其他数时,会做许多多余的操作
2把不该它标记的24和30都标记了,以至于到了3标记的时候会重复,所以为了避免重复,我们强烈要求2不要多管闲事(而不是叫3不要去标记24),只标记前面几个数后面的数应该然后它们自己去标记。

这里我们需要回过头去看第1个知识点:
(i从2到MAX_N都枚举)
从2开始:4 6 8 10 12 14 16 18 20 22 24 26…都是合数
从3开始:6 9 12 15 18 21 24 27 30…都是合数
从4开始:8 12 16 20 24 28 …都是合数
从5开始:10 15 20 25 30 35 40 45 50…都是合数
从6开始:12 18 24 30 36 42 48 …都是合数
从7开始:14 21 28 35 42 49 56 63 70 77 …都是合数


这里我们还需要回过头去看第3个知识点:
从2开始:4 6 8 10 12 14 16 18 20 …都是合数
从3开始:9 12 15 18 21 24 …都是合数
从4开始:16 20 24 28 …都是合数
从5开始:25 30 35 40 45 50…都是合数
从6开始:36 42 48 54 60 …都是合数
从7开始:49 56 63 70 77 …都是合数


在上两个知识点上,我们从2到MAX_N都遍历,但是取数的时候,只取素数(这个素数目前已知的素数)做乘积:
没取素数做乘积:
从2(素数)开始:4 6 8 10 12 14 16 18 20 22 24 26… 都是合数
从3(素数)开始:6 9 12 15 18 21 24 27 30… 都是合数
从4开始:8 12 16 20 24 28 … 都是合数
从5(素数)开始:10 15 20 25 30 35 40 45 50… 都是合数
从6开始:12 18 24 30 36 42 48 … 都是合数
从7(素数)开始:14 21 28 35 42 49 56 63 70 77 …都是合数


取素数做乘积:
从2(素数)开始:4 都是合数
从3(素数)开始:6 9 都是合数
从4 开始:8 12 16都是合数
从5(素数)开始:10 15 20 25(20不可以因为4是合数4*5不行)都是合数
从6 开始:12 18 24 30 36都是合数
从7(素数)开始:14 21 28 35 42 49都是合数
从8 开始:16 24 32 40 48 56 64都是合数
从9 开始:18 27 36 45 54 63 72 81都是合数
从10 开始:20 30 40 50 60 70 80 90 100 都是合数


这样确实可以少重复很多次,但一定不是线性的O(n)时间复杂度,因为12、16、18、24、这样的数字还会重复(虽然重复率被我们降到很低)
我们能不能保证一次都不重复呢?可以!一次都不重复筛选叫线性筛。线性筛:每个合数必有一个最小素因子。可以自证,也可也看百科。
12、16、18、24为什么会重复?不就是因为它的最小素因子是2吗?然后又被3这个素因子给标记了一次导致重复。
所以我们把刚才
取素数做乘积
增加一个限制,取到最小素数(最小素因子)做乘积就停止,这样就可以

取最小素数做乘积:
从2(素数)开始:4 都是合数
从3(素数)开始:6 9 都是合数
从4 开始:8 12 16 都是合数(此处改变,因为2是4的最小素因子)
从5(素数)开始:10 15 20 25(20不可以因为4是合数4*5不行)都是合数
从6 开始:12 18 24 30 36 都是合数(此处改变,因为2是6的最小素因子)
从7(素数)开始:14 21 28 35 42 49都是合数
从8 开始:16 24 32 40 48 56 64 都是合数(此处改变,因为2是8的最小素因子)
从9 开始:18 27 36 45 54 63 72 81 都是合数(此处改变,因为3是9的最小素因子)
从10 开始:20 30 40 50 60 70 80 90 100 都是合数(此处改变,因为2是10的最小素因子)


神奇!!!!!
我们在写清楚点:


取最小素数做乘积:
从2(素数) 开始: 4
从3(素数) 开始: 6 9
从4 开始: 8
从5(素数) 开始: 10 15 25
从6 开始: 12
从7(素数) 开始: 14 21 35 49
从8 开始: 16
从9 开始: 18 27
从10 开始: 20

欧几里得真的令人钦佩,大神!
下面来看代码:
我们每次都需要用到前面已经找到的素数,所以必须把素数保存一下,可以另外开一个数组保存,也可以在原数组上使用,不妨碍向后寻找素数。

int prime[MAX_N + 5];
void init(){
    for(int i = 2; i <= MAX_N; i++){
    	//如果prime[i]是0,是素数就往前搬,prime[0]是计数器
        if(!prime[i]) prime[++prime[0]] = i;
        .......
        .......
        .......
    }

    return ;
}

保存之后,我们只在素数里面筛选合数,当前i和每个素数相乘来标记

int prime[MAX_N + 5];
void init(){
    for(int i = 2; i <= MAX_N; i++){
    	//如果prime[i]是0,是素数就往前搬,prime[0]是计数器
        if(!prime[i]) prime[++prime[0]] = i;
        for(int j = 1; j <= prime[0]; j++){
            prime[prime[j] * i] = 1;
        }
    }

    return ;
}

此时需要线性筛的话就要把在最小素因子那里停下来,不要向后筛选了.怎么知道是不是最小素因子?i % prime[j] == 0只要能被除尽就是了,直接break

int prime[MAX_N + 5];
void init(){
    for(int i = 2; i <= MAX_N; i++){
    	//如果prime[i]是0,是素数就往前搬,prime[0]是计数器
        if(!prime[i]) prime[++prime[0]] = i;
        for(int j = 1; j <= prime[0]; j++){
            prime[prime[j] * i] = 1;
            if(i % prime[j] == 0) break;//第一个标记之后停掉,后面不用标记了,线性筛的重点
        }
    }

    return ;
}

特殊情况,不能越界哟 if(prime[j] * i > MAX_N) break;

  int prime[MAX_N + 5];
  void init(){
      for(int i = 2; i <= MAX_N; i++){
      	//如果prime[i]是0,是素数就往前搬,prime[0]是计数器
          if(!prime[i]) prime[++prime[0]] = i;
          for(int j = 1; j <= prime[0]; j++){
         		if(prime[j] * i > MAX_N) break;
              prime[prime[j] * i] = 1;
              if(i % prime[j] == 0) break;//第一个标记之后停掉,后面不用标记了,线性筛的重点
          }
      }
  
      return ;
  }
附录1:线性筛表格
取最小素数做乘积:
从2(素数)开始:4
从3(素数)开始:6 9
从4 开始:8
从5(素数)开始:10 15 25
从6 开始:12
从7(素数)开始:14 21 35 49
从8 开始:16
从9 开始:18 27
从10 开始:20
附录2:可运行代码
#include <stdio.h>
#define MAX_N 100

int prime[MAX_N + 5];
void init(){
    for(int i = 2; i <= MAX_N; i++){
        if(!prime[i]) prime[++prime[0]] = i;
        for(int j = 1; j <= prime[0]; j++){
            if(prime[j] * i > MAX_N) break;
            prime[prime[j] * i] = 1;
            if(i % prime[j] == 0) break;
        }
    }

    return ;
}


int main(){
    init();
    for(int i = 1; i <= prime[0]; i++){
        printf("%d\n", prime[i]);
    }
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值