對Alexia(minmin)網友代碼的評論及對“求比指定數大且最小的‘不重復數’問題”代碼的改進


  應Alexia(minmin)網友之邀,到她的博客上看了一下她的關於“求比指定數大且最小的‘不重復數’問題”的代碼(百度2014研發類校園招聘筆試題解答),並在評論中粗略地發表了點意見。

  由於感覺有些看法在評論中無法詳細表達,也由於為了更詳細地說明一下我的 算法:求比指定數大且最小的“不重復數”問題的高效實現 博文中沒有說清楚的一些想法,並給出這個問題更加完美的代碼,故制此文。歡迎Alexia(minmin)網友和其他網友指正。

  Alexia(minmin)網友在其博文中對其算法思想描述得很清楚:

1. 給定N是一個正整數,求比N大的最小“不重復數”,這里的不重復是指沒有兩個相等的相鄰位,如1102中的11是相等的兩個相鄰位故不是不重復數,而12301是不重復數。

算法思想:當然最直接的方法是采用暴力法,從N+1開始逐步加1判斷是否是不重復數,是就退出循環輸出,這種方法一般是不可取的,例如N=11000000,你要一個個的加1要加到12010101,一共循環百萬次,每次都要重復判斷是否是不重復數,效率極其低下,因此是不可取的。這里我采用的方法是:從N+1的最高位往右開始判斷與其次高位是否相等,如果發現相等的(即為重復數)則將次高位加1,注意這里可能進位,如8921—>9021,后面的直接置為010101...形式,如1121—>1201,此時便完成“不重復數”的初步構造,但此時的“不重復數”不一定是真正的不重復的數,因為可能進位后的次高位變為0或進位后變成00,如9921—>10001,此時需要再次循環判斷重新構造直至滿足條件即可,這種方法循環的次數比較少,可以接受。

  下面是Alexia(minmin)網友的代碼:

// 求比指定數大且最小的“不重復數”

#include <stdio.h>

void minNotRep(int n)
{
    // 需要多次判斷
    while(1)
    {
        int a[20], len = 0, i, b = 0;
        // flag為true表示是“重復數”,為false表示表示是“不重復數”
        bool flag = false;

        // 將n的各位上數字存到數組a中
        while(n)
        {
            a[len++] = n % 10;
            n = n / 10;
        }

        // 從高位開始遍歷是否有重復位
        for(i = len - 1; i > 0; i--)
        {
            // 有重復位則次高位加1(最高位有可能進位但這里不需要額外處理)
            if(a[i] == a[i - 1] && !flag)
            {
                a[i - 1]++;
                flag = true;
            }
            else if(flag)
            {
                // 將重復位后面的位置為0101...形式
                a[i - 1] = b;
                b = (b == 0) ? 1 : 0;
            }
        }

        // 重組各位數字為n,如果是“不重復數”則輸出退出否則繼續判斷
        for(i = len - 1; i >= 0; i--)
        {
            n = n * 10 + a[i];
        }

        if(!flag)
        {
            printf("%d\n", n);
            break;
        }
    }
}

int main()
{
    int N;

    while(scanf("%d", &N))
    {
        minNotRep(N + 1);
    }

    return 0;
}

  我對這段代碼的總體看法是,main()寫的很好,因為很短,很容易看懂。

  主要缺點是,main寫在了源程序的后面,我個人認為這種風格欠佳——頭重腳輕(參見《品悟C》p262,“貪小便宜——省略函數類型聲明等問題”)。

  理由是,看文章我們總是先看標題,同樣的道理讀代碼也總是先讀main()。把main()置於源代碼的后部於人於己都不利於閱讀。

  這種寫法唯一的好處是可以省寫函數類型聲明。這是初學者非常喜歡占的一個小便宜。但從長遠以及稍微大一些規模的代碼來看,這個小便宜微不足道得可以忽略不計。(我記得要么是在我以前發的博文中,要么就是在《品悟C》這本書里詳細地講過這件事。)

  這段代碼的另一個缺點是,minNotRep()太大。原因主要是minNotRep()這個函數不但完成了求不重復數,還順便輸出了這個不重復數。這很不好,函數的功能應該單一,而且函數應該越小越好。因此,minNotRep()不應該定義為

void minNotRep(int n);

的形式,更好一些的寫法應該是返回不重復數

int minNotRep(int n);

或者

void minNotRep(int *n);

直接把原來的數改為不重復數,然后在main()中再考慮輸出。

  事情要一件一件地做,指望一個函數完成所有事情,代碼顯然不夠從容,函數也必然臃腫。
  所以,從整體上來說,代碼這樣安排為好 

/*
諸函數類型聲明:
輸入N();
求最小不重復數();
輸出();
*/

int main()
{
    int N;

    //輸入N();
    //求最小不重復數();
    //輸出();

    return 0;
}

/*
諸函數定義:
輸入N()
{
}
求最小不重復數();
{
}
輸出();
{
}
*/

   再來看minNotRep()函數。

  在這個函數中,首先把n離散,然后將離散后的數字用一數組(int a[20])和數字的位數len表示。這個結構沒有問題,很適合從高位到低位找重復數字要求(但是如果是我,則一定會把這兩者構造成一個統一的數據結構。為什么?不解釋。因為這是常識,)。問題在於這兩個變量被放在了while(1)循環的內部,由於它們是局部auto變量,因而意味着每次循環都要重新建立這個數組和len變量。這顯然是不妥的。這兩個變量應該放在while(1)循環的外部,這樣每次循環就不必重新建立這兩個變量了。

  與此類似,n的分解離散及合成寫在了while(1)循環之內,這就意味着每次循環都必須重新分解再重新合成,這是無意義的多余動作。經與作者溝通交流發現,作者是因為沒有很好地處理進位問題才不得不這樣處理的。以19901212為例

  首先,分解為 1、9、9、0、1、2、1、2存入數組,

        while(n)
        {
            a[len++] = n % 10;
            n = n / 10;
        }
View Code

   在數組中的順序是:2 1 2 1 0 9 9 1
  然后從高位到低位找重復數字
  找到之后如果flag為false則加1

        // 從高位開始遍歷是否有重復位
        for(i = len - 1; i > 0; i--)
        {
            // 有重復位則次高位加1(最高位有可能進位但這里不需要額外處理)
            if(a[i] == a[i - 1] && !flag)
            {
                a[i - 1]++;
                flag = true;
            }
            else if(flag)
            {
                // 將重復位后面的位置為0101...形式
                a[i - 1] = b;
                b = (b == 0) ? 1 : 0;
            }
        }

   我不得不說,我很不喜歡這個flag,因為除了表現出一種別扭的思維,這里它沒有別的用處。(參見flag標志什么?哦,它標志代碼餿了 )這段代碼完全可以這樣寫:

        for(i = len - 1; i > 0; i--)
        {
            if(a[i] == a[i - 1] )
            {
                a[i - 1]++;
                break ;
            }
        }
        for ( 從 i-20 )
        {
            // 將重復位后面的位置為0101...形式
         }

無論從邏輯上還是形式上都更為簡潔。

  關於這個flag要說的另一件事情是,它是bool類型。這種類型C語言中是沒有的(C99中有_Bool類型),作者恐怕是把C語言和C++混為一談了。國內很多大學生都犯這個毛病,甚至專業程序員中也有很多人C和C++不分。這個錯誤很廣泛,無疑首先是教材或書籍的責任。(參見《品悟C》p4,“C啊,多少C++假汝之名而行——C和C++不分”)

  當然支持C99的編譯器可以這樣用,但前提是必須

#include <stdbool.h>

才行。可是在代碼中我沒有發現這條預處理命令。因此bool是誤用無疑。

  回到被打斷的話題,加1之后,數組中變成了

2 1 2 1 0 10 9 1

  由於作者沒有及時處理這個10,所以才不得不在循環體內不斷地分解與合成。其實這時只要對數組稍微處理一下,模擬一下進位,將數組改為

2 1 2 1 0 0 0 2

  就用不着反復地分解、合成了。

  緊接着,代碼將0 0左側的數組元素改寫成了“0101...形式”:

0 1 0 1 0 0 0 2

  這里的代碼有兩個問題。的問題是

  第一,

a[i - 1] = b;

  這句我認為是一個BUG。因為前面說的是a[i]與a[i-1]重復(並且有a[i - 1]++;),所以“// 將重復位后面的位置為0101...形式”應該是從a[i-2]而不是a[i - 1]開始改。但  a[i-2]也不對,因為所在循環for(i = len - 1; i > 0; i--)中的 i 最小可以為1,所以a[i-2]存在數組越界的問題。

  第二問題是,由於加1之后重復位前面可能又出現了新的重復位,所以這里的“將重復位后面的位置為0101...形式”幾乎是一個無意義的操作。這個動作僅僅是在最后一次才有意義,這就是我不肯接受這種寫法的原因。寫代碼其實和下圍棋一樣,任何一個高手下圍棋絕對不肯走一步顯而易見沒有用處的“廢棋”。反對直接填寫“0101...”的另一個原因是,這是人“算”的,不是程序“算”的。程序員的任務是用程序發出命令讓計算機去做,而不是越俎代庖地替代程序和計算機。

  我在這里的寫法是將重復位后面各個位置上的數字改為0。而且為了不至於反復地進行無意義地重復寫0,使用了一點小技巧。這個小技巧,就評論情況來看,目前還沒有人看懂。

  好,評論就到這里。下面講一下我在這里的處理。依然是以以19901212為例,在數組中的順序是:2 1 2 1 0 9 9 1。

  我首先用 end = 0 這個變量規定了重復位后面改為0的最后一位。

  用b_point = search ( &map )確定最前面的重復位,在這個例子里應該是5 (199) 。然后將199加1,並在數組中模擬了進位(  add_1( &map , b_point ) ; ) ,

   for ( i = from ; i < p_m->top ; i ++ )        //進位處理  
   { 
      p_m->t[i + 1] += p_m->t[i] / 10u ; 
      p_m->t[i] %= 10u ; 
   } 
     
   if ( p_m->t[p_m->top] > 9u )                  //最高位有進位  
   { 
      p_m->t[p_m->top + 1] = p_m->t[p_m->top] / 10u ; 
      p_m->t[p_m->top ++ ] %= 10u ; 
   } 

(順便說一句,這里的p_m->t[p_m->top ++ ] %= 10u ;一句一直是讓我感到有些惴惴不安的,生怕“求道於盲”那樣精通C語言的網友提出質疑。)
之后數組變為

2 1 2 1 0 0 0 2

然后將數組中從end到b_point-1的元素改為0(一共5個),數組變為

0 0 0 0 0 0 0 2

  最后再將b_point的值賦給end,由於每次循環修改的是從b_point-1到end之間的元素,這樣下次就不會再修改數組最左面那5個元素的值了。

我的失誤:

  我的失算之處是,最初也被題目中的“給定任意一個正整數”中的“正整數”三個字給迷惑了。直到寫完代碼我才意識到,這個題目跟正整數幾乎沒什么關系。把輸入視為一個十進制形式正整數的字符序列,不但完全滿足原來問題的要求,而且不限於整數類型的范圍限制。為此,重新給出可處理最多100位正整數的代碼如下:

#include <stdio.h>

#define MAX 100 
typedef  struct
         {
            unsigned char t[ MAX + 1 ] ; 
            int top ;                //記錄第一位數的下標 
         } 
         Map ;

void input( Map * );
void reverse( unsigned char [] , int );
void exchange( unsigned char * , unsigned char * );
void squeeze( Map * );
void find( Map * );
int  search( const Map * );
void add_1( Map * , const int );
void clear( Map * , const int , const int );
void out( const Map * );

int main( void )
{
   Map num ;
   
   input( & num );     //輸入正整數 
   add_1( & num , 0 ); //加1 
   find ( & num );     //求不重復數 
   out  ( & num );     //輸出 
   
   return 0;
}

void squeeze( Map * p_m )
{
   while ( p_m -> t[ p_m -> top ] == 0 ) 
      p_m -> top -- ;   
}

void exchange( unsigned char * p1 , unsigned char * p2 )
{
   unsigned char c = * p1 ;
   * p1 = * p2 ;
   * p2 = c ;
}

void reverse( unsigned char a[] , int n )
{
   int i ;
   
   for ( i = 0 , n -- ; i < n ; i ++ , n -- )
      exchange( a + i , a + n );

}

void input( Map *p_m )
{
   int c ;
   
   p_m -> top = -1 ;
   
   while ( ( c = getchar () ) != '\n' )
   {
      if ( c < '0' || c > '9' || p_m -> top > MAX )
         break ;

      p_m -> top ++ ;
      p_m -> t[ p_m -> top ] = c - '0' ;      
   }
   
   reverse( p_m -> t , p_m -> top + 1 );//顛倒次序 
   
   squeeze( p_m ); //去掉開頭的0 
   
}

void clear( Map * p_m , const int from , const int to )
{
   int i ;
   
   for ( i = from - 1 ; i > to - 1; i -- )
      p_m->t[i] = 0u ;
}

void add_1( Map * p_m , const int from )
{
   int i ;
   
   p_m->t[from] ++;                             //最低位加1 
   
   for ( i = from ; i < p_m->top ; i ++ )       //進位處理 
   {
      p_m->t[i + 1] += p_m->t[i] / 10u ;
      p_m->t[i] %= 10u ;
   }
   
   if ( p_m->t[p_m->top] > 9u )                 //最高位有進位 
   {
      p_m->t[p_m->top + 1] = p_m->t[p_m->top] / 10u ;
      p_m->t[p_m->top ++ ] %= 10u ;
   }
}

int  search( const Map * p_m )
{
   int i ;
   
   for ( i = p_m->top ; i > 0  ; i-- )
   {
      if ( p_m->t[i] == p_m->t[i-1] )
         break ;
   }
   
   return i - 1 ; 
}

void find( Map * p_m )
{
   int end = 0 , b_point ;
   
   while ( ( b_point = search ( p_m ) ) > -1 ) //為-1時說明不是不重復數 
   {
      add_1( p_m , b_point );                 //重復數部分加1 
      clear( p_m , b_point , end );           //后面改為0 
      end = b_point ;                         //確定下次循環的處理范圍 
   }
}

void out( const Map * p_m )
{
   for (int i = p_m -> top ; i >= 0 ; i -- )
      printf( "%u" , p_m -> t[i] );
      
   putchar('\n');            
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM