zoukankan      html  css  js  c++  java
  • 用算法求N(N>=3)之内素数的个数


    首先。我们谈一下素数的定义。什么是素数?除了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亿,

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


    总结:

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

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


  • 相关阅读:
    TP5中的小知识
    php中Redis的扩展
    html js css压缩工具 可以实现代码压缩
    Python 基础
    操作系统简介
    计算机硬件
    Linux命令 比较文件
    Linux命令 查看及修改文件属性
    Linux命令 文件备份归档恢复
    Linux命令 文件的建立移动删除
  • 原文地址:https://www.cnblogs.com/slgkaifa/p/6867343.html
Copyright © 2011-2022 走看看