前言
素数求解是学习编程中的一道经典题目,在学校学习时老师也是经常用一种方法就打发了我们,那么求一段区间内的素数到底有哪些方法呢?到底那种方法计算会最快,效率最高?接下来我们就来讲讲素数求解的N种境界。
一.素数的定义
素数即质数,指一个大于1的自然数,除了1和它本身外,不能被其他自然数(质数)整除。通俗来说就是这个数除了1和它自身外再也没有其他因数。例如2,3,5,7等都是素数,它们除了1和本身外再无其他因子。2是最小的质数。
1既不是质数也不是合数。合数是与质数相对的数,合数是指除了1和它本身外还有其他因数的数。
例如2,4,6,8等皆是合数。
二.试除法
境界1-试除法(初级)
如何判断一个数是否为素数?
方法如下:
拿2到i-1的数字去试除i
1.如果i被整除,就说明i不是素数。
2.如果2到i-1之间的数字都不能整除i,说明i是素数。
代码如下:
#include<stdio.h>
int main()
{
int i = 0;
int count = 0;//素数个数
for (i = 100; i <= 200; i++)
{
int j = 0;
for (j = 2; j < i; j++)
{
if (i % j == 0)//i % j == 0说明i除了1和它自身外好友其他因数,所以i不是素数
{
break;
}
}
if (i == j)//i == j说明2到i-1之间没有i的因数
{
printf("%d ", i);
count++;
}
}
printf("\ncount=%d\n", count);
return 0;
}
上面这个代码可能看不出它高效不高效,判断一个程序高效不高效,主要看其在完成程序所用时间,我们对上述代码增加一个对循环次数的监测,这样通过观察循环次数就能够很直观的知道其高不高效。
#include<stdio.h>
int main()
{
int i = 0;
int count1 = 0;//素数个数
int count2 = 0;//循环次数
for (i = 100; i <= 200; i++)
{
int j = 0;
for (j = 2; j < i; j++)
{
count2++;
if (i % j == 0)
{
break;
}
}
if (i == j)
{
printf("%d ", i);
count1++;
}
}
printf("\ncount1=%d\n", count1);
printf("count2=%d\n", count2);
return 0;
}
从上面的代码中可以看到整个循环一共循环了3292次,下面我们来看第二种的解法。
境界2—优化版(去偶数)
由素数的定义可知素数只有1和它本身两个因子,那么大于2的偶数肯定都不是素数,因此我们在源头上就可以排除一大半。看代码:
#include<stdio.h>
int main()
{
int i = 0;
int count1 = 0;//素数个数
int count2 = 0;//循环次数
for (i = 100; i <= 200; i++)
{
int j = 0;
int flag = 1;//假设i是素数
for (j = 2; j < i; j++)
{
count2++;
if (i % j == 0)
{
flag = 0;//i不是素数
break;
}
}
if (flag == 1)//说明i是素数
{
printf("%d ", i);
count1++;
}
}
printf("\ncount1=%d\n", count1);
printf("count2=%d\n", count2);
return 0;
}
优化过后的代码总共循环了3241次,相较上一个代码确实是快了一些,但是不是很明显,上述中flag是一个标志,它代替了第一个代码素数的判断语句,算是第一个代码的另一种写法。但是显然,这不是最优解。接下来看第三种境界。
境界3(sqrt--开平方)
我们知道,一个合数除了1和它本身外还存在其他因子,而且这些因子中必定至少有一个小于等于这个数的开平方,例如3*4=12,3 < 2√3,再比如4 * 4 = 16,2 < 4,所以m * n = k,那么m 和 n 中至少有一个小于等于sqrt(k)。sqrt()是VS编译器中的一个库函数,功能是开平方。看代码:
#include<stdio.h>
#include<math.h>
int main()
{
int i = 0;
int count1 = 0;//素数个数
int count2 = 0;//循环次数
for (i = 101; i < 200; i+=2)//偶数不可能是素数
{
int flag = 1;//假设i为素数
for (int j = 2; j <= sqrt(i); j++)
{
count2++;
if (i % j == 0)
{
flag = 0;//i不是素数
break;
}
}
if (flag == 1)
{
printf("%d ", i);
count1++;
}
}
printf("\ncount1=%d\n", count1);
printf("count2=%d\n", count2);
return 0;
}
可见,count2=342,循环只循环了342次,这极大地减少了程序实现功能的时间,极大的提高了程序效率。这个程序相比以上两个程序的效率就有明显提升,循环次数少了10倍不止。其实总的来说,试除法都大同小异,只有这一种可能看起来比其他几种有着质的提升。所以这种方法我强烈推荐掌握,这样当你和别人共同写这一题时你打代码才会给人眼前一亮的感觉。这个境界就是最优解了吗?还有没有其他算法呢?我们再来看另一个境界。
三.筛选法
境界1—初级筛选法
筛选法是由古希腊时期名叫埃拉托斯特尼的一个人发现的,他发现可以通过在涂蜡的木板记下数字,接着在蜡上以记一个点来作为删去一个数字,然后从最小的质数开始一个接着一个的划掉这些质数的倍数的方法来实现求质数。质数查找完成后,这有着密密麻麻小点的涂蜡板看上去就像一个筛子,所以就把着这种方法叫做:“埃拉托斯特尼筛选”,简称:筛选法。
我们可以看出筛选法和试除法其实有着本质上的区别,试除法是判断每一个数是不是素数来达到目的;而筛选法不是如此,筛选法是将不是素数的数全部去除,然后得到余下的数实现素数的查找。
我们如何来筛选我们需要的数字和筛去我们不需要的数字呢?上面我们讲过,1既不是质数,也不是合数,因此首先就要把1划去,2是最小的素数,所以2不能筛去,但是区间内2的倍数要全部筛去,接着3是素数,3也不能筛去,同理3的倍数要全部筛去,然后留下5,7,筛去其全部倍数。可能还有一些小伙伴不清楚,我们看下面这个动态图就能直观的感受到了。
代码怎么实现呢?由上可知,我们只需先定义一个容量足够大的数组将所有数装下,然后将数组内所有元素初始化为1,再将2,3,5,7的全部倍数都赋值为0,那么剩下的就是素数。代码如下:
#include<stdio.h>
#include<math.h>
int main()
{
int i = 0;
int count1 = 0;//用以统计素数的个数
int count2 = 0;//用统计筛选次数
int arr[200] = { 0 };//创建数组
for (i = 2; i <= 150; i++)//初始化
{
arr[i] = 1;
}
for (i = 2; i <= 150; i++)
{
if (arr[i])
{
int j = 0;
printf("%d ", i);//打印素数
count1++;
for (j = i + i; j <= 150; j += i)//把素数的倍数都赋为0;
{
arr[j] = 0;
count2++;
}
}
}
printf("\ncount1=%d", count1);
printf("\ncount2=%d", count2);
return 0;
}
由上图代码可知用筛选法求区间内素数是可行的。但是这种筛选就完美吗?就不存在漏洞吗?
假如现在让你用筛选法求100到200区间内所有素数,你会怎么写?现在我们将100到200区间套入上述代码看一下还可不可行?代码如下:
#include<stdio.h>
int main()
{
int i = 0;
int count1 = 0;//统计素数的个数
int count2 = 0;//统计循环次数
int arr[210] = { 0 };
for (i = 101; i < 200; i++)//1既不是质数也不是合数
{
arr[i] = 1;
}
for (i = 101; i < 200; i++)//去偶数,偶数不可能是质数
{
int j = 0;
if (arr[i])
{
printf("%d ", i);//打印素数
count1++;
}
for (j = i + i; j < 200; j += i)
{
arr[j] = 0;
count2++;
}
}
printf("\n素数个数为:%d\n", count1);
printf("循环次数为:%d\n", count2);
return 0;
}
可见,代码完全可以运行起来,但是从控制台我们可以明显看出运行结果是错误的,虽然没有语法错误,但是结果不对。这是为什么呢?因为 筛选法只适合从一开始即数字2开始筛选,例如,如果你要求100到200这个区间内的素数,那么你用筛选法 就必须从2筛选到200,这是因为区间太小,而筛选数字太大,容易漏筛和错筛。
举个例子:100到200之间的第一个素数是101,而101的两倍已经超出了区间范围,更本无法进入第二个循环的内层for循环对其倍数进行赋0.当102进入循环时,arr[102]为真,符合判断条件,直接打印进素数行列内,然而102是个合数,并不是素数。这就是为什么容易漏筛和错筛的原因。所以筛选法只适合从一开始即数字2开始筛选。
那么用筛选法还能不能求100到200区间内素数的个数呢?答案是肯定的,当然可以。看代码:
#include<stdio.h>
int main()
{
int i = 0;
int count1 = 0;//统计素数的个数
int count2 = 0;//统计循环次数
int arr[400] = { 0 };
for (i = 2; i < 200; i++)//1既不是质数也不是合数
{
arr[i] = 1;
}
for (i = 2; i < 200; i++)
{
int j = 0;
if (arr[i])
{
if (i > 100 && i < 200)
{
printf("%d ", i);//打印素数
count1++;
}
}
for (j = i + i; j < 200; j += i)
{
arr[j] = 0;
count2++;
}
}
printf("\n素数个数为:%d\n", count1);
printf("循环次数为:%d\n", count2);
return 0;
}
可以看出用筛选法求区间内素数也是可行的。但是要循环689次 ,而用试除法只要342次。难道这就说明筛选法不行了吗?我们来看下面这个代码:
#include<stdio.h>
int main()
{
int i = 0;
int count1 = 0;//统计素数的个数
int count2 = 0;//统计循环次数
int arr[400] = { 0 };
for (i = 2; i < 200; i++)//1既不是质数也不是合数
{
arr[i] = 1;
}
for (i = 2; i < 200; i++)
{
int j = 0;
if (arr[i])
{
if (i > 100 && i < 200)
{
printf("%d ", i);//打印素数
count1++;
}
}
for (j = i * i; j < 200; j += i)
{
arr[j] = 0;
count2++;
}
}
printf("\n素数个数为:%d\n", count1);
printf("循环次数为:%d\n", count2);
return 0;
}
这是上面那个程序的优化版,至于为什么要这么修改,看完筛选法的境界2你就会明白,优化过后代码循环次数明显下降,效率名校提升。你可千万不要看他循环了351,而试除法的境界三只用了342次就觉得这个优化版的筛选法不行啊!你要明白这个筛选法的筛选区间可是1到200啊,而试除法境界三的试除区间远远没有这个大,但是它们的循环次数却只相差几次,可想而知,这筛选法的厉害之处。下面我们就来讲讲这个优化版的筛选法。
境界2——筛选法Plus
上述代码在我们筛选的过程中,有些数字被重复筛选,降低了筛选效率。比如6,6是2的倍数,也是3的倍数,所以2的倍数筛选一次,3的倍数筛选一次。再如12,2倍数筛选一次,3的倍数也筛选一次。这就导致了重复筛选,极大地降低了筛选效率。当我们筛选3的倍数时,6被筛选2的倍数的时候筛选了,而9没有被筛选;当我们筛选5的倍数时,10被筛选2的倍数的时候筛选了,15被筛选3的倍数的时候筛选了,20被筛选2的倍数的时候筛选了,只有25没有被筛选;可以看出,我们应当从素数平方的地方开始筛选,这样就可以很好的提高效率。看代码:
#include<stdio.h>
#include<math.h>
int main()
{
int i = 0;
int count1 = 0;//用以统计素数的个数
int count2 = 0;//统计筛选次数
int arr[200] = { 0 };//创建数组
for (i = 2; i <= 150; i++)//初始化
{
arr[i] = 1;
}
for (i = 2; i <= 150; i++)//用下标代表数字
{
if (arr[i])
{
int j = 0;
printf("%d ", i);//打印素数
count1++;
for (j = i * i; j <= 150; j += i)//从平方开始筛选,把素数的倍数都赋为0;
{
arr[j] = 0;
count2++;
}
}
}
printf("\ncount1=%d\n", count1);
printf("count2=%d\n", count2);
return 0;
}
同样是求1到150之间的所有素数,这个程序就只用了166次,明显快上来许多。这里所求区间太短,还不是很明显,当求更长区间时,你会发现其高效之处,这里就不再一一赘述。
总结
素数的求解方法有很多,有句俗话叫做活到老学到老,如何让你的代码让人感觉眼前一亮,学习更多的求解方法肯定是重中之重。它会让你的代码看起来更鲜活,更生动,最重要的是能让你又前进一步。素数求解的N种境界到此就要结束了,希望我的博客能帮到各位未来编程领域的大牛们。喜欢的小伙伴还望点赞收藏+关注。谢谢支持!