哥德巴赫猜想于1742年提出,是数论中存在已久的未解问题之一。用现代的数学语言,哥德巴赫猜想可以陈述为:任一大于2的偶数,都可以表示成两个素数之和。
//不禁想说一句,古人属实优秀!
本文将进行哥德巴赫猜想9位数以内的验证,内容包括对问题的理解,寻找问题的解决办法,初步拟出算法,算法优化,二次优化,换个角度再优化,使用位运算终极优化。
博文稍长,需要耐心读;笔者水平有限,有错误还望指正。
文章目录
一、切入问题
-
对于任意一个大于2的偶数,把它分解为两个质数之和,代码角度讲就是定义一个num,同时满足 i 是质数,(num-i)是质数,这样便找到了一个分解方式。
-
判断 i 和(num-i)是否为质数,可以通过编写一个函数实现。
-
验证范围maxNum由用户输入,我们从2开始遍历到maxNum的所有偶数。
-
特别提醒:
(1)为了测试代码效率,我们引入了标准库中的time.h,用来计算程序运行时间。
#include <time.h>
(2)为了表示boolean类型,我们定义了
typedef unsigned char boolean;
写进mec.h,然后把头文件和.c源文件放到了一个文件夹下。
二、代码1.0版
#include <stdio.h>
#include <time.h>
#include "mec.h"
boolean isPrime(int num);
boolean isGuessRight(int num);
boolean Goldbach(int maxNum);
boolean Goldbach(int maxNum) {
int i;
for (i = 6; i < maxNum; i += 2) {
if (!isGuessRight(i)) {
printf("不符合哥德巴赫猜想的数:%d\n", i);
return FALSE;
}
}
return TRUE;
}
boolean isGuessRight(int num) {
int i;
for (i = 3; i <= num / 2; i += 2) {
if (isPrime(i) && isPrime(num - i)) {
return TRUE;
}
}
return FALSE;
}
//判断质数的函数
boolean isPrime(int num) {
int i;
for (i = 2; i < num && num % i; i++) {
}
return i >= num;
}
int main(int argc, char const *argv[]){
int maxNum;
boolean ok;
long startTime;
long endTime;
printf("请输入范围:");
scanf("%d", &maxNum);
startTime = clock();
ok = Goldbach(maxNum);
endTime = clock();
endTime -= startTime;
printf("耗时:%d.%03d秒\n", endTime / 1000, endTime % 1000);
if (!ok) {
printf("Oh my god! Goldbach's guess is wrong!\n");
} else {
printf("Goldbach's guess is right!\n");
}
return 0;
}
三、耗时测试
本来我们要进行9位数以内的哥德巴赫猜想验证,但是……当我进行6位以内的数验证时……就已经等了好久:
(鬼知道我在这六百秒的时间里经历了什么……啊无非就是以为我的程序出错了,想关掉终端又关不了,就瞎点,然后还尝试中断程序运行,我不知道怎么中断,就敲cls,其实没用的,最后佛了,等着它。)
所以9位数的……我不打算等了,外面花儿要谢辽。
其实我们只是为了说明问题,和后续的程序运行效率做对照,所以不在此浪费时间。
四、思路优化
分析代码1.0版,哥德巴赫猜想局部验证的程序最耗时且易做优化的部分应该是: 质数的判断。
为什么这么说呢?
其一,假设num是一个很大的质数,比如197,按照从3开始,每一个的奇数验证能否整除197,需要验证97次左右。然而事实上,根本没必要从3验证到195,只需要验证到197的算数平方根就可以了。
其二,对于每一个大于2的偶数,它一定不是质数,直接返回FALSE就可以,根本不用进入for循环判断,而for循环又是极浪费时间的。
所以我们的主要任务是优化质数判断函数。
五、代码2.0版
局部代码:
//优化后的isPrime()函数
boolean isPrime(int num) {
int i;
int range = ((int) sqrt(num)) + 1;
if (num == 2) {
return TRUE;
}
if (num % 2 == 0) {
return FALSE;
}
for (i = 3; i < range && num % i; i += 2) {
}
return i >= range;
}
六、耗时测试
先输6位以内的和上面做对照:
之前耗时622秒,现在耗时0.7秒,622 / 0.7 = 876,提升了八百多倍,真棒!
再来看看7位数的:
22秒还可以
8位数以内的我还在等……
继续等……
来了:
500多秒又有点长
但总的来说,2.0版本的改进还是有显著成效的!
不过,还能再优化吗?of course!
七、创新优化
不妨换个思维:对于质数的判断,如果能从推导是否为质数,改为直接判断是否为质数,是不是就快多了?
所谓直接判断,现有一个这样的思路:创建一个数组,数组元素的值为0或1。0表示元素所对应的数组下标是质数,1表示元素所对应的数组下标不是质数。
基于思想:以空间换取时间
如果采用上方式完成质数判断,则需要以下步骤:
1.先“标识”出所有的质数。
2.判断一个数是否为质数。
那么“标识”出所有的质数具体该怎么做呢?
1.先建一个质数池 primePool[ ],或者写成 *primrPool,再动态分派存储空间。
2.筛选质数,主要步骤:
(1).筛除2的倍数。
(2).筛除3的倍数。
(3).筛除5的倍数。(4的倍数也是2的倍数,已被筛除)
(4).筛除7的倍数。(6的倍数也是2,3的倍数,已被筛除)
(5).筛除11的倍数。(8,9,10的倍数也是2,3,2的倍数,已被筛除)
…………
其实,对于i,没必要处理2i、3i、(i-1)*i的数值,只需要直接从i^2的元素开始筛除。
基于此我们引入了“质数池”的概念,用来存放0或1,请看下列代码。
八、代码3.0版
#include <stdio.h>
#include <time.h>
#include <malloc.h>
#include "mec.h"
boolean *primePool;
boolean isPrime(int num);
boolean isGuessRight(int num);
boolean Goldbach(int maxNum);
void makePrime(int maxNum);
void makePrime(int maxNum) {
int i;
int j;
primePool = (boolean *) calloc(sizeof(boolean), maxNum);
for (i = 4; i < maxNum; i += 2) {
primePool[i] = 1;
}
for (i = 3; i*i < maxNum; i += 2) {
if (primePool[i] == 0) {
for (j = i * i; j < maxNum; j += i) {
primePool[j] = 1;
}
}
}
}
boolean Goldbach(int maxNum) {
int i;
for (i = 6; i < maxNum; i += 2) {
if (!isGuessRight(i)) {
printf("不符合哥德巴赫猜想的数:%d\n", i);
return FALSE;
}
}
return TRUE;
}
boolean isGuessRight(int num) {
int i;
for (i = 3; i <= num / 2; i += 2) {
if (isPrime(i) && isPrime(num - i)) {
return TRUE;
}
}
return FALSE;
}
boolean isPrime(int num) {
return primePool[num] == 0;
}
int main(int argc, char const *argv[])
{
int maxNum;
boolean ok;
long startTime;
long midTime;
long endTime;
printf("请输入范围:");
scanf("%d", &maxNum);
startTime = clock();
makePrime(maxNum);
midTime = clock() - startTime;
ok = Goldbach(maxNum);
endTime = clock() - startTime;
printf("筛选质数耗时:%d.%03d秒\n哥德巴赫猜想验证耗时:%d.%03d秒\n",midTime / 1000, midTime % 1000, endTime / 1000, endTime % 1000);
if (!ok) {
printf("Oh my god! Goldbach's guess is wrong!\n");
} else {
printf("Goldbach's guess is right!\n");
}
free(primePool);
return 0;
}
九、耗时测试
同样,先看6位数以内的:
8位数以内的:
是不是也很棒!
9位数的验证终于也出世啦:
十、评价3.0版
总体来说,3.0版本的操作是成功的,且是极快速的,但是存在大量内存消耗。因为,上面的方式是用一个unsigned char (即boolean)来表示0或者1的,这种方式使得内存利用率非常低,一个字节只用了1/8也太浪费辽!
所以,有了plus无敌版的问世。
十一、终极优化
之前写过位运算的巧妙使用,不妨先来看看这篇文章:位运算的“凶悍”操作
这里就不一 一再介绍了,后面的代码会直接自带位运算的buff加成。
思路:考虑到用一个位表示一个数,而一个字节有8个位,所以每一个字节从0~7标注。
申请字节数:maxNum / 8 + 1
明晰两个重要转换:
x = num / 8; <=> x = num >> 3;
y = num % 8; <=> y = num & 7;
十二、超级凶悍无敌plus版
在头文件mec.h中注入:
#define SET(n, i) ((n) | (1 << ((i) ^ 7)))
#define CLR(n, i) ((n) & ~(1 << ((i) ^ 7)))
#define GET(n, i) (((n) >> ((i) ^ 7)) & 1)
对makePrime函数做的局部改进:
//plus版-质数池
void makePrime(int maxNum) {
int i;
int j;
primePool = (boolean *) calloc(sizeof(boolean), (maxNum >> 3) + 1);
for (i = 4; i < maxNum; i += 2) {
primePool[i >> 3] = SET(primePool[i >> 3], i & 7);
}
for (i = 3; i*i < maxNum; i += 2) {
if (GET(primePool[i >> 3], i & 7) == 0) {
for (j = i * i; j < maxNum; j += i) {
primePool[j >> 3] = SET(primePool[j >> 3], j & 7);
}
}
}
}
十三、耗时测试
老规矩,先看6位数以内的:
与1.0版做个对比吧:622 / 0.3 = 2073
提速两千倍,凶不凶悍!!!
那我们最初的执念——9位数以内验证呢,来看:
时间基本与3.0版持平,但是空间利用率却比3.0版高出不少,所以还是很值得采纳的。
十四、感悟
1.写算法要兼顾时间复杂度、空间复杂度。
2.算法优化的本质是做深入的数学分析。
3.写的代码要“会说话”。
4.打开思维,大胆创新,追求卓越。
5.处理好细节。
完结,撒花!