S1&2.個人項目時間估算
PSP表格如下:
|
S3.優化程序
優化所耗費的時間大致占據代碼復審和測試階段的40%,具體時間參照上表
優化點:
A. 存儲潛在的冗余表達式
在表達式重復性方面,筆者考慮采用犧牲空間換取時間的方式,使用Dictionary類管理程序生成表達式過程中產生的參考數據,具體數據模式如下:
<表達式結果,List<以此為結果的所有表達式>>
這里強調的所有有兩點含義,一是指程序輸出的表達式中,所有以鍵值為結果的表達式,而也包括所有與這些表達式重復的式子(即通過交換律可以互相轉換)。
例如:
1
↓-----映射----->
{(1+7)÷(2×4)
(1×2)-(3-2)//這兩個式子由程序生成並輸出,它們有相同的結果但並不重復
(7+1)÷ (2×4)
(7+1)÷ (4×2)
(1+7)÷ (4×2)
(2×1)- (3-2)//這四個式子與前面兩個構成了重復,它們在程序運行過程中產生但不輸出,不過由於其值為1,所以也添加到此List中
就以實現這一點為目標,程序進行了兩方面優化:
一是在生成表達式的過程中,同時生成與該表達式重復的所有式子並通過ref修飾的List<String>參數予以輸出,一旦輸出便添加到Dictionary中作為下一個表達式的參考;
另一則是以String的形式存儲該表,而不是直接存儲表達式對象,通過重寫顯示轉換方法,實現Expression類與String類的快速轉換
代碼如下:
1 public static implicit operator Expression(String expStr) 2 { 3 String result = MathCore.calculate(expStr); //計算表達式結果 4 String pattern = "(\\+)||(-)|(×)|(÷)|"; 5 Regex regex = new Regex(pattern); 6 MatchCollection list = regex.Matches(expStr); //利用正則匹配得出表達式中的運算符個數 7 int opnum = list.Count; 8 return new Expression(expStr, result, opnum); 9 }
這樣,本程序回避重復性的算法思路就很清晰了:
生成表達式並獲得結果——>通過交換律生成與之等價的表達式列——>通過結果進行查表,如果有該結果,則再查有無重復式子(如無該結果,則表達式必然有效,直接插表即可)——>如有,則放棄該表達式 ; 如無,則將該表達式的表達式列插入,繼續生成直至達到規定數目
如何證明程序中真的沒有出現重復的式子?
根據這個算法就很簡單了,顯然我們可以將輸出的所有表達式和相應生成的重復表達式一並輸出(本項目中該功能通過-i開啟,通過CheckSame.exe對當前目錄下Exercise.txt進行檢測),因為輸出的表達式也與自身重復,所以只有輸出的表達式會在輸出文件中出現兩次,這樣如果輸出的表達式中有兩個重復了,即一個式子與另一個式子生成的重復表達式相同,該表達式就會出現兩次以上,這樣就很容易通過查重程序檢測出來了。只要理解的重復性規則正確,保證對於每個表達式充分生成了與之重復的所有表達式,該檢測方法即可有效對結果的重復性進行檢測。
B. 表達式結果伴隨而表達式生成,與計算模塊獨立
當我得知,還需要為這個程序設計一個計算表達式結果並與答案表比對的模塊時,我的第一反應是直接用這個計算模塊去求生成的有效表達式的值,因為這樣程序的編碼,設計會簡單很多。但是,相應的,生成一遍、再讀一遍、再出棧入棧各種算一遍,耗費的時間資源就高了起來。此外,生成的表達式,其計算順序未必與生成順序一致。
比如:e => e1 - e2 => e1 - e5 - e6 => e3×e4 - e5 - e6 => 5×2 - 6 -1
如果按照生成時從后往前的順序,先算e2:6-1,再算e1:5×2,最后算e1-e2: 5×2 - 6 -1 ,結果必然是錯的;
為了解決此問題,我標准化了表達式的生成規范,即:對於產生的任何二元表達式,都為其在外面加上括號,通過括號,就完整的保留了整個表達式生成過程中各個部分的出現次序,運算的優先級也與生成次序統一到了一起,這樣就可以做到生成一個子表達式時,迅速得到其結果並反饋給母表達式使用,且無優先級影響,不會出錯。得到一元式或二元式的結果很容易,一級一級向上傳遞結果,最終結果計算就得到了簡化,而試圖進行括號匹配,或對原式一點點做拆分、優先級分析是很麻煩的(不過在計算模塊中,我們仍然必須實現這一點),這樣比產生運算式答案的效率上就大大提升了
C. 使用long而不是int
雖然本程序目標對象是小學生,不過這樣隨機產生的運算式仍有可能因為通分問題產生數值非常大的分子,這里引用我所在團隊一位同學的論斷
我們剛才提到了值域的擴大對於最終結果的最大值的影響是十分之大的,其能高達r^8倍。比如下面這個例子,如果我們的值域是20:
( 16'11/15 ÷ 17'4/19 ) × ( 6'1/2 ÷ 12'1/15 )
這里還有一個坑的地方,在於值域的上限。上面提到分子中最高有可能到r^8的水准是有計算依據的。我們設想存在這樣一個數字,其分子接近r^2,分母接近r,但是分子與分母互質。設這個數為x,那么
x * x * x * x 完全可以達到r^8的數量級。按這樣計算,如果使用int
定義分子分母的類型,那么只要 r 達到 15 ,就可能出現超過 int 型范圍的數字,最后導致結果為 負數。
大家可以去參看他的博客(http://www.cnblogs.com/SivilTaram/p/4828591.html),在本次工程項目中,我很多想法都來自於他的啟示。
因此,在分數類中,我們需要將分子分母定義為long,也即長整形,這樣便可以使得程序支持的值域達到200以上,基本可以滿足需求了。
由於Math類的很多方法是針對整形的,我們需要針對長整形重新寫一下方法,如Abs取絕對值,Max取最大值,以及計算兩個長整形數的GCD算法,這些筆者都整合到MathCore這個靜態方法類里了,前面提到用於計算表達是值的calculate()方法自然也是該類的成員
D.計算模塊的簡化
數字表達式計算式是本項目里面另一個需要投入精力的地方,筆者自己寫了一個計算程序,並非最優,但通過采用棧的數據結構,也做到了一定程度上對計算過程的簡化(無中綴后綴表達式轉換)
算法思路如下:首先針對本體的表達式規范(無負數,但有帶分數,僅含四則運算符和括號),通過用正則表達式獲得其各個元素,代碼如下:
1 String pattern = "(\\d+'\\d+\\/\\d+)|(\\d+\\/\\d+)|(\\d+)|(\\+)|(-)|(×)|(÷)|(\\()|(\\))";//列舉所有可能的操作數或操作符形式 2 Regex reg = new Regex(pattern); 3 exp = "(" + exp.Replace("\\s+", "") + ")";//通過加括號,我們可以保證任何計算過程回溯的終點為“(”,這有助於處理形式統一 4 MatchCollection ele = reg.Matches(exp);
然后通過建棧,逐一壓入操作元素,對於不同操作符,作如下處理:
1.如遇到×
÷且后一個元素為操作數的話(即非“(”),取當前棧頂操作數與之運算並將結果壓入棧中;
2.如遇到“)”則自后向前(由於棧是后進先出的)運算至棧頂出現“(”並將其取出
(注意考慮 -e1-e2 、-e1+e2的情況,自后向前運算時不僅要取運算符和前一個操作數,當運算為加減法時前一個操作符是否為-號也是需要去處理的)
取出並算出結果后,如前一個運算符(當前棧頂)為×
÷,則此結果與前一個操作數運算后,繼續取棧頂,循環判斷至前一個運算符為+-或"("時,將最終運算結果壓入棧頂結束過程
生成時所加的括號規范作用在這里就體現出來了,對於該算法,計算出最后結果的時候,整個式子必須有至少一對括號才可以。
E. 除法減法有效性處理
本程序設計一大核心思想就在於盡可能的避免冗余性處理,對於除法或減法無效的情況(減法出現負數,除法整除或者分母為0)均可以通過調換操作數(減數和被減數,分子和分母)的位置進行處理,當然,對於操作數相等的情況應盡量避免
關於性能分析,因為VS2012為早期的桌面免費版(社區版?),似乎並沒有這樣的工具,所以只好借同學的VS生成了一張
S4 項目測試
首先分享程序本體及測試文件,重復性檢測程序,在配有的Readme文檔中,我作了相應的說明。(下載 密碼:7ck5)
測試用命令行參數:(Defauts: -r 10 -n 10000 -o 3)
1.ExpressionOutputer.exe
Time Cost: 00:00:00.1730099
2.ExpressionOutputer.exe -e ./Exercise.txt -a ./Answer.txt (Depend on exercise file)
Time Cost(Defauts Setting): 00:00:05.0092865
3.ExpressionOutputer.exe -e ./Exercise2.txt -a ./Answer2.txt (Depend on exercise file)
Time Cost(Defauts Setting): 00:00:05.1052920
4.ExpressionOutputer.exe -r 1 -n 30
Time Cost: 00:00:00.0150009
5.ExpressionOutputer.exe -r 2 -n 500
Time Cost: 00:00:00.0160009
6.ExpressionOutputer.exe -r 3 -n 10000
Time Cost: 00:00:00.3540203
7.ExpressionOutputer.exe -r 5 -n 20000 -o 4
Time Cost: 00:00:00.4230242
8.ExpressionOutputer.exe -r 10 -n 100000 -i
Time Cost: 00:00:03.2331849
9.ExpressionOutputer.exe -r 10 -n 100000 -o 5 -i
Time Cost: 00:00:04.5312591
10.ExpressionOutputer.exe -r 255 -n 1000000
Time Cost: 00:00:12.8217333
-o與-i是筆者程序引入的兩個新參數,均在readme中有所介紹,-o為限制操作符個數,-i為開啟冗余表達式輸出
存在問題:
1.程序對於輸入處理,在報錯機制上尚不完善,因此仍需規范輸入,對於負數表達式的支持不完全(支持負數結果但不支持負操作數)
2.對於除法,實際上程序允許被除數與除數相等的情況,因此使得低范圍下生成題目的數量和速度得到了大幅提升,對於除法表達式中必須為真分數這一規定的意義持疑問態度,依據博文中的規定:
- 真分數:1/2, 1/3, 2/3, 1/4, 1’1/2, …
實際上規定的是帶分數,帶分數是假分數的另一種表達形式,而假分數允許取值為1,並非特例
3.對於命令行參數的處理尚不完善,還有很多規范參數輸入的地方沒有做,如取值范圍與生成表達式數目之間應當遵循的一定關系
由於時間原因這些問題遺留了下來,還請見諒
S5 收獲與體會
通過本次的個人項目練習,我收獲了很多有關C#的高級運用技巧,如運算符重載,傳出參數,靜態方法類,IO流,Diretionary類等等,也許是因為C#的語法特點與JAVA的面向對象思想十分相似,這門語言上手還是蠻快的。在設計項目,進行編程的時候充分考慮用戶需求並試圖做一些優化,這樣的事情在上學期的OO課中已經得到了充分鍛煉,因此一些處理方法,如方法正確性證明,前后置條件覆蓋性分析,根據用戶需求規划各個類的功能部,工廠模式等在軟工這樣的項目型作業中就能夠學以致用。不過個人項目畢竟是個人項目,代碼風格,可讀性,以及團隊協作等方面仍需通過結對項目和團隊項目進行鍛煉,筆者將會在后續的課程任務中繼續努力,逐步達到並嘗試超越 軟件工程這門課程對於學生在能力上的要求。
以上 by kibbon