北京理工大學軟體學院陳朔鷹院長的苦思

來源:互聯網
上載者:User

 

前面的話( 2006 年 3 月 19 日):

三次上機過程中,我在機房中轉了好多圈,走馬觀花中由於沒有什麼人問問題所以就自己主動觀察了不少同學是如何編寫程式的,居然發現,有一些同學就是坐在機器前,看見我過來了馬上動動鍵盤,“做出”正在編程的樣子,於是我也注意以下他的螢幕,看看編寫了多少行,再過一會兒,我又轉過來了,看看同學還是做出編程的樣子,但螢幕上的程式沒有什麼變化。看見裝樣子的同學,我到先不好意思了,於是同幾個同學聊天,問問他們編寫程式的思路是什麼,幾句話之後明白了,他們對問題沒有什麼思路,也不知道該如何設計演算法。

幾次上機之後明白了,同學們拿到一個稍微複雜的問題之後,不知道該如何思考!

我當時的感覺是非常悲哀。首先是教師的悲哀,沒有教好,只給了學生“魚”,而沒有給學生“漁”。然後是學生的悲哀,已經被灌輸到“不會”、懶得獨立思考的地步。

思考的過程還真難於表現,要想講明白也確實不容易。作為老師給出結果最簡單,但要講明思考和探索的過程也同樣困難,於是我試圖通過記錄自己完成一個程式的主要過程,來反映自己思考問題的過程,是如何從已知出發設計出適當的演算法,編寫出適當的程式,通過調試。

以下就是星期六從晚上 10 點開始邊看電視、邊記錄思考過程、邊編寫程式的過程記錄。前五步用了 6 個小時,第六步(程式最佳化)是星期天上午用了 2 個小時完成的。哈哈,效率是低了一點。後來又用了 1 小時看了全部文字,修改了明顯的文字錯誤,但沒有在文字中加什麼修飾,也沒有修正思考過程中後來證明是錯誤的東西,最後加上這段文字。

寫到這裡,我想起我的一位大學老師在談到學生在大學應該學什麼的時候,曾經說過的一句話:“學會生活,學會學習,學會思考”。大學是人生最幸福的四年,讓我們不負這美好的春光。這樣我最後為下面的文字寫下如此的題目——

我思故我行

求不超過 500 位的兩個整數相加(減)

首先想到的是:一定不能使用常規的運算。要解決這個問題的關鍵是:

1 、超長整數該如何表示?

2 、在設計的資料結構上,該如何進行加減法操作?

第一步:設計資料結構和程式的基本結構

顯然要首先設計資料結構:

最直接的表示是採用數組,資料類型可以採用 int 或 char 。

l 如果採用 char 型,用數組中的一個元素儲存整數中的一位,則在中間運算過程要進行 char 類型向 int 類型的資料轉換。

l 如果採用 int ,用數組中的一個元素儲存整數中的一位,則在中間運算過程不需要進行資料類型轉換,但在進行超長整數輸入 / 輸出時需要進行整數轉換。

綜合考慮,我們決定採用 char 儲存整型資料。於是在輸入的時候可以簡單採用語句:

char string[1024];scanf(”%s”, string);數組 string 中儲存的資料格式字串“ 12345+678 ”。在確定資料結構之後,進行演算法設計。程式的基本結構如下:{ 輸入運算式;將整數 1 => 數組 a ;運算子號 => operator ;將整數 2 => 數組 b ;if ( operator==’+’ )add( a, b, c) ; // 假設運算結果放入字元數組 c 中elsesub( a, b, c) ;輸出結果數組 c ;}

這樣就形成了兩個函數 add 和 sub ,分別完成加 / 減運算。

這樣我們需要至少 4 個數組:

char string[1024];char a[512], b[512], c[512] ; // c = a op b
第二步:設計核心演算法——加法 add

在這裡,核心演算法有兩個,我們首先設計加法 add 。對於加法運算我們大家最熟悉的就是從小學就會的方法,於是我們來研究在小學學習的加法過程,然後用程式進行類比。手工進行加法的過程是:

l 列豎式,個位對齊;

l 從個位開始依次向高位按位相加,

l 如果出現進位,則進位要參與上一高的運算。

如果用程式類比這個過程,則首先要對齊兩個整數的個位。可以設計出 add 演算法一如下:

add1 ( char a[ ], char b[ ], char c[ ] ){ 對齊整數的個位;從個位開始向高位逐位進行按位相加;}

我們來考慮“對齊整數個位”的演算法。由於資料輸入進入數組 string 的時候是最高位在數組的前面(整數的最高位在數組的 a[0] ),所以應該將最低位放在數組的最前面(最低位放入 a[0] ),這個演算法實際就是一個串反向。

我們來加細前面提出的 add 演算法二如下:

add2 ( char a[ ], char b[ ], char c[ ] ){ 將數組 a 串反向; // 可以使用庫函數 strrev 進行串反向將數組 b 串反向; // 對齊整數的個位,最低位在數組下標位 0 的位置從個位開始向高位逐位進行按位相加,結果放入數組 c 中;將數組 c 串反向; // 再將數組 c 中的高位放在數組下標 0 的位置}

下面對中間的相加的過程進行逐步加細,得到演算法三:

add3 ( char a[ ], char b[ ], char c[ ] ){ 將數組 a 串反向; // 可以使用庫函數 strrev 進行串反向將數組 b 串反向; // 對齊整數的個位,最低位在數組下標位 0 的位置// 從個位開始向高位逐位進行按位相加,結果放入數組 c 中;i = 0 ;while ( 不是最高位 ) {a[i] 和 b[i] 對應位和前面的進位相加 => c[i] ;i++ ;}將數組 c 串反向; // 再將數組 c 中的高位放在數組下標 0 的位置}

由於進位的問題,則需要引進一個標誌進位的變數 carry ,初始值為 0 。演算法 3 可以進一步加細為演算法 4 。我們順手可以將其中部分改寫為程式或偽程式了。

add4 ( char a[], char b[], char c[] ){ int i, carry=0; // carry 位進位strrev(a); // 將數組 a 串反向strrev(b); // 將數組 b 串反向// 從個位開始向高位逐位進行按位相加,結果放入數組 c 中;i = 0 ;while ( 不是最高位 ) {if ( carry + a[i]+b[i] >= 10 ) {carry = 1;c[i] = a[i] + b[i] -10; // 這裡需要完成 char 向 int 的轉換}else {carry = 0;c[i] = a[i] + b[i]; // 這裡需要完成 char 向 int 的轉換}i++ ;}strrev(c); // 將數組 c 串方向}

在思考中間的 while 迴圈的時候,我又想到一個問題,那就是如果出現兩個相加的整數步等長的情況該怎麼辦?顯然加到後來就不是兩個對應位相加了。再考慮一種特殊的情況“ 9999+1 ”,從十位開始,個位產生的進位就會不斷向前再產生進位,一直到使得結果多出一位。因此,演算法 4 的中間部分只適合兩個整數等長的情況,不適合不等長的情況。因此要進一步修改,得到演算法 5 。

add5 ( char a[ ], char b[ ], char c[ ] ){ int i, carry=0;strrev(a); // 將數組 a 串反向strrev(b); // 將數組 b 串反向// 從個位開始向高位逐位進行按位相加,結果放入數組 c 中;i = 0 ;while ( 串 a 和串 b 都沒有結束 ) {if ( carry + a[i] + b[i] >= 10 ) {carry = 1;c[i] = a[i] + b[i] -10; // 這裡需要完成 char 向 int 的轉換}else {carry = 0;c[i] = a[i] + b[i]; // 這裡需要完成 char 向 int 的轉換}i++ ;}if ( 串 a 沒有結束 )處理串 a 餘下的高位部分 // 可以調一個函數 addcarry(a,c, int carry)else處理串 b 餘下的高位部分 // 可以調一個函數 addcarry(b,c, int carry)strrev(c); // 將數組 c 串反向}

對於新產生的函數 addcarry ,要處理餘下的高位部分,則還要將前面的相加得到的進位帶上,而且如果在加數最高位處理結束後,還產生了新的進位,則在結果中要增加一位新的位作為結果的最高位。因此可以得到 addcarry 的演算法。

addcarry (char a[ ], char c[ ], int carry ){ while ( 串 a 沒有結束 ) {if ( carry + a[i] >= 10 ) {carry = 1;c[i] = a[i] -10; // 這裡需要完成 char 向 int 的轉換}else { // 無進位,則可以不再處理carry = 0;c[i] = a[i];}i++;}if ( carry != 0 ) { // 處理最後一個進位c[i] = carry;c[i+1] = ‘/0’;else c[i] = ‘/0’;}

對演算法 add 進行整理,盡量採用 C 語言的語句進行描述。得到 add 函數。

add ( char a[ ], char b[ ], char c[ ] ){ int i, carry=0;strrev(a); // 將數組 a 串反向strrev(b); // 將數組 b 串反向// 從個位開始向高位逐位進行按位相加,結果放入數組 c 中;i = 0 ;while ( a[i]!=’/0’ && b[i]!=’/0’ ) { // 串 a 和串 b 都沒有結束if ( carry + a[i]-’0’ + b[i]-’0’ >= 10 ) {carry = 1;c[i] = a[i] + b[i]-’0’-10;}else {carry = 0;c[i] = a[i] + b[i]-’0’;}i++ ;}if ( a[i]!=’/0’ ) // 處理串 a 餘下的高位部分addcarry(a,c, int carry);else if ( b[i]!=’/0’ ) // 處理串 b 餘下的高位部分addcarry(b,c, int carry);strrev(c); // 將數組 c 串反向}

對演算法 addcarry 進行整理,盡量採用 C 語言的語句進行描述。得到 addcarry 函數。

addcarry (char a[], char c[], int carry ){ int i=0;while ( a[i]!=’/0’ ) {if ( carry + a[i] > ’9’ ) {carry = 1;c[i] = ’0’;}else { // 無進位,則可以不再處理carry = 0;c[i] = a[i];}i++;}if ( carry != 0 ) { // 處理最後一個進位c[i] = carry;c[i+1] = ‘/0’;else c[i] = ‘/0’;}
第三步:進行資料輸入之後的準備工作

在第一步已經完成運算式輸入工作,下面要進行的是整數的拆分和運算子的識別。這個演算法比較簡單了,只要逐個處理字元就可以了,我們可以簡單寫出如下處理常式段:

char string[1024], a[512], b[512], c[512], operator;int i;scanf(”%s”, string);i=0;while ( ‘0’<=string[i] && string[i]<=’9’ ) { // 將整數 1 => 數組 a ;a[i] = string[i];i++;}operator = string[i] ;while ( ‘0’<=string[i] && string[i]<=’9’ ) { // 將整數 2 => 數組 b ;b[i] = string[i];i++;}
第四步:設計核心演算法—— sub 函數

略。

第五步:合成整個 main 程式,完成加法運算

int main (){ char string[1024], a[512], b[512], c[512], operator;int i;scanf(”%s”, string);i=0;while ( ‘0’<=string[i] && string[i]<=’9’ ) { // 將整數 1 => 數組 a ;a[i] = string[i];i++;}operator = string[i] ;while ( ‘0’<=string[i] && string[i]<=’9’ ) { // 將整數 2 => 數組 b ;b[i] = string[i];i++;}if ( operator==’+’ )add( a, b, c); // 假設運算結果放入字元數組 c 中elsesub( a, b, c);printf ( ”%s” , c ) ;}
第五步:調試

將 sub 函數寫為:

sub (){ return;}

1. 通過編譯,保證沒有語法錯誤。初始的程式至少有 22 個錯誤。增加了變數 j 。

2. 開始運行程式,輸入: 123+12 ,程式運行結束,但顯示的結果為亂碼。這樣的結果非常正常,因為我們不是神仙,編出的程式不可能馬上一點錯誤都沒有。

3. 單步開始運行,跟蹤程式中數組 a 、 b 、 c 的變化過程。發現在設計演算法的時候沒有考慮到當兩個整數長度相等是應該如何處理最後的進位,於是打上一個小些“補丁”。得到運行正確的運行結果。

4. 初步錯誤排除後,看是系統運行測試案例。

l 12+1234 // 測試不等長情況沒有任何進位的情況

l 1234+12 // 同樣

l 1234+2341 // 測試等長無進位情況

l 1089+8011 // 測試等長中間有進位的情況

l 1+9 // 測試最簡單的有進位、結果增加 1 位情況

l 99999+1 // 測試最常見的極限情況

l 11+99999 // 同上

l 0+88

l 88888+0

說明:在這個過程中沒有使用任意的太長的資料,因為測試資料應該是事先可以預知運行結果的,如果不能預知運行結果,那面對實際的運行結果,就無法判斷出程式結果對錯,也就失去了運行測試案例的意義。

通過調試,我們可以發現程式中的 N 多錯誤,通過跟蹤程式中變數的值,可以改正錯誤,得到正確的程式如下。

#include <stdio.h>int main (){ char string[1024], a[512], b[512], c[512], operator;int i, j;scanf("%s", string);i=0;while ( '0'<=string[i] && string[i]<='9' ) {a[i] = string[i];i++;}a[i]='/0';operator = string[i];i++;j=0;while ( '0'<=string[i] && string[i]<='9' ) {b[j] = string[i];i++;j++;}b[j]='/0';if ( operator=='+' )add( a, b, c);elsesub( a, b, c);printf("%s/n", c);return 0;}add ( char a[], char b[], char c[] ){ int i, carry=0;strrev(a);strrev(b);i = 0;while ( a[i]!='/0' && b[i]!='/0' ) {if ( carry + a[i]-'0' + b[i]-'0' >= 10 ) {c[i] = carry + a[i] + b[i]-'0'-10;carry = 1;}else {c[i] = carry + a[i] + b[i]-'0';carry = 0;}i++;}if ( a[i]!='/0' )addcarry( &a[i], &c[i], carry);else if ( b[i]!='/0' )addcarry( &b[i], &c[i], carry);else if ( carry!=0 ) {c[i] = carry+'0';c[i+1]='/0';}else c[i]='/0';strrev(c);}addcarry (char a[ ], char c[ ], int carry ){ int i=0;while ( a[i]!='/0' ) {if ( carry + a[i] > '9' ) {carry = 1;c[i] = '0';}else {carry = 0;c[i] = a[i];}i++;}if ( carry != 0 ) {c[i] = carry+'0';c[i+1] = '/0';}else c[i] = '/0';}sub (){ return;}
第六步:最佳化程式

顯然這個程式不是最佳化的程式,應該進行最佳化。

可以進行的最佳化包括:

1. 可以消去數組 a 和數組 b 。利用 string 空間就可以。

2. 在 add 過程中,可以免去串 a 和串 b 的反向過程。

3. 可以使用指標完成串操作。

4. 在處理最後進位的地方 add 過程與 addcarry 有重複的地方。

最佳化後的程式如下:

#include <stdio.h>int main (){ char string[1024], c[512], operator;int i;scanf("%s", string);i=0;for ( i=0; '0'<=string[i] && string[i]<='9'; i++ ) ;operator = string[i];string[i++]='/0';if ( operator=='+' )add( string, &string[i], c);elsesub( string, &string[i], c);printf("%s/n", c);return 0;}add ( char *a, char *b, char *c ){ int i, carry=0;char *p, *q, *r, *s ,*end;for ( p=a+strlen(a)-1,q=b+strlen(b)-1,r=c; a<=p && b<=q; p--,q--,r++ ) {*r = carry + *p + *q - '0';if ( *r > '9' ) {*r = *r - 10;carry = 1;}else carry = 0;}s = (a>p) ? q : p;end = (a>p) ? b : a;for ( ; s >= end; s--, r++ ) {*r = *s + carry;if ( *r > '9' ) {carry = 1;*r = '0';}else carry = 0;}if ( carry != 0 )*r++ = carry+'0';*r = '/0';strrev(c);}sub (){ return;}

重新運行前面的測試案例,保證測試案例完全正確。

此時,這個程式有點像專業人員編寫的 C 語言程式了。

第七步:對程式的再最佳化

其實這個程式在數組空間使用上是非常浪費的,因為數組的一個元素只儲存了一個數字,效率非常低。我們完全可以採用 int 型的數組來儲存資料,而且也可以採用整型的表示方法。這樣需要重新設計新的演算法。


新的加法演算法請參見《 C 語言程式設計習題集(第二版)》中的 10.17 題和 10.18 題。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.