使用线性筛以及素数筛求出某范围内的所有素数

使用线性筛以及素数筛求出某范围内的所有素数

一 素数的定义

我们规定:若一个数字的因数只有1和它本身,那么这个数就是素数(1除外,所以最小的素数是2)

二 如何编程实现?

1 暴力求解
#include <iostream>
#include <vector>
using namespace std;
#define max_n 1000      //表示求1000以内(包含1000)的所有素数

vector<int> prime;      //用于存储所有的素数

bool judge(int n) {     //评判函数,判断是否满足质数条件,是素数则返回true,否则返回false
    for (int i = 2; i * i <= n; i++) {
        if (n % i == 0) return false;
    }
    return true;
}

int main() {
    int cnt = 0;
    for (int i = 2; i <= max_n; i++) {
        if (judge(i)) {
            prime.push_back(i); cnt++;
        }
    }
    cout << "素数个数为:" << cnt << endl;
    //将prime中的素数输出
    vector<int>::iterator it;
    int tmp = 0;
    for (it = prime.begin(); it != prime.end(); it++) {
        printf("%d\t", *it);
        if (tmp++ % 10 == 9) printf("\n");
    }
    return 0;
}

注:甚至还有小伙伴使用更加暴力的做法,也就是在judge()函数中,for循环从2到n,而不是从2到 n \sqrt{n} n 。其实这样是完全没有必要的。

我们假设: n = a * b (假设a < b),那么此时一定有:a <= n \sqrt{n} n 并且 b => n \sqrt{n} n ,否则的话,若a b均小于或者a b均大于 n \sqrt{n} n ,那么他们的乘积一定不等于n。因此若for循环从2到 n \sqrt{n} n 此时还没有一个数,能够使 n % i == 0 成立,那n肯定是素数,从 n \sqrt{n} n 到n这部分数字完全是没有必要判断。

2 使用素数筛
2.1 素数筛的基本思想

若某一个数字为素数,那么该数字的整数倍(1倍除外)肯定是合数。
为此我们引入一个数组 prime[max_n + 5] = {0},其中max_n表示求解的是max_n以内的所有素数,数组初始化为0。并且我们规定若 prime[i] == 0,则 i 是素数,否则 i 为合数。
在这里插入图片描述

2.2 对应代码如下
#include <iostream>
using namespace std;
#define max_n 1000

int prime[max_n + 5] = {0};

void init() {
    for (int i = 2; i * i <= max_n; i++) {
        if (prime[i]) continue;     //若被标记,为合数不考虑
        for (int j = i * i; j <= max_n; j += i) {   //i为素数,此时将其倍数做标记,表示为合数
            prime[j] = 1;
        }
    }
    //为了后面操作方便,我们从prime[1]向后依次存放找到的素数
    for (int i = 2; i <= max_n; i++) {
        if (!prime[i]) prime[++prime[0]] = i;
    }
}

int main() {
    init();
    int tmp = 0;
    cout << "素数的个数为:" << prime[0] << endl;
    for (int i = 1; i <= prime[0]; i++) {
        printf("%d\t", prime[i]);
        if (tmp++ % 10 == 9) printf("\n");
    }
    return 0;
}

注:本实验代码的核心就是init()函数中的两个for循环嵌套。总的来说我们就是要把素数的整数倍(1倍除外)标记为1。但是细心的小伙伴会问,这样的话,里层for循环 j 应该从 2 ∗ i 2 * i 2i开始呀,为什么是 i 的平方开始呢?

举个例子吧,假设现在 i = 5,prime[5] = 0,因此我们进入里层循环,如果 j 从 2 ∗ i 2 * i 2i开始,也就是 2 * 5开始,标记prime[10] = 1。这样确实可以,但是在prime[2] = 0的时候,也就是2为素数,此时我们已经把所有2的倍数全部置成1了,也就是说prime[10]早已经被标记为1。此时若再标记,不就是重复操作了吗!!!

2.3 素数筛算法的时间以及空间复杂度

空间复杂度: O(n)
时间复杂度: O(n * log(log(n))) //时间复杂度不太确定,回头我再看看

3 使用线性筛
3.1 素数筛的不足

举个例子,对于数字30而言,在素数筛中它肯定被标记为1,但是它被标记为1的次数,有几次?,仅仅只有一次吗? 答案显然不是!!

数字30 被2标记了一次,被3标记了一次,被5标记了一次,也就是说30这个数字一共被标记了3次。
“不对不对”,可能会有小伙伴问,你之前在素数筛中不是存在过重复标记吗?当时你不是把内层循环从i * i 开始,减少了重复标记呀!!

对,但这只是减少标记,我们可以顺着程序流程走一遍:2是素数,所以我们从4开始,把所有2的倍数全部标记为1;3是素数,所以我们从9开始,把所有3的倍数全部标记为1;5也是素数,因此我们从25开始,把所有5的倍数全部置为1。显然在内层循环中,30被标记了3次。

所以有没有更好的方法,可以每一个数字只会被标记一次呢? 答案就是: 线性筛 !

3.2 线性筛的基本思想

在素数筛中我们根据素数来标记素数的整数倍,但线性筛中我们使用其他的数。

我们使用整数M,来标识整数N,此时M和N存在如下关系:

  1. 设 p 是 N 的最小质因数
  2. N = p * M
  3. p 比 数字M的最小质因数还要小
  4. 此时我们可以标记 M * p’ (p’是小于p的所有素数的集合中的元素) 位置上的数字
    在这里插入图片描述
    为了彻底明白,大家可以手写模拟2到30的标记过程,若其中有一个数字被标记了两次或以上,那么肯定有哪里出错了,自己再回头看看基本思想。
3.3 线性筛代码实现
#include <iostream>
using namespace std;
#define max_n 1000

int prime[max_n + 5] = {0};

void init() {
    for (int i = 2; i <= max_n ;i++) {
        //若是素数,此时将从prime[1]开始向后存储求出的素数,所以prime[0]表示素数个数
        if (!prime[i]) prime[++prime[0]] = i;
        for (int j = 1; j <= prime[0]; j++) {
            if (i * prime[j] > max_n) break;
            prime[i * prime[j]] = 1;
            if (i % prime[j] == 0) break;
        }
    }
}

int main() {
    init();
    int tmp = 0;
    printf("素数个数是:%d\n", prime[0]);
    for (int i = 1; i <= prime[0]; i++) {
        printf("%d\t", prime[i]);
        if (tmp++ % 10 == 9) printf("\n");
    }
    return 0;
}

注:代码实现时,用原数组从prime数组1号位开始向后存储素数, prime[0]存储当前素数的个数。程序会有点难理解 (毕竟自己是过来人,知道不容易),但还请仔细研读。

3.3 线性筛算法的时间以及空间复杂度

顾名思义:
空间复杂度: O(n)
时间复杂度: O(n)
因此大家以后遇到求解素数相关的问题时,尽量使用线性筛。

线性筛算法绝不仅仅只用来求解素数问题,它更多的是为用作一种算法框架,在许多其他的算法中也会用到,切记!!!!!!!!

三 题外话

  1. 这是自己第一次在CSDN上写博客,虽然自己之前用过Markdown格式编辑器(Typora),但是两者之间的操作还是存在区别的,不过还好,不会的可以上网搜索,毕竟是自己的处女作,值得留念!
  2. 贬低自己的话就不想再说了,知道自己的不足,才会迈向成功。自己也不想立flag,只想说:“不求自己一定要有多么的努力,但求自己可以一步一个脚印,每一天都在进步”,加油,路漫漫其修远兮,吾将上下而求索,与君共勉!
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值