問題:
問題出處見 C語言初學者代碼中的常見錯誤與瑕疵(5) 。
在該文的最后,曾提到完成的代碼還有進一步改進的余地。本文完成了這個改進。所以本文討論的並不是初學者代碼中的常見錯誤與瑕疵,而是對我自己代碼的改進和優化。標題只是為了保持系列的連續性。
改進
程序的總體思想沒有改變,所以main()函數不需要任何改動。
int main( void ) { unsigned n ; puts( "數據組數=?" ); scanf( "%u" , &n ); while ( n -- > 0 ) { int x ; puts( "整數X=?" ); scanf( "%d", & x ); printf("%d\n" , get_nearest( x ) ); //求最接近x的素數 } return 0; }
進一步的改進體現在
typedef struct prime_list { unsigned prime; struct prime_list * next; } Node; int get_nearest( int x ) { int step = 0 ; //步長增量 int sign = -1; //符號 Node * head = NULL ; //素數鏈表 while ( ! be_prime( x , & head ) ) x += ( sign = - sign ) * ++ step ; my_free(head) ; return x ; }
這里增加了一個鏈表head,用於存儲素數表。這樣,在判斷素數時只用較小的素數試除就可以了,這可以使計算數量大為減少。因為與自然數相比,素數的數量很少(≈ln n / n 個)。此外,在判斷完x是否為素數之后,如果需要判斷下一個數(x += ( sign = - sign ) * ++ step ;)是否為素數,這個素數表還可以重復使用,最多再向其中添加一個素數就可以了。(注意最初素數表是空的)
判斷素數的方法很簡單,小學生都懂。
bool be_prime( int x , Node * * pp ) //根據素數表pp判斷x是否為素數 { if ( x <= 1 ) return false ; if ( x == 2 ) return true ; if ( get_remainder( x , pp ) == 0 ) // x對素數表pp中素數逐個求余有0值 return false ; return true ; }
但是由於素數表(*pp==NULL)可能是空的,因此
int get_remainder( int x , Node * * pp )//x對素數表pp中素數逐個求余 { while ( * pp == NULL || sqr_less( (*pp) -> prime , x ) )//表中素數個數不足 add_1_prime ( pp ) ; //表中增加一個素數 Node * p = * pp ; while ( p != NULL ) { if ( x % p -> prime == 0 ) return 0; p = p -> next ; } return !0 ; } bool sqr_less ( int n , int x ) { return n * n < x ; }
需要先向其中添加素數
add_1_prime ( pp ) ;
直到
sqr_less( (*pp) -> prime , x )
依從小到大次序最后加入的那個素數的平方不小於x為止。
對於
void add_1_prime( Node * * pp ) { if ( * pp == NULL ) { add ( 2 , pp ); //第一個素數 return ; } int next_p = ( * pp )->prime + 1 ; //從最后一個素數之后開始找下一個素數 while ( !be_prime( next_p , pp ) ) next_p ++ ; add( next_p , pp ); //將下一個素數加入素數表 }
來說,加入第一個素數——2很容易,但是尋找素數表中最大素數后的下一個素數時,卻需要判斷一個整數是否是素數
be_prime( next_p , pp )
這樣,就會發現,這個過程最初是由判斷某個數x是否是素數開始,
be_prime( x , & head )
在判斷過程中需要建立素數表,
add_1_prime ( pp ) ;
而建立素數表,又需要判斷某個數是否是素數
be_prime( next_p , pp )
這樣就形成了一個極其復雜的間接遞歸調用。更為復雜的是,在調用的過程中,素數表本身即不斷地被使用,而自身也處於不斷的變化狀態之中,即不斷地被添加進新的素數,與復雜的間接遞歸一道,構成了比復雜更復雜的復雜的代碼結構與復雜的數據結構的復雜的結合體。有興趣的話可以自己算一下圈復雜度,如此復雜的情況通常並不容易遇到。
這種局面完全是由於精打細算造成的,由於對速度的斤斤計較,從而形成了一幅小貓在拼命咬自己尾巴同時小貓自己又在不斷變化的復雜無比的動態畫面。由此我們不難理解,為什么有人說,“不成熟的優化是萬惡之源”(Premature optimization is the root of all evil!- Donald Knuth)。因為優化往往意味着引人復雜。復雜也是一種成本,而且是一種很昂貴的成本。
就這個題目而言這種成本應該算是值得,因為對於求一個較大的最接近的素數問題而言(例如對於109這個量級),兩套代碼的速度有天壤之別。
增強可讀性?
如果把建立素數表的要求寫在get_nearest()函數中,可能會使代碼可讀性變得更好些。
int get_nearest( int x ) { int step = 0 ; //步長增量 int sign = -1; //符號 Node * head = NULL ; //素數鏈表 while ( 建立最大素數平方不小於x的素數表() , ! be_prime( x , & head ) ) x += ( sign = - sign ) * ++ step ; my_free(head) ; return x ; }
但這里的這個這個“,”是免不掉的,且圈復雜度不變。
至於這種寫法是否真的改善了可讀性,恐怕是見仁見智。
進一步提高效率
沒什么更好的辦法,只能用點“賴皮”手段,即充分運用已有的素數知識,幫計算機算出一部分素數。
void add_1_prime( Node * * pp ) { if ( * pp == NULL ) { add ( 2 , pp ); //第一個素數 return ; } switch ( ( * pp ) -> prime ) { case 2: add ( 3 , pp ); return ; case 3: add ( 5 , pp ); return ; /* 這里可以依樣寫多個case,只要是按照素數從小到大的次序*/ default: { int next_p = ( * pp )->prime + 1 ; //從最后一個素數之后開始找下一個素數 while ( !be_prime( next_p , pp ) ) next_p ++ ; add( next_p , pp ); //將下一個素數加入素數表 return ; } } }
這里switch語句的結構非常有趣。
重構
/*
問題:
素數
在世博園某信息通信館中,游客可利用手機等終端參與互動小游戲,與虛擬人物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語言初學者代碼中的常見錯誤與瑕疵”系列博文
版本:V 2.1
*/
#include <stdio.h> #include <stdbool.h> typedef struct prime_list { unsigned prime; struct prime_list * next; } Node; int get_nearest( int ); bool be_prime( int , Node * * ); int get_remainder( int , Node * * ) ; void add_1_prime( Node * * ); bool sqr_less ( int , int ); void add ( int , Node * * ); void my_malloc( Node * * ); void my_free( Node * ); int main( void ) { unsigned n ; puts( "數據組數=?" ); scanf( "%u" , &n ); while ( n -- > 0 ) { int x ; puts( "整數X=?" ); scanf( "%d", & x ); printf("%d\n" , get_nearest( x ) ); //求最接近x的素數 } return 0; } int get_nearest( int x ) { int step = 0 ; //步長增量 int sign = -1; //符號 Node * head = NULL ; //素數鏈表 while ( ! be_prime( x , & head ) ) x += ( sign = - sign ) * ++ step ; my_free(head) ; return x ; } bool be_prime( int x , Node * * pp ) //根據素數表pp判斷x是否為素數 { if ( x <= 1 ) return false ; if ( x == 2 ) return true ; if ( get_remainder( x , pp ) == 0 ) // x對素數表pp中素數逐個求余有0值 return false ; return true ; } int get_remainder( int x , Node * * pp )//x對素數表pp中素數逐個求余 { while ( * pp == NULL || sqr_less( (*pp) -> prime , x ) )//表中素數個數不足 add_1_prime ( pp ) ; //表中增加一個素數 Node * p = * pp ; while ( p != NULL ) { if ( x % p -> prime == 0 ) return 0; p = p -> next ; } return !0 ; } bool sqr_less ( int n , int x ) { return n * n < x ; } //“偷奸耍滑”的add_1_prime() void add_1_prime( Node * * pp ) { if ( * pp == NULL ) { add ( 2 , pp ); //第一個素數 return ; } switch ( ( * pp ) -> prime ) { case 2: add ( 3 , pp ); return ; case 3: add ( 5 , pp ); return ; /* 這里可以依樣寫多個case,只要是按照素數從小到大的次序*/ default: { int next_p = ( * pp )->prime + 1 ; //從最后一個素數之后開始找下一個素數 while ( !be_prime( next_p , pp ) ) next_p ++ ; add( next_p , pp ); //將下一個素數加入素數表 return ; } } } //老老實實的add_1_prime() //void add_1_prime( Node * * pp ) //{ // if ( * pp == NULL ) // { // add ( 2 , pp ); //第一個素數 // return ; // } // // int next_p = ( * pp )->prime + 1 ; //從最后一個素數之后開始找下一個素數 // // while ( !be_prime( next_p , pp ) ) // next_p ++ ; // // add( next_p , pp ); //將下一個素數加入素數表 //} void add ( int prime , Node * * pp ) { Node * temp ; my_malloc( & temp ); temp -> prime = prime ; temp -> next = * pp ; * pp = temp ; } void my_malloc( Node * * p_p ) { if ( ( * p_p = malloc( sizeof (* * p_p) ) ) == NULL ) exit(1); } void my_free( Node * p ) { Node * temp ; while ( ( temp = p ) != NULL ) { p = p->next; free( temp ); } }
相關博客:
偶然發現Jingle Guo網友后來研究同一問題的一篇博文,我感覺對閱讀此文的網友可能有一定的參考價值,故在此給出相關鏈接:從關於素數的算法題來學習如何提高代碼效率。