題目:
6.某人有12品脫的啤酒一瓶,另有一個8品脫和5品脫的容器。按照如下步驟操作
(0)把12品脫酒瓶內的酒倒入8品脫的容器,倒滿為止;
(1)把8品脫的容器內的倒入5品脫的容器,倒滿為止;
(2)把5品脫容器內酒的倒入12品脫酒瓶,倒完為止;
(3)把8品脫酒瓶內的酒倒入5品脫的容器,倒完為止;
(4)把12品脫的容器內的倒入8品脫的容器,倒滿為止;
(5)把8品脫容器內酒的倒入5品脫酒瓶,倒滿為止;
(6)把5品脫容器內酒的倒入12品脫酒瓶,倒完為止;
編程類比這個過程,輸出最後各容器內剩多少酒。
這個問題不難,但對初學者來說能做對並不很容易。如何能正確、高效、有條理地完成這個題目的代碼,是本文要討論的主題。
1.解題策略
做任何事都存在策略,寫代碼也是如此。
好的策略能使代碼寫得優雅從容事半功倍,差的策略讓人手忙腳亂、事倍功半甚至無功而返。
經驗表明,拿到題目不假思索就猝然開寫,最後代碼的品質通常都不高明,是為無謀。相反,在寫代碼之前深思熟慮,謀後而動是優質代碼的必要前提。
本題目步驟雖多,但無外乎“倒滿”與“倒完”,正確地類比出這兩個步驟,是保證程式正確性的前提。
把大的問題拆解為若干小的問題,再逐個對小問題各個擊破,此為代碼編寫之道。
所以優秀代碼的功夫恰恰是在代碼之外。在編寫之前對問題的思考與分解,貌似無功,其實比代碼本身更重要。
2.善積小勝
把問題分解為小問題後,面臨的首要問題就是把解決小問題的代碼寫好。
很多人輕視這一點,最後的代碼即使能勉強使用,也不過是在千瘡百孔上面打了無數補丁,這樣的代碼遠遠算不上優秀的代碼。這種短視的工作方法忽視了一個最基本的常識,你不可能用一堆品質低劣的零件組裝成一輛品質超群的汽車。(據業內人士講,國產汽車就是這樣生產的)
所以,要想寫出優秀的代碼,就必須懂得“勿以善小而不為,勿以惡小而為之”這個道理,並努力加以踐行。
首先寫從A容器倒入B容器並倒滿的代碼。
3.學會抽象
程式員必須學會抽象:抽象地提出問題,並解決抽象地解決問題。缺乏這種能力只會對著12、8、5這樣的具體數值寫代碼則永遠寫不出優秀的代碼。
與解決問題相比,正確地提出問題更為重要。在實際編寫程式的過程中,程式員需要不斷地自己向自己提出問題,然後加以解決。如果不能正確地提出問題,也就不可能正確地解決問題。
從A容器倒入B容器並倒滿問題:
若,A容器的容積為V_A升,最初有啤酒C_A升;B容器的容積為V_B升,最初有啤酒C_B升。且V_A、C_A、V_B、C_B皆為整數,並滿足C_A+C_B ≥ V_B 。把A容器內的啤酒倒入B容器直至倒滿為止,問之後兩容器內的啤酒各有多少。
4.次序安排
解決抽象的問題必然需要抽象地解決問題,然而電腦只能針對具體的數值進行計算。所以下面的代碼假設了一些具體的數值並通過符號常量把這些資料引入程式使問題具體化。這些符號常量帶來的好處之一就是更方便組織測試。我們很難對代碼的正確性心中有底,然而沒有經過測試的代碼讓人心中更沒底。
下面代碼解決的具體問題是:
若,A容器的容積為12升,最初有啤酒12升;B容器的容積為8升,最初有啤酒0升。把A容器內的啤酒倒入B容器直至倒滿為止,問之後兩容器內的啤酒各有多少。
/*
若,A容器的容積為V_A升,最初有啤酒C_A升;
B容器的容積為V_B升,最初有啤酒C_B升。
且V_A、C_A、V_B、C_B皆為整數,並滿足C_A+C_B≥V_B。
把A容器內的啤酒倒入B容器直至倒滿為止,問之後兩容器內的啤酒各有多少。
*/
#include <stdio.h>
#include <stdlib.h>
#define V_A 12U
#define V_B 8U
//初始值
#define C_A 12U
#define C_B 0U
int main( void )
{
unsigned beer_A = C_A ,
beer_B = C_B ;
//把A容器內的酒倒入B容器,倒滿為止
beer_B = V_B ; //B容器被倒滿 //註:這段代碼是錯誤的
beer_A -= ( V_B - beer_B ) ; //A容器減去被倒出的
printf("%u,%u\n", beer_A , beer_B );
system("PAUSE");
return 0;
}
測試結果並不理想,這段程式的運行後的輸出為:
12,8
這是初學者常見的一個錯誤:忽視次序。實際上代碼所表達對電腦要求的不僅僅是運算,還有運算的次序,如果運算沒有遵循正確的次序,程式是不可能正確的。
上述代碼的錯誤在於先進行了“beer_B = V_B”這個賦值運算,這樣beer_B中原來儲存的值就由於儲存了新的值而“消失”了,此後再計算“( V_B - beer_B )”就不再是B容器中倒入了或A容器倒出多少啤酒。如果要正確地類比這個過程應該先進行第二個賦值運算。在編程時應該注意,變數在不同的時刻儲存的值的意義不同,初學者往往在變數的值改變之後還把變數的值誤以為是先前的值的意義。
改正之後的代碼為
/*若,A容器的容積為V_A升,最初有啤酒C_A升;B容器的容積為V_B升,最初有啤酒C_B升。且V_A、C_A、V_B、C_B皆為整數,並滿足C_A+C_B≥V_B。把A容器內的啤酒倒入B容器直至倒滿為止,問之後兩容器內的啤酒各有多少。*/#include <stdio.h>#include <stdlib.h>#define V_A 12U#define V_B 8U//初始值 #define C_A 12U#define C_B 0U int main( void ){ unsigned beer_A = C_A , beer_B = C_B ; //把A容器內的酒倒入B容器,倒滿為止 beer_A -= ( V_B - beer_B ) ; //A容器減去被倒出的 beer_B = V_B ; //B容器被倒滿 printf("%u,%u\n", beer_A , beer_B ); system("PAUSE"); return 0;}
改正後程式的運行結果為:
4,8
這個結果與預期相符。再以另外幾組資料測試,測試的一般原則應為兩個容器中啤酒的各種邊界值和中間值(12,11和8,2,0)的各種合理組合:
#define C_A 12U
#define C_B 2U
#define C_A 12U
#define C_B 8U
#define C_A 11U
#define C_B 0U
#define C_A 11U
#define C_B 2U
#define C_A 11U
#define C_B 8U
測試結果都正確,這些測試可以增加編程者對代碼的自信。
從這裡可以看到,符號常量的一個巨大的好處是在測試時不用修改代碼部分,而對代碼的任何修改都可能引入新的錯誤。
5.依樣畫瓢
很容易在前面“倒滿”代碼的基礎上給出“倒完”的代碼並組織測試。
/*若,A容器的容積為V_A升,最初有啤酒C_A升;B容器的容積為V_B升,最初有啤酒C_B升。且V_A、C_A、V_B、C_B皆為整數,並滿足C_A+C_B≤V_B。把A容器內的啤酒倒入B容器直至倒 完 為止,問之後兩容器內的啤酒各有多少。*/#include <stdio.h>#include <stdlib.h>#define V_A 8U#define V_B 12U//初始值 #define C_A 8U#define C_B 0U//#define C_A 8U //用於測試//#define C_B 2U//#define C_A 6U//#define C_B 0U//#define C_A 6U//#define C_B 2U//#define C_A 0U//#define C_B 12U//#define C_A 0U//#define C_B 8U//#define C_A 0U//#define C_B 0U int main( void ){ unsigned beer_A = C_A , beer_B = C_B ; //把A容器內的酒倒入B容器,倒完為止 beer_B += beer_A ; //B容器加上A容器的 beer_A = 0 ; //A容器被倒空 printf("%u,%u\n", beer_A , beer_B ); system("PAUSE"); return 0;}
6.完成程式
完成了前面的準備,就可以簡單輕鬆地解決題目提出的問題了。
首先給出代碼的架構:
#include <stdio.h>#include <stdlib.h>//容積 #define V_12 12U #define V_8 8U #define V_5 5U //初始值#define C_12 12U #define C_8 0U #define C_5 0U int main( void ){ unsigned beer_12 = C_12 , beer_8 = C_8 , beer_5 = C_5 ; //三個容器最初所盛啤酒 //把12品脫酒瓶內的酒倒入8品脫的容器,倒滿為止; //把8品脫的容器內的倒入5品脫的容器,倒滿為止; //把5品脫容器內酒的倒入12品脫酒瓶,倒完為止; //把8品脫酒瓶內的酒倒入5品脫的容器,倒完為止; //把12品脫的容器內的倒入8品脫的容器,倒滿為止; //把8品脫容器內酒的倒入5品脫酒瓶,倒滿為止; //把5品脫容器內酒的倒入12品脫酒瓶,倒完為止; printf("%u,%u,%u\n", beer_12 , beer_8 , beer_5 ); system("PAUSE"); return 0;}
由於“倒滿”與“倒完”的代碼已經完成:
beer_A -= ( V_B - beer_B ) ; //A容器減去被倒出的
beer_B = V_B ; //B容器被倒滿
和
beer_B += beer_A ; //B容器加上A容器的
beer_A = 0 ; //A容器被倒空
所以只要將這兩段代碼分別複製粘貼到代碼中需要的地方再稍加編輯即可。最後的代碼為:
#include <stdio.h>#include <stdlib.h>//容積 #define V_12 12U #define V_8 8U #define V_5 5U //初始值#define C_12 12U #define C_8 0U #define C_5 0U int main( void ){ unsigned beer_12 = C_12 , beer_8 = C_8 , beer_5 = C_5 ; //三個容器最初所盛啤酒 //把12品脫酒瓶內的酒倒入8品脫的容器,倒滿為止; beer_12 -= ( V_8 - beer_8 ) ; beer_8 = V_8 ; //把8品脫的容器內的倒入5品脫的容器,倒滿為止; beer_8 -= ( V_5 - beer_5 ) ; beer_5 = V_5 ; //把5品脫容器內酒的倒入12品脫酒瓶,倒完為止; beer_12 += beer_5 ; beer_5 = 0 ; //把8品脫酒瓶內的酒倒入5品脫的容器,倒完為止; beer_5 += beer_8 ; beer_8 = 0 ; //把12品脫的容器內的倒入8品脫的容器,倒滿為止; beer_12 -= ( V_8 - beer_8 ) ; beer_8 = V_8 ; //把8品脫容器內酒的倒入5品脫酒瓶,倒滿為止; beer_8 -= ( V_5 - beer_5 ) ; beer_5 = V_5 ; //把5品脫容器內酒的倒入12品脫酒瓶,倒完為止; beer_12 += beer_5 ; beer_5 = 0 ; printf("%u,%u,%u\n", beer_12 , beer_8 , beer_5 ); system("PAUSE"); return 0;}
運行結果為:
6,6,0