前m个素数是指从2开始的m个素数,比如前3个素数是:2,3,5;前10个素数是:2,3,5,7,11,13,17,19,23,29。
要找出前m个素数,很容易想到的算法是从2开始依次判断每一个整数是否是素数,直到找到m个素数。首先涉及到判断一个给定的数n是否为素数,其最简单的算法是,依次判断所有大于1且小于n的整数中是否存在能被n整除的数i,若存在则n不是素数,不存在则n为素数。代码实现如下:
//算法1
int isPrimeNumber(long n)
{
for(int i = 2; i < n; i++)
{
if(n % i == 0)
return 0; //n不是素数
}
return 1; //n是素数
}
该算法最简单明了,但有很大的缺点,判断次数多,冗余操作多,时间复杂度为O(n)。
实际上,算法1中n除以i得到的商小于i后,依然没有出现整除,则之后的整数已无需判断,就可以判定n为素数。因此对该算法进行一个简单的优化,将for(int i = 2; i < n; i++)
改为for(int i = 2; i <= n/i; i++)
,所得到的新算法记为算法2,其冗余的判断整除的操作减少很多,时间复杂度减少到O(√n)。
但是该算法依然存在冗余判断整除操作,例如n已经无法整除2和3,那么显然也无法整除6,但该算法依然会对6进行整除判断。若将算法中的这类冗余操作完全去除,则运算速度将会进一步加快。那么有没有一种算法能做到没有冗余呢?
我们已知在数学上判断素数的方法:试除法,用从2开始的各个质数依次去除n,如果存在某一个质数整除,那么n不是质数;若不能整除,一直尝试到商小于除数,那么n是一个质数。这个方法没有冗余判断操作。
在打印前m个素数的算法中,可以使用一个数组来记录已经找到的质数。前几个素数已经为人们所熟知,无需再进行计算,因此可以把它们提前记录在这个数组中,比如可以将前8个素数2,3,5,7,11,13,17,19预先填入数组,后面找到的其他素数依次填入其中。将该算法记为算法3。以算法3为基础,得到打印前m个素数的C代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#define MAX_SIZE 10000 //最多记录的素数个数
long primeNumberArray[MAX_SIZE] = {2, 3, 5, 7, 11, 13, 17, 19};
//初始化数组,填入前8个熟知的素数
int currentPrimeNumber = 8; //记录数组中已有的素数个数
//判定一个给定的数n是否为素数,若是返回1,不是返回0,出错返回-1
int isPrimeNumber(long n)
{
long index = 0;
for ( index = 0; index < currentPrimeNumber; index++)
{
if ( 0 == n % primeNumberArray[index] )
return 0; //n不是素数
else if ( primeNumberArray[index] > (n/primeNumberArray[index]))
return 1; //n是素数
}
return -1;
}
int main()
{
int m;
int result = 0, i = 0;
long currentNumber = 21; //要判断的数,数组记录到19,从21开始
printf("number of prime numbers to print: ");
scanf("%d", &m);
printf("\n");
while (currentPrimeNumber < m)
{
result = isPrimeNumber(currentNumber);
if (1 == result) //找到一个新的素数,记录到数组中
{
primeNumberArray[currentPrimeNumber] = currentNumber;
currentPrimeNumber++;
}
else if (-1 == result)
{
printf("Fatal error occur!\n");
}
currentNumber += 2; //大于2的偶数均为和数,因此递增2
}
//打印前m个素数
printf("Index Prime Number\n");
for (i = 0; i < m; i++)
printf("%10d %ld\n",i + 1, primeNumberArray[i]);
return 0;
}
为了验证这个无冗余算法的性能优势,我们在该程序中的while循环前后记录时间点,用以计算该程序耗时,以微秒为单位,更改后的main函数如下:
int main()
{
int m;
int result = 0, i = 0;
long currentNumber = 21;
struct timeval start, end;
struct timezone tz;
long timeelapse = 0;
printf("number of prime numbers to print: ");
scanf("%d", &m);
printf("\n");
gettimeofday(&start, &tz); //开始计时
while (currentPrimeNumber < m)
{
result = isPrimeNumber(currentNumber);
if (1 == result)
{
primeNumberArray[currentPrimeNumber] = currentNumber;
currentPrimeNumber++;
}
else if (-1 == result)
{
printf("Fatal error occur!\n");
}
currentNumber += 2;
}
gettimeofday(&end, &tz); //结束计时
printf("Index Prime Number\n");
for (i = 0; i < m; i++)
{
printf("%10d %ld\n",i + 1, primeNumberArray[i]);
}
//计算并打印耗时,单位微秒
timeelapse = (end.tv_sec - start.tv_sec)*1000*1000 + end.tv_usec - start.tv_usec;
printf("timeelaspe = %ld\n", timeelapse);
return 0;
}
在Ubuntu虚拟机上将函数isPrimeNumber(long n)
依次替换为算法1,2和3,在打印前1000个、前5000个和前10000个素数时重复运行5次,观测时间耗费(微秒)取平均值,统计得到如下表格:
打印个数 | 1000 | 5000 | 10000 |
---|---|---|---|
算法1 | 40589 | 1155341 | 4978551 |
算法2 | 1583 | 23223 | 71557 |
算法3 | 902 | 7133 | 17284 |
根据统计结果可以明显看出,算法1耗时最长,没有实用性,算法3在所有情况下的用时最少,效率最高。当打印素数的个数逐渐增加时,算法3相对于算法2的优势越来越大。但是算法3的使用也有一定局限性,其能判定的最大整数不能超过数组中最后一个素数的平方,并且其需要保存所得到的素数列表,因此比前两个算法占用更多的内存空间。