應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; }
在數組中的順序是: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-2 到 0 ) { // 將重復位后面的位置為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'); }