使用线性筛以及素数筛求出某范围内的所有素数
一 素数的定义
我们规定:若一个数字的因数只有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 2∗i开始呀,为什么是 i 的平方开始呢?
举个例子吧,假设现在 i = 5,prime[5] = 0,因此我们进入里层循环,如果 j 从 2 ∗ i 2 * i 2∗i开始,也就是 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存在如下关系:
- 设 p 是 N 的最小质因数
- N = p * M
- p 比 数字M的最小质因数还要小
- 此时我们可以标记 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)
因此大家以后遇到求解素数相关的问题时,尽量使用线性筛。
线性筛算法绝不仅仅只用来求解素数问题,它更多的是为用作一种算法框架,在许多其他的算法中也会用到,切记!!!!!!!!
三 题外话
- 这是自己第一次在CSDN上写博客,虽然自己之前用过Markdown格式编辑器(Typora),但是两者之间的操作还是存在区别的,不过还好,不会的可以上网搜索,毕竟是自己的处女作,值得留念!
- 贬低自己的话就不想再说了,知道自己的不足,才会迈向成功。自己也不想立flag,只想说:“不求自己一定要有多么的努力,但求自己可以一步一个脚印,每一天都在进步”,加油,路漫漫其修远兮,吾将上下而求索,与君共勉!