尋找100以內素數的不同境界
最近幾個月,一直受到某位前輩的指導與引領。在他的推薦和建議,俺也漸漸認識到了開個部落格來record自己在學習中的一些心得體會的重要性,於是申請開通部落格也應運而生:D。(這裡,也推薦大家去閱讀一下大佬的關於為什麼開部落格的建議:[BetterExplained]為什麼你應該(從現在開始就)寫部落格 – 劉未鵬 | Mind Hacks,這是一篇09年的文章,現在看依然很受啟發,寫部落格一舉多得,何樂而不為呢?
作為俺的第一篇部落格文章,對於主題的選取,俺沒有糾結多久。由於俺最近為了刷分選擇了復修C語言:(,咱選擇深度剖析那道經典C語言題及面試典例——求出100以內的所有素數。
一些小夥伴可能看到這個,就覺得太過簡單就划走了。但俺一直認為,能將一件簡單的事開發到極致也不失為一種樂趣,同時也在一定程度反映了你的程式碼基礎水平
★Level 1
最土的辦法——試除法,幾乎是新手才會使用。就是從2試到這個數減一未知,沒有任何技術含量,不推薦使用。
★Level 2
較之1稍強一些,就是從2試到這個數的一半,工作量少一半,但依然不推薦。
★Level 3
有些小夥伴可能稍稍思索了一下,會發現,除了2以外,所有可能的質因數都是奇數。所以,他們就先嚐試 2,然後再嘗試從 3 開始一直到 x/2 的所有奇數。工作量又少了一半,仍然不推薦。
★Level 4
這是國內一些教材上列舉出了的方法,即從2試到這個數的開平方。原理就是很基本的數學因子分解的規律,如100=10*10=4*25*1*100=2*50=....,可以觀察出,100的兩個因子,必有一個小於100的開方,一個大於等於100的開方。故我們試除的時候,只需試到這個數的開方取整即可。
#include<stdio.h> #include<math.h> int isPrime(int a){ int i=0; for (i = 2; i <= sqrt(a);i++){ if(0==a%i){ return 0; } } return 1; } int main(){ int arr[100]; for (int i = 0; i < 100;i++){ arr[i] = i + 2; if(isPrime(arr[i])) printf("%d\n", arr[i]); } return 0; }
★Level 5
在這裡,有些聰明的小夥伴可能會提出,可不可以同時利用Level 3和Level 4呢,就是從3取到這個數的開方,並只取其中的奇數,這不就是再提高一層了嗎?我的回答是可以,但還不夠:P
我們提出一個數101作作分析,可以看到,我們首先對101開方並取整,得到10,然後依次取3,5,7,9進行試除,但是仔細觀察,我們便發現9的試除是沒有必要,因為9是3的倍數,3如果無法整除,那麼9也必然無法整除,因此,我們只要嘗試小於√x的質數即可。而這些質數,恰好前面已經算出來了(是不是覺得很妙?)。
所以聰明的小夥伴,再每次求出一個質數時會將它儲存起來,再下一次的試除中再利用,這大大提高了效率。
★Level 6
接下來,俺就要介紹今天的主角,埃拉託斯特尼篩法,也稱素數篩。它的原理極其巧妙,通俗易懂,且具有普適性。
具體操作步驟是:第一步:把目前沒有被刪除的數中第一個數(最小的數)畫圈,然後把這個畫圈的數後面所有這個數的倍數刪除,這步中畫圈的數就是2,被刪除的數就是4,6,8,...... 。第二步:與第一步的操作方式類似,具體來說就是把目前沒有畫圈也沒有被刪除的數中的最小的一個數即3畫圈,再把比3大的3的倍數刪除,即刪除,9,15,...... (注意,3的倍數中有的也是2的倍數,比如6,12等等,之前已經被刪除過了。這樣一直做下去,一定可以把所有100以內的素數全部找出來。)
- 我從維基上找到的一張動圖非常形象地直觀地體現了篩法的工作原理:
剖出程式碼如下:
#define N 100 #include "stdio.h" int main(){ int i,j; int arr[N]; for(i = 0;i<N;i++) { arr[i]=i+1; } arr[0] = 0;//1不是素數,所以將下標0 的元素設定為0 //進行素數判斷 for(i = 1;i < N-1;i++){ for(j = i+1;j < N;j++){ if(arr[i] != 0 && arr[j] != 0)//如果進行到3的時候2後面一定有數被置為0了,這裡我們需要判斷一下是不是有0 if(arr[j] % arr[i] == 0){ arr[j] = 0; } } } //迴圈輸出 int cnt = 0; for(i = 0;i<N;i++) { if(arr[i] != 0) { printf("%d\n",arr[i]); cnt++; } } return 0; }
提升容器的不同境界:
stadge 1:
咱先說說最土的搞法(新手時期)——直接構造一個整型的容器。在篩的過程中把發現的合數刪除掉,最後容器中就只剩下質數了。
那麼,為什麼咱都不推薦這種方法捏??
首先,整型的容器,浪費記憶體空間。比方說,你用的是32位的C/C++或者是Java,那麼每個 int 都至少用掉4個位元組的記憶體。當 N 很大時,記憶體開銷就成問題了。
其次,當 N 很大時,頻繁地對一個大的容器進行刪除操作可能會導致頻繁的記憶體分配和釋放(具體取決於容器的實現方式);而頻繁的記憶體分配/釋放,會導致明顯的CPU佔用並可能造成記憶體碎片。
最後,太樸素沒有深度,不能裝逼。
stadge 2
為了避免境界1導致的弊端,更聰明的小夥伴們會構造一個定長的布林型容器(通常用陣列)。比方說,質數的分佈範圍是1,000,000,那麼就構造一個包含1,000,000個布林值的陣列。然後把所有元素都初始化為 true。在篩的過程中,一旦發現某個自然數是合數,就以該自然數為下標,把對應的布林值改為 false。
全部篩完之後,遍歷陣列,找到那些值為 true 的元素,把他們的下標打印出來即可。
此種境界的好處在於:其一,由於容器是定長的,運算過程中避免了頻繁的記憶體分配/釋放;其二,在某些語言中,布林型佔用的空間比整型要小。比如C++的 bool 僅用1位元組。
stadge 3
雖然境界2解決了境界1的弊端,但還是有很大的優化空間。有些程式猿會想出按位(bit)儲存的思路。這其實是在境界2的基礎上,優化了空間效能。俺覺得:C/C++出身的或者是玩過組合語言的,比較容易往這方面想。
以C++為例。一個bool佔用1位元組記憶體。而1個位元組有8個位元,每個位元可以表示0或1。所以,當你使用按位儲存的方式,一個位元組可以拿來當8個布林型使用。所以,達到此境界的程式猿,會構造一個定長的byte陣列,陣列的每個byte儲存8個布林值。空間效能相比境界2,提高8倍(對於C++而言)。如果某種語言使用4位元組表示布林型,那麼境界3比境界2,空間利用率提高32倍。
小結
童鞋們,看到這裡,可能會在心中長舒一口氣:終於結束了!
但是!俺想告訴大家的是,永遠不要把侷限自己的眼光,也不要束縛住自己滴思想。要知道,山外有山、天外有天。每一個技術領域裡面的每一個細小的分支,深究下去都有很多的門道與奧妙。在你深究的過程中,必然會學到很多東西。深究的過程也就是你能力提高的過程。