首先。我们谈一下素数的定义。什么是素数?除了1和它本身外,不能被其它自然数整除(除0以外)的数

称之为素数(质数);否则称为合数。

依据素数的定义,在解决问题上,一開始我想到的方法是从3到N之间每一个奇数进行遍历,然后再依照素数的定义去逐个除以3到

根号N之间的奇数,就能够计算素数的个数了。

于是便编写了以下的代码:

(代码是用C++编写的)

#include<iostream>
#include <time.h>
using namespace std; const int N = 1000000; int compuPrimeN(int); int main(char argc, char* argv[])
{
int iTimeS = clock();
int iNum = compuPrimeN(N);
int iTimeE = clock(); cout << iNum << endl;
cout << "算法时间:" <<iTimeE - iTimeS<<"毫秒"<< endl;
getchar();
return 0;
} int compuPrimeN(int maxNum)
{
//算法1
int iNum = 1; //起始记上2
bool bPrime = true;
for (int i = 3; i <= maxNum; i += 2)
{
bPrime = true;
for (int j = 3; j <= (int)sqrt(i); j += 2)
{
if (i%j == 0)
{
bPrime = false;
break;
}
}
if (bPrime)
iNum++;
} return iNum;
}

执行后如图所看到的:

由此可见。算法的性能不是非常好,在时间上还有非常大能够优化的空间。

那么,该怎样优化?

首先,我是想,既然去掉了2的倍数,那么能不能去掉3的倍数。但后来

发现,在第二个循环里第一个取余的就是3,那么3的倍数事实上仅仅计算了一次

就过滤,全部没有必要再往下思考。

后来我想到。在第二个循环里。3取余过了,假设没跳出循环,那么6。9之类的

应该不用继续取余,同理。5取余过了。那么10,15...就不该继续取余,由于取余

5不为0,那么取余10,15肯定也不为0.换言之。那么不该取余的事实上是合数!

why?由于假设是合数,那么比他根号本身小的数里肯定有它能取余的,也就是

之前我们想过滤掉不想取余的数,这样一来,事实上我们仅仅要在第二循环里取余

比其根号本身要小的质数就能推断出来了!而那些质数我们在求该数之前就已经

找出来了,那么我们仅仅要将其记录下来即可了!!

于是乎,遵循乎该思路,我将compuPrimeN()函数重写,写出了第2个算法:

int compuPrimeN(int maxNum)
{
//算法2
int iNum = 1; //记录素数总个数
int iRecN = 1; //记录在数组内素数的个数
bool bPrimeN = true;
int sqrtMaxN = (int)sqrt(maxNum);
//我们要记录小于sqrtMaxN内的素数,为使空间分配最优,大小为x/ln(x)*1.2,
//由于科学家发现一个求素数大致范围的近似公式x/ln(x),
//为了不数组越界,多加20%范围
//注意maxNum为3时为特例。由于此处ln(根号3)为0
int* iPrime = new int[maxNum == 3 ? 1 : (int)((float)sqrtMaxN / log(sqrtMaxN)*1.2)]; for (int i = 3; i <= maxNum; i += 2)
{
bPrimeN = true;
//仅仅要取余范围内的素数就好了
for (int j = 1; j < iRecN; j++)
{
if (i%iPrime[j] == 0)
{
bPrimeN = false;
break;
}
}
if (bPrimeN)
{
if (i <= sqrtMaxN)
{
iPrime[iRecN] = i;
iRecN++;
iNum = iRecN;
}
else
iNum++;
}
}
delete []iPrime;
return iNum;
}

执行后如图所看到的:

看,优化后算法的时间性能比原来好了19倍左右。

那能不能更快呢?

我想理论上是能够的,由于前面的算法都用到了一种思想,

事先过滤掉了2,3的倍数。假设我们能把5,7,11的倍数都

事先过滤掉那不是更快吗?

这里为什么没有9,由于9的倍数即是3的倍数啊,咦?好像

发现了什么。和算法2的思想有点类似,假设我们能事先过滤掉

质数倍数,那么不是能过滤掉非常多合数了吗。而对于该质数+1。

无非是两种情况。其一是它是被过滤掉的合数,其二是它是质数。

否则它应该在之前过滤掉的啊!!而我们仅仅要在过滤的过程中,

把遇到的不能过滤的统计起来。不就是我们所求的质数吗?

这样一来,时间性能不是能更进一步优化了吗?对,可是要事先

过滤掉这么多的合数。并将其行为记录下来,就要消耗极大的

空间了,这就是典型的空间换时间!

于是,我写的算法3便诞生了,例如以下:

int compuPrimeN(int maxNum)
{
//算法3
//用bool型大数组来记录,true为素数,false为偶数
//由于求素数个数,所曾经两个能够忽略.
bool* bArray = new bool[maxNum + 1];
for (int i = 2; i <= maxNum; i++)
bArray[i] = true; int iNum = 0;
for (int i = 2; i <= maxNum; i++)
{
//替换后面的合数为false
if (bArray[i])
{
iNum++;
for (int j = i + i; j <= maxNum; j += i)
{
bArray[j] = false;
}
}
}
delete []bArray;
return iNum;
}

执行后如图:

哇!

没想到算法的时间居然可以优化如此高速。!可是,好像耗费的空间

存储有点多,仅用bool型的数组记录似乎有点浪费,能不能在每一个bit上用0或1

来取代记录呢?

于是。我又写了以下的算法:

int compuPrimeN(int maxNum)
{
//算法4
//用每一个位0或1来分别表示合数和素数
//优点是内存空间利用最大化
int size = maxNum % 8 == 0 ? maxNum / 8 : maxNum / 8 + 1;
unsigned char* array = new unsigned char[size];
for (int i = 0; i < size; i++)
array[i] = 127; int iNum = 0, iBit = 0, index = 0;
for (int i = 2; i <= maxNum; i++)
{
index = i / 8;
(iBit = i % 8) == 0 ? iBit = 7, index-- : iBit--; if (array[index] & (1 << iBit))
{
iNum++;
for (int j = i + i; j <= maxNum; j += i)
{
index = j / 8;
(iBit = j % 8) == 0 ? iBit = 7, index-- : iBit--;
array[index] = array[index] & (~(1 << iBit));
}
}
}
delete []array;
return iNum;
}

执行结果如图:

尽管因为二进制的计算使其在时间性能上比算法3要慢上那么一点,

可是换做bit来记录素数或合数,却是让空间存储变为了原来的1/8,

其优点是不言而喻的。假设没有内存空间问题。那么用算法3也是

无可厚非的。假设对内存空间要求比較严格,那么算法2才是最佳

首选。

//--------------------------------------------------------------------------------------------------------------------

可是除了上面四种算法之外,我想到了一种近乎作弊的第5种方法。这样的方法用在比赛的

题目中,可能会引起非议。但在实际应用之中,却是一种非常值得借鉴的方法,这之中蕴含

着一种非常重要的思想。我称之为“用已知换未知”。!

其思想为:将求出的已知数据按一定格式保存起来,在以后须要的时候,仅仅要读取一次

该数据。就能求得该结果。其算法时间为O(1).

比如该问题,如果我们在实际应用的过程中仅须要用到N <= 1亿的N以内的素数个数

(N需求很多其它时可在计算机存储范围内对应添加,只是对应的预处理时间也会添加)。

那么我们能够先调用例如以下这个函数将N(N <= 1亿)以内素数个数的数据用二进制存储起来。

void savPrimeN(int maxNum)
{
ofstream ofPrimeF("PrimeNum.data", ios::binary);
int iNum = 0; //预先写入两次0,分别作为0,1以内素数的个数
for (int i = 0; i < 2;i++)
ofPrimeF.write((const char*)(&iNum), sizeof(int));
//用bool型大数组来记录,true为素数,false为偶数
//由于求素数个数,所曾经两个能够忽略.
bool* bArray = new bool[maxNum + 1];
for (int i = 2; i <= maxNum; i++)
bArray[i] = true; int sizeInt = sizeof(int);
for (int i = 2; i <= maxNum; i++)
{
//替换后面的合数为false
if (bArray[i])
{
iNum++;
for (int j = i + i; j <= maxNum; j += i)
{
bArray[j] = false;
}
}
ofPrimeF.write((char*)(&iNum), sizeInt);
}
delete []bArray;
ofPrimeF.close();
}

如今我们已经将N(N <= 1亿)以内素数的个数依照每4个字节的格式存储到二进制文件其中了,那么当我们须要

求N(N<=1亿)以内素数的个数的时候,我们仅仅要到该二进制文件里读取对应的数据就能够了。

例如以下所看到的:

int compuPrimeN(int maxNum)
{
//算法5
ifstream ifPrimeN("PrimeNum.data", ios::binary);
int iNum = 0;
ifPrimeN.seekg(maxNum*4, ios::beg);
ifPrimeN.read((char*)(&iNum), sizeof(iNum));
ifPrimeN.close(); return iNum;
}

看看如今的运算时间:

由于clock()函数计算程序启动到函数调用占用CPU的时间是精确到毫秒的,

这也就意味着我们算法的时间不超过1毫秒!!

而这一切,都是得益于我们

自己所建立的一个所谓的“数据库”,有了这个“数据库”,仅仅要保证N<=1亿,

我们在运算时间的性能上都是毫无压力的。。!

总结:

在思考和编码中。我深深的体会到了,算法优化的重要性。而要想成为

一个优秀的程序猿,那么就必须明确。算法是程序的灵魂。!

用算法求N(N&gt;=3)之内素数的个数的更多相关文章

  1. c语言经典算法—求0—7 所能组成的奇数个数

    题目:求0—7 所能组成的奇数个数. 算法思想:这个问题其实是一个排列组合的问题,设这个数为sun=a1a2a3a4a5a6a7a8,a1-a8表示这个数的某位的数值,当一个数的最后一位为奇数时,那么 ...

  2. C++迪杰斯特拉算法求最短路径

    一:算法历史 迪杰斯特拉算法是由荷兰计算机科学家狄克斯特拉于1959 年提出的,因此又叫狄克斯特拉算法.是从一个顶点到其余各顶点的最短路径算法,解决的是有向图中最短路径问题.迪杰斯特拉算法主要特点是以 ...

  3. poj 3565 uva 1411 Ants KM算法求最小权

    由于涉及到实数,一定,一定不能直接等于,一定,一定加一个误差<0.00001,坑死了…… 有两种事物,不难想到用二分图.这里涉及到一个有趣的问题,这个二分图的完美匹配的最小权值和就是答案.为啥呢 ...

  4. HDU-1233 还是畅通工程 (prim 算法求最小生成树)

    prim 算法求最小生成树 还是畅通工程 Time Limit: 4000/2000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Oth ...

  5. Dijkstra算法求单源最短路径

    Description 在每年的校赛里,所有进入决赛的同学都会获得一件很漂亮的t-shirt.但是每当我们的工作人员把上百件的衣服从商店运回到赛场的时候,却是非常累的!所以现在他们想要寻找最短的从商店 ...

  6. ZOJ Problem - 2588 Burning Bridges tarjan算法求割边

    题意:求无向图的割边. 思路:tarjan算法求割边,访问到一个点,如果这个点的low值比它的dfn值大,它就是割边,直接ans++(之所以可以直接ans++,是因为他与割点不同,每条边只访问了一遍) ...

  7. HDU 1269 迷宫城堡 tarjan算法求强连通分量

    基础模板题,应用tarjan算法求有向图的强连通分量,tarjan在此处的实现方法为:使用栈储存已经访问过的点,当访问的点离开dfs的时候,判断这个点的low值是否等于它的出生日期dfn值,如果相等, ...

  8. Kruskal和Prim算法求最小生成树

    Kruskal算法求最小生成树 测试数据: 5 6 0 1 5 0 2 3 1 2 4 2 4 2 2 3 1 1 4 1 输出: 2 3 1 1 4 1 2 4 2 0 2 3 思路:在保证不产生回 ...

  9. 克鲁斯卡尔(Kruskal)算法求最小生成树

    /* *Kruskal算法求MST */ #include <iostream> #include <cstdio> #include <cstring> #inc ...

随机推荐

  1. APM-应用性能管理

    APM(应用性能管理) 在信息科学和系统控制领域,APM致力于监控和管理应用软件性能和可用性.通过监测和诊断复杂应用程序的性能问题,来保证软件应用程序的良好运行(预期的服务),APM已经商用 基本定义 ...

  2. Django自定义User模型和登录验证

    用户表已存在(与其他App共用),不能再使用Django内置的User模型和默认的登录认证.但是还想使用Django的认证框架(真的很方便啊). 两个步骤: 1)自定义Use模型,为了区分系统的Use ...

  3. 洛谷P1236 算24点

    题目描述 几十年前全世界就流行一种数字游戏,至今仍有人乐此不疲.在中国我们把这种游戏称为“算24点”.您作为游戏者将得到4个1~9之间的自然数作为操作数,而您的任务是对这4个操作数进行适当的算术运算, ...

  4. Spring Boot的web开发&静态资源配置方式

    Web开发的自动配置类:org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration 1.1. 自动配置的ViewResolve ...

  5. 【leetcode】Find All Anagrams in a String

    [leetcode]438. Find All Anagrams in a String Given a string s and a non-empty string p, find all the ...

  6. vscode golang配置说明

    一.vscode-go插件安装 go 1.10.2 https://golang.org/dl/ 需要墙 vscode 1.23.1 https://code.visualstudio.com/ vs ...

  7. Java原来如此-反射机制

    在Java运行时环境中,对于任意一个类,能知道这个类有哪些属性和方法.对于任意一个对象,能调用它的任意一个方法.这种动态获取类的信息以及动态调用对象的方法的功能来自于Java语言的反射(Reflect ...

  8. formValidator阻止提交跳转

    formValidator这个前台校验插件非常好用,其中有几个很有特点的方法可以单独提出使用,效果非常棒这里要说的是其核心方法之一,阻止提交动作,先校验,校验成功再执行提交动作 $("#ph ...

  9. Codeforces Gym101502 A.Very Hard Question

    2017 JUST Programming Contest 3.0 昨天的训练赛,打的好难过,因为被暴打了,写了8题,他们有的写了9题,差了一道dp,博客上写7道题的题解. 因为有一道是套板子过的,并 ...

  10. Vue开发之路由进阶

    1.路由组件传参 在一个页面中,需要根据路由获得参数,然后在页面进行逻辑处理,可以通过$route来获取相关参数 但是这样一来,页面组件与路由耦合太高,为了解耦,页面组件可以在更大程度上进行复用,可以 ...