問題:
素數
在世博園某信息通信館中,游客可利用手機等終端參與互動小游戲,與虛擬人物Kr. Kong 進行猜數比賽。
當屏幕出現一個整數X時,若你能比Kr. Kong更快的發出最接近它的素數答案,你將會獲得一個意想不到的禮物。
例如:當屏幕出現22時,你的回答應是23;當屏幕出現8時,你的回答應是7;
若X本身是素數,則回答X;若最接近X的素數有兩個時,則回答大於它的素數。
輸入:第一行:N 要競猜的整數個數
接下來有N行,每行有一個正整數X
輸出:輸出有N行,每行是對應X的最接近它的素數
樣例:輸入
4
22
5
18
8
輸出
23
5
19
7
原代碼:
1 #include <stdio.h>
2 int prime(int x) //判斷是否為素數
3 { 4 int i; 5
6 if (x==1) 7 return 0; 8
9 for(i=2;i<x;i++) 10 { 11 if(!(x%i)) 12 return 0; 13 } 14
15 return 1; 16 } 17
18 int main() 19 { 20 int i,j,n,a[6],temp1,temp2; 21 int prime(int x); 22
23 //printf("n=");
24 scanf("%d",&n); 25
26 for (i=0;i<n;i++) //輸入相應數值
27 { 28 scanf("%d",&a[i]); 29 } 30
31 for(i=0;i<n;i++) 32 { 33
34 temp1=(temp2=a[i]); 35 for(j=a[i];;j++) //求出大於等於數值的素數
36 { 37 if(prime(j)) 38 { 39 temp1=j; 40 break; 41 } 42 } 43
44 for(j=a[i]-1;;j--) //求出小於數值的素數
45 { 46 if(prime(j)) 47 { 48 temp2=j; 49 break; 50 } 51 } 52
53 printf("%d\n",temp1-a[i]<=a[i]-temp2?temp1:temp2); 54 } 55
56 return 0; 57 }
評析:
int i,j,n,a[6],temp1,temp2; int prime(int x);
老問題,變量太多,數據結構設計不合理,函數類型定義位置不當。實際上這種問題有一個以不變應萬變的“標准”句型:
#include <stdio.h> int main( void ) { int n ; scanf("%d" , &n); while ( n -- > 0 ) { //定義解決問題需要的變量 //輸入測試數據 //解決問題 } return 0; }
其中,裸體的“scanf("%d" , &n);”只在這種特定情況(刷題)下可以接受,對於一般情況,在輸入前給用戶一個提示信息為好。
for (i=0;i<n;i++) //輸入相應數值 { scanf("%d",&a[i]); } for(i=0;i<n;i++) { temp1=(temp2=a[i]); for(j=a[i];;j++) //求出大於等於數值的素數 { if(prime(j)) { temp1=j; break; } } for(j=a[i]-1;;j--) //求出小於數值的素數 { if(prime(j)) { temp2=j; break; } } printf("%d\n",temp1-a[i]<=a[i]-temp2?temp1:temp2); }
顯然應該合並為一個for語句。原作者大概誤以為必須全部輸入之后才能開始解決問題,估計是題目對輸入輸出樣式的說明容易讓人誤解,實際上輸入與輸出是相互獨立的。不必拘泥於先全部輸入后再開始逐個解決問題。
for(j=a[i];;j++) //求出大於等於數值的素數 { if(prime(j)) { temp1=j; break; } } for(j=a[i]-1;;j--) //求出小於數值的素數 { if(prime(j)) { temp2=j; break; } }
這里存在幾方面的問題,下面逐個評述。
首先,拋開具體構思,僅僅從代碼形式上來說這中寫法就是不能容忍的。
代碼寫得一定要“拽”(DRY)(參見代碼寫得要"拽"(DRY)——《C解毒》試讀)。
而這里的兩條for語句,本質上是一模一樣的。可以說,一個是對另一個的復制粘貼(如果寫的時候連復制粘貼都沒用到,而是老老實實一個字一個寫成的,那就更成問題了。那說明連計算機都不會用)。
所以無論如何這兩條語句都應該想到用函數實現:
temp1 = find_prime(a[i],1); //求從a[i]開始的第一個素數(步長為1); temp2 = find_prime(a[i]-1,-1);//求從a[i]-1開始的第一個素數(步長為-1);
這樣寫代碼要漂亮多了。
再說一下邏輯上的缺陷。
“大於等於”的素數求出之后,如果是“等於”,那么問題已經得解,就沒必要執行第二條循環語句了,所以應該跳過第二條循環語句。
for( i = 0 ; i < n ; printf("%d\n",solve) , i++ ) { for(j=a[i];;j++) //求出大於等於數值的素數 { //…… } if ( temp1 == a[i] ) { //輸出該素數(temp1或a[i]) continue ; //轉到計算下一個a[i] } for(j=a[i]-1;;j--) //求出小於數值的素數 { //…… } printf("%d\n",temp1-a[i]<=a[i]-temp2?temp1:temp2); }
如果用函數求下一個素數,代碼還可以寫得更漂亮一些:
for(i=0;i<n;i++) { int temp1 , temp2 ; if ( (temp1 = find_prime(a[i],1)) == a[i] ) { printf("%d\n",temp1);//輸出temp1 continue ; } temp2 = find_prime(a[i]-1,-1) printf("%d\n",temp1-a[i]<=a[i]-temp2?temp1:temp2); }
這顯然要比原來的代碼干凈整潔多了。如果覺得兩條printf()調用不好,還可以這樣寫(又是逗號表達式,呵呵):
for( i = 0 ; i < n ; printf("%d\n",solve) , i++ ) { int temp1 , temp2 , solve ; if ( (temp1 = find_prime(a[i],1)) == a[i] ) { solve = temp1 ; continue ; } temp2 = find_prime(a[i]-1,-1) solve = temp1-a[i]<=a[i]-temp2?temp1:temp2 ; }
此外要說一下的是,無論是在main()中還是在prime()函數中,作者都沒有認真考慮當輸入整數並不處於兩個素數之間的情況。事實上這是有可能的。比如輸入為1,那么找小於1的素數是不可能的。但在原代碼中,明顯能看出作者根本就沒意識到這件事(main()中沒有相關的處理,prime()函數沒有對0和負整數進行判斷)。這應該說是原代碼中的一個BUG。
最后再說一下總體思路方面的問題。
作者的思路是先“求出大於等於數值的素數”,再“求出小於數值的素數”。這個思路可行,但不夠好。比較好的思考應該是:
- 看這個數(假定是X)是不是素數
- 如不是,判斷X+1是不是素數
- 如不是,判斷X-1是不是素數
- 如不是,判斷X+2是不是素數
- 如不是,判斷X-2是不是素數
- ……
這種想法更自然,一旦找到素數就完活兒,而不會做無用功。
而原作者的代碼則存在這樣的可能:先求出一個比X大很多的素數,但其實解是X-1;或者求出大於X的素數是X+1,又去找出一個比X小很多的素數。這兩種情況下,計算機總要做一些很無聊且無意義的工作。
當然,原代碼還可以繼續改進,但在錯誤的總體思路指導下,改進的空間極其狹仄。即使改了,也無法徹底根除指令雍余的問題。
而按照下面次序尋找最近素數
X X+1 X-1 X+2 X-2 X+3……
則不存在類似問題。
不難發現,這個序列相鄰兩項差的絕對值,恰好構成自然數列:1 2 3 4 5 ……
據此,重構如下:
重構:
/* 問題: 素數 在世博園某信息通信館中,游客可利用手機等終端參與互動小游戲,與虛擬人物Kr. Kong 進行猜數比賽。 當屏幕出現一個整數X時,若你能比Kr. Kong更快的發出最接近它的素數答案,你將會獲得一個意想不到的禮物。 例如:當屏幕出現22時,你的回答應是23;當屏幕出現8時,你的回答應是7; 若X本身是素數,則回答X;若最接近X的素數有兩個時,則回答大於它的素數。 輸入:第一行:N 要競猜的整數個數 接下來有N行,每行有一個正整數X 輸出:輸出有N行,每行是對應X的最接近它的素數 樣例:輸入 4 22 5 18 8 輸出 23 5 19 7 作者:薛非 出處:http://www.cnblogs.com/pmer/ “C語言初學者代碼中的常見錯誤與瑕疵”系列博文 */ #include <stdio.h> #include <stdbool.h> int get_nearest( int); bool be_prime( int ); int main( void ) { unsigned n ; puts( "數據組數=?" ); scanf("%u" , &n); //這里沒寫輸入提示
while ( n -- > 0u ) { int x ; scanf("%d" , & x); printf("%d\n" , get_nearest( x ) ); } return 0; } int get_nearest( int x ) { int n , s ;//步長增量,符號
for ( n = 0 , s = -1 ; ! be_prime( x ) ; n ++ , s = -s , x += s * n ) { } return x ; } bool be_prime( int x ) { int fac ; if ( x <= 1 ) return false ; for ( fac = 2 ; fac * fac <= x ; fac ++ ) if ( x % fac == 0 ) return false ; return true ; }
不足:
重構的代碼中,be_prime()函數,僅僅從功能角度來說並沒有什么問題。但若放在問題的背景下,它的效率太低了。這個函數需要反復地對每一個x都進行for ( fac = 2u ; fac * fac <= x ; fac ++ )這樣的循環判斷,如果問題要求判斷的只有一個整數,這種寫法也許無可厚非。但問題要求判斷的是X X+1 X-1 X+2 X-2 X+3……這樣一個序列,這樣的寫法就非常笨拙了。但是若想進一步改進卻也不那么容易。這次就不改了,我將在后續的博文中給出改進的寫法。