首先,我们谈一下素数的定义,什么是素数?除了1和它本身外,不能被其他自然数整除(除0以外)的数
称之为素数(质数);否则称为合数。
根据素数的定义,在解决这个问题上,一开始我想到的方法是从3到N之间每个奇数进行遍历,然后再按照素数的定义去逐个除以3到
根号N之间的奇数,就可以计算素数的个数了。
于是便编写了下面的代码:
(代码是用C++编写的)
#include<iostream>
#include <time.h>
using namespace std;
#define 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才是最佳
首选。
总结:
在思考和编码中,我深深的体会到了,算法优化的重要性,而要想成为
一个优秀的程序员,那么就必须明白,算法是程序的灵魂!!
原文地址:http://blog.csdn.net/beyond_ray/article/details/39401641