在C语言学习中,了解到一个这么一个算法,简单记录一下自己的理解。
1、素数
素数也叫作质数,就是只能被1和它自己整除的自然数,与之相对的数叫做合数。1既不是素数也不是合数。
找100以内的所有素数,简单的实现方法就是将每个数从2开始整除,一直除到n - 1,如果都不能被整除,则说明这个数为素数。C语言代码实现如下:
#include<stdio.h>
#define MAX_N 100
int is_prime(int n){
for(int j = 2; j < n; ++j){
if(n % j == 0) return 0;
}
return 1;
}
int main(){
for(int i = 2; i < MAX_N; ++i){
if(is_prime(i)) printf("%d\n", i);
}
return 0;
}
以上代码的时间复杂度为O(n²),但我们可以优化一下,除数只需要枚举到n的开平方就能判断是否是素数了。因为数n如果有比n的开平方小的因子,那它一定有一个相对应的比n的开平方大的因子,所以循环到n的开平方就没有向上找的必要了,代码如下:
#include<stdio.h>
#define MAX_N 100
int is_prime(int n){
for(int j = 2; j * j <= n; ++j){
if(n % j == 0) return 0;
}
return 1;
}
int main(){
for(int i = 2; i < MAX_N; ++i){
if(is_prime(i)) printf("%d\n", i);
}
return 0;
}
以上算法的时间复杂度为O(n * lbn)
2、素数筛
除了以上的算法以外,我们还可以创建一个数组,下标为合数的则标记为1,素数则标记为0。
总体的思想是用素数去标记合数,如2是素数,则4 = 2 * 2, 6 = 2 * 3, 8 = 2 * 4都是合数,代码实现如下。
#include<stdio.h>
#define MAX_N 100
int nums[MAX_N + 5];
void select_prime(){
for(int i = 2; i <= MAX_N; ++i){
// 0为素数,1为合数
if(!nums[i]) {
nums[++nums[0]] = i; // nums[0]记录素数的个数,nums[0]之后记录素数
for(int j = i; j <= MAX_N / i; ++j){
nums[j * i] = 1; // 用素数标记合数
}
}
}
return;
}
int main(){
select_prime();
// 输出所有素数
for(int i = 1; i <= nums[0] ; ++i){
printf("%d\n", nums[i]);
}
return 0;
}
以上代码的空间复杂度为O(n),时间复杂度为O(n * loglog n)
3、线性筛
素数筛的时间复杂度为O(n * loglog n),而不是O(n),是因为在素数筛中会出现重复标记的问题,如 2 * 3 = 6, 3 * 2 = 6,可以看出数字6被重复标记了。为了解决这个问题,保证每个数只被标记一次,所以用到了线性筛算法,算法思想是用一个整数M去标记合数N,其中N和M有以下性质:
- N中的最小素数为P,
- N可以表示为P * M
- P一定小于等于M中最小的素数因子
- 利用M * P’ (所有不大于M中最小素数的集合)标记N’ 、N’’、N’’’
C语言代码如下
#include<stdio.h>
#define MAX_N 100
int nums[MAX_N + 5];
void select_prime(){
for(int m = 2; m <= MAX_N; ++m){
if(!nums[m]) nums[++nums[0]] = m;
for(int i = 1; i <= nums[0]; ++i){
if(nums[i] * m > MAX_N) break;
nums[nums[i] * m] = 1;
if(m % nums[i] == 0) break;
}
}
}
int main(){
select_prime();
for(int i = 1; i <= nums[0] ; ++i){
printf("%d\n", nums[i]);
}
return 0;
}
线性筛算法能保证每个数字只被标记一次,因此该算法的空间复杂度和时间复杂度均为O(n)。