这是我非常喜欢的一道编程题目。不要小看这道题,它看似简单,实则奥妙无穷。由于这是C语言的入门篇,只介绍最简单,也最容易想到的方法:试除法。但哪怕是试除法,也有不少变化。
要想了解试除法,首先要知道什么是素数。简单来说(可能不太严谨,不过我们只重视思路):素数就是质数,也就是只能被1和本身整除的数。
所以就诞生了判断素数最简单、最朴素的方法,假设要判断数i是不是素数,只需要拿2~(i-1)之间的数去试除i,如果其中有一个数被i整除,那么i就不是素数;反之,如果2~(i-1)之间的数都不能被i整除,就说明i是素数。
比如说,判断7是不是素数,发现2,3,4,5,6都不能被7整除,得出结论:7是素数。或者判断,15是不是素数,发现2不能被15整除,但试到3发现可以被15整除,说明15是合数,不是素数。
有了这些铺垫,就很容易做出下面这道编程题:
打印100到200之间的素数,并统计个数
完整代码如下:
#include <stdio.h>// 打印100~200之间的素数,并统计个数
int main()
{int i = 0;int count = 0; // 统计个数// 产生100~200之间的整数for (i = 100; i <= 200; i++){int flag = 1; // 假设i是素数// 判断i是不是素数// 拿2~i-1的数去试除i,如果整除就不是素数,如果都不整除就是素数// 产生2~i-1的数int j = 0;for (j = 2; j < i; j++){if (i % j == 0){flag = 0; // 当i不是素数时把j改为0break;}}if (1 == flag) // 由于i没有被改成0,说明i是素数{count++;// 输出素数printf("%d ", i);}}// 输出个数printf("\ncount = %d\n", count);return 0;
}
这里先用for循环产生100~200之间的整数,然后产生2~(i-1)之间的数去试除i,如果整除就把flag置成0。当内层循环结束时,判断flag的值,如果是1,说明i没有被任何一个j整除,就说明i是素数;如果i是0,说明中间i被某一个j整除,从而说明i不是素数。
一定要用flag来判断是不是素数吗?其实不用。稍稍修改一下代码,就能把flag去掉
#include <stdio.h>// 打印100~200之间的素数,并统计个数
int main()
{int i = 0;int count = 0; // 统计个数// 产生100~200之间的整数for (i = 100; i <= 200; i++){// 判断i是不是素数// 拿2~i-1的数去试除i,如果整除就不是素数,如果都不整除就是素数// 产生2~i-1的数int j = 0;for (j = 2; j < i; j++){if (i % j == 0){break;}}if (i == j) // 判断i是不是素数,如果2~(i-1)之间的数都不能被i整除,j则为i的值才跳出上面这个循环{count++;// 输出素数printf("%d ", i);}}// 输出个数printf("\ncount = %d\n", count);return 0;
}
但是去掉flag后代码变得不那么好理解,所以我还是倾向于有flag的版本,接下来我们对有flag的版本做一些优化,提高代码的效率。
首先,一个很容易想到的点是,所有的偶数都肯定不是素数,所以只需产生所有的奇数即可。
#include <stdio.h>int main()
{int i = 0;int count = 0; // 统计个数// 产生100~200之间的整数for (i = 101; i < 200; i += 2) // 偶数肯定不是素数,所以只需产生100~200之间的奇数{int flag = 1; // 假设i是素数// 判断i是不是素数// 拿2~i-1的数去试除i,如果整除就不是素数,如果都不整除就是素数// 产生2~i-1的数int j = 0;for (j = 2; j < i; j++){if (i % j == 0){flag = 0; // 当i不是素数时把j改为0break;}}if (1 == flag) // 由于i没有被改成0,说明i是素数{count++;// 输出素数printf("%d ", i);}}// 输出个数printf("\ncount = %d\n", count);return 0;
}
接着就是重点了!首先存在下面这条定理(同样是不太严谨的表述,只是为了易于理解):如果一个数m能写成a*b的形式,那么a和b之中至少有一个数会小于或等于m的算术平方根!(定理1)
举个例子:100=4*25=10*10
当写成4*25时,4就比100的算术平方根(10)要小;当写成10*10时,两个因子都是10,都等于100的算术平方根(10)。
证明很简单,用反证法,假设两个因子都比m的算术平方根大,那么它们的乘积也会比m大,这与它们的乘积等于m矛盾!所以假设不成立,也就是说,至少有一个因子小于或等于m。
讲了一大堆,这根求解素数有什么关系呢?事实上,这个定理可以减少试除的次数!前面我们是用2~(i-1)的数去试除i,但其实只需要拿2~i的算术平方根的数去试除i就行了。
有朋友又纳闷了:为啥呢?很简单,如果2~i的算术平方根之间的数有一个能被i整除,很自然就说明了i是合数,不是素数,这个很好理解。
如果2~i的算术平方根之间的数都不能被i整除(命题1),那么i的算术平方根到(i-1)之间的数也不可能被i整除(命题2)。证明如下:仍然使用反证法,假设在满足命题1的前提下,命题2为假,也就是存在i的算术平方根到(i-1)之间的数能被i整除,也就是说,i能分解为两个整数的因子,那么根据定理1,就至少有一根因子介于2~i的算术平方根之间,也就是说,2~i的算术平方根之间的数一定至少存在一个数能被i整除,与命题1矛盾!所以假设不成立,也就是说,如果命题1成立,命题2一定成立!所以就不用把2~(i-1)之间的数全部试除一遍了,只需要拿2~i的算术平方根之间的数去试除就行了,大大节省了工作量。
比方说,本来要判断101是不是素数,需要拿2~100之间的数去试除,但是现在只需要拿2~101的算术平方根(10点几)之间的数去试除,也就是拿2到10之间的数去试除,效果可想而知。而且,每判断一个数就可以节省这么多计算量,整体效率就大大提升了。
优化后的代码如下:(有一个细节,上面这些只是理论上的论证,实际上由于我们已经从源头上去掉了所有的奇数,所以试除的数从3开始即可,无需从2开始)
#include <stdio.h>
#include <math.h>// 打印100~200之间的素数,并统计个数
int main()
{int i = 0;int count = 0; // 统计个数// 产生100~200之间的整数for (i = 101; i < 200; i += 2) // 偶数肯定不是素数,所以只需产生100~200之间的奇数{int flag = 1; // 假设i是素数// 判断i是不是素数// 拿2~i-1的数去试除i,如果整除就不是素数,如果都不整除就是素数// 但是事实上只需要拿2~i的算术平方根之间的数去试除i// 产生2~i的算数平方根int j = 0;for (j = 3 /* 其实这里从3开始即可,因为源头上已经去除了所有的偶数 */ ; j <= sqrt(i); j++) // 这里的sqrt是一个库函数,用于计算平方根,头文件是<math.h>{if (i % j == 0){flag = 0; // 当i不是素数时把j改为0break;}}if (1 == flag) // 由于i没有被改成0,说明i是素数{count++;// 输出素数printf("%d ", i);}}// 输出个数printf("\ncount = %d\n", count);return 0;
}
但是这段代码仍然有很大的优化空间。比方说,判断101是不是素数,真的需要拿2,3,4,5,6,7,8,9,10都去试除吗?显然无需这么麻烦。比如说4,6,8什么的就没必要了,也就是说,只需拿奇数去试除,偶数都不需要。继续修改代码
#include <stdio.h>
#include <math.h>// 打印100~200之间的素数,并统计个数
int main()
{int i = 0;int count = 0; // 统计个数// 产生100~200之间的整数for (i = 101; i < 200; i += 2) // 偶数肯定不是素数,所以只需产生100~200之间的奇数{int flag = 1; // 假设i是素数// 判断i是不是素数// 拿2~i-1的数去试除i,如果整除就不是素数,如果都不整除就是素数// 但是事实上只需要拿2~i的算术平方根之间的数去试除i// 产生2~i的算数平方根int j = 0;for (j = 3 /* 其实这里从3开始即可,因为源头上已经去除了所有的偶数 */ ; j <= sqrt(i); j += 2 /* 这里偶数都不需要去试除 */ ) // 这里的sqrt是一个库函数,用于计算平方根,头文件是<math.h>{if (i % j == 0){flag = 0; // 当i不是素数时把j改为0break;}}if (1 == flag) // 由于i没有被改成0,说明i是素数{count++;// 输出素数printf("%d ", i);}}// 输出个数printf("\ncount = %d\n", count);return 0;
}
但是本质上,如果把一个合数拆分成几个因子,是可以完全拆分成质数因子的,也就是说,只需要拿质数来试除就行了。但是这样的话,这段代码就要大幅度修改了。篇幅有限,再加上码字有点累了,就暂且到这里吧。
最后说一下,试除法是质数判断方法中最简单、最基础同时效率也最低的一种方法,其他方法诸如筛选法,会从本质上大幅提升效率。这些我会在后面的博客中介绍。