首先,我們談一下素數的定義,什麼是素數?除了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億,
我們在運算時間的性能上都是毫無壓力的!!!
總結:
在思考和編碼中,我深深的體會到了,算法優化的重要性,而要想成爲
一個優秀的程序員,那麼就必須明白,算法是程序的靈魂!!