引言
最近在学习数据结构与算法,偶然看到素数筛选的题目,在阅读了多位大佬的博文后总结了素数筛选的三种算法。其中有一些个人的理解,如有错误,还请大家不吝赐教。
问题引入
统计n以内的素数个数,0、1除外
例:输入:100 输出:25
算法
1.暴力算法
暴力算法即遍历寻找素数的个数。在判断某一个数是否为素数时,只需要从2开始,遍历到
n
\sqrt{n}
n。举个例子,合数8,可以是2*4,也可以是4*2,当判断了2为该数的一个质因子后,该数就已经被认为不是素数了,后面的4也就没有必要判断了,而这个判断的分界点恰好就是
n
\sqrt{n}
n。而在C语言中j<=
n
\sqrt{n}
n可以表示为j*j<=n.
也正是如此,传统暴力算法的时间复杂度就是O(n
n
\sqrt{n}
n)
//利用两层for循环,第一层遍历2-n的每一个数
//第二层判断这个数是不是素数
void brute_force(int n)//传入n判断n以内有多少个素数
{
int count = 0;//记录素数个数
int i,j;
int flag;//定义一个变量,若为合数,则该变量的值变为0
for (i = 2; i < n; i ++)
{
flag = 1;//先假定该数为素数
for (j = 2; j * j <= i; j ++)//也可以用sqrt()函数计算根号n(注意头文件的引用)
{
if (i % j == 0)//找到这个数的质因子
{
flag = 0;//flag==0说明这个数是合数
break;
}
}
if (flag)
count++;
}
printf("暴力算法 %d以内有%d个素数\n", n, count);
}
可以注意到,素数除了2以外都是奇数,因此,我们可以对暴力算法优化一下
void brute_force(int n)//暴力算法(优化后)
{
int count = 0;
int i,j;
int flag = 1;
if (n >=2)
count++;
for (i = 3; i < n; i += 2)//每次i加2,只找奇数中的素数
{
flag = 1;
for (j = 3; j * j <= i; j += 2)
{
if (i % j == 0)
{
flag = 0;
break;
}
}
if (flag)
count++;
}
printf("暴力算法 %d以内有%d个素数\n", n, count);
}
2.埃氏筛选
埃氏筛选的主要思想是先把n以内的合数全部找出来,合数排除以后,其余的就全部是素数了,运用的方法是以空间换时间。
实现方式:首先建立一个数组用以储存n以内的数的状态,如果是合数,则该以该数为下标的数值为1。如果不是合数,则为素数,值为0。通过这个素数i,去筛选出n以内以i为质因子的合数(2*i,3*i,……)
在埃氏筛选中,若内层循环的j是从2*i开始,则时间复杂度为O(n
log
n
\log{n}
logn);
若内层循环j是从i*i开始,则时间复杂度为O(n
l
o
g
l
o
g
n
log{log{n}}
loglogn)
void eratosthenes(int n)//埃氏筛选
{
int no_prime[10000] = {0};//先假定全部为素数。找到一个数不是素数,就把这个值变为1
int i, j;
int count = 0;
for (i = 2; i< n; i++)
{
if (!no_prime[i])//是素数
{
count++;
for (j = i * i; j < n; j += i)//将以i为质因子合数全部标记为1
no_prime[j] = 1;
}
}
printf("埃氏筛选 %d以内有%d个素数", n, count);
}
3.欧氏筛选
埃氏筛选中,利用每个素数的倍数,找到合数,但还是免不了一个合数被重复筛选。如合数45,在被3*15筛选一次的情况下,还会被5*9再次筛选,做了无用功。埃氏筛选不能避免的就是一个合数被多个素数因子筛选多次,而欧氏筛选恰好避开这一点,保证了每一个合数都是被它的最小素数因子筛选一次后不会再被其它素数因子筛选。
实现方式:以空间换时间,建立两个数组,第一个和埃氏筛选中的数组一样,标记数的类型(0为素数,1为合数);第二个数组则是储存n以内的所有素数。
内层循环的功能是将i与已知的素数相乘,把这个数标记为合数,如果已知的素数中有一个数是i的素数因子,则退出内层循环。
举个例子,假设i为4,这时is_prime[10000]数组中存有两个素数,2、3。在将4*2=8标记为合数之后,显然4%2==0,则会跳出内层循环。若不跳出,则4*3=12也会标记为合数;而当i为6时,6*2=12会再次被标记为合数。一个合数被两次标记,也就和埃氏筛选一样做了无用功。所以if语句必不可少!
void euler(int n)//欧氏筛选
{
int is_prime[10000];//储存素数
int no_prime[10000] = {0};//标记数,开始假定全部为素数
int count = 0;
int i, j;
for (i = 2; i < n; i++)
{
if (!no_prime[i])//如果i下标的数是素数,就把它存入素数的数组中
is_prime[count++] = i;
for (j = 0; j < count && i * is_prime[j] < n; j++)
{
no_prime[i * is_prime[j]] = 1;
if (i % is_prime[j] == 0)//关键,保证每一个数只被它的最小质因子筛出
break;
}
}
printf("\n欧氏筛选 %d以内有%d个素数", n, count);
}
输出结果
后记
暴力算法容易理解,但效率最低;埃氏筛选虽提高了效率,但在内层循环j=i*i中,若筛选大数时,则会出现数据溢出的情况;欧氏筛选即为线性筛选,时间复杂度是三种算法中最低的,但需要好好理解if语句的功能。
筛选素数的方法除了以上三种以外还有很多,本文未提及的还有寻找区间内素数个数的筛选算法。
第一次写文章,在加深理解的同时,希望对读者有所帮助。
参考:
Eratosthenes筛法(埃式筛法)时间复杂度分析