用算法求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億,

我們在運算時間的性能上都是毫無壓力的!!!


總結:

在思考和編碼中,我深深的體會到了,算法優化的重要性,而要想成爲

一個優秀的程序員,那麼就必須明白,算法是程序的靈魂!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章