1. 程式人生 > 其它 >BUAA-OO-第一單元總結

BUAA-OO-第一單元總結

BUAA-OO-第一單元總結

全文目錄

一、基於度量分析程式結構和架構設計

第一次作業

第一次作業難點在於對錶達式的解析,由於表示式的構成層次存在巢狀關係,初次接觸時顯得有些無從入手,需要花大量時間理清作業的形式化表述,並考慮好需要建立哪些類,類和類之間存在哪些關係,如何對錶達式進行解析。由於本次作業僅含有x一個自變數,也只有冪函式一種初等函式,在理清表示式構成因子的邏輯關係後,表示式的化簡反而並不難處理,只需要選擇好容器和變數型別即可。

參考助教的表示式解析方法,並根據根據形式化表述中的描述:

因子 → 變數因子 | 常數因子 | 表示式因子

故可以提供一個Factor介面,建立Expression類(即表示式因子)和Number類(即變數因子和常數因子)兩個類實現該介面,同時建立一個Term類作為Expression類的組成。其中ExpressionTerm構成,在程式中表現為Expression中有一個型別為ArrayList<Term>的屬性;TermFactor構成,在程式中表現為Term類有一個型別為ArrayList<Factor>的屬性。通過這種方式解析,即將原式劃分為表示式、項、冪函式

三個層次。

而對於計算來說,一般性的思路為:對錶達式各項進行計算併合並,再對錶達式進行合併同類項,具體流程如下圖所示

同時由於第一次作業中僅存在自變數x,並且僅存在冪函式一種初等函式,於是筆者通過HashMap儲存最終化簡結果,其中Key為冪函式的次數,Value為冪函式的係數。通過檢索HashMap的鍵值即可進行同類項合併以及最終的表示式輸出。

在設計中遇到連續符號的處理問題,因為不想給表示式因子賦予指數和係數兩個屬性(事實上解析時對錶達式的乘方筆者是通過乘多個相同因子處理的),而且因為沒有充分理解形式化表述中符號的要求,所以一度成為筆者設計時的一個難點。最後採用瞭如下方法,解析遇到符號時進行迴圈處理,如果符號後面緊跟表示式,則視為表示式乘了一個

1-1

第一次作業的UML圖如下所示,整體來看本次作業實現方式還是比較“面向物件”的,對錶達式的層次分析邏輯合理、思路清晰,並且具有較強的魯棒性,強測和互測都沒出現問題,在測試時也只發現一處解析時對括號的匹配存在錯誤。但如print()等部分方法,還是存在“面前過程”的現象,後續可以通過在Term類和Number類同樣提供一個print()方法解決,呼叫Expression類的print()方法的過程中呼叫Term類的print()方法,以此來減少Expression承擔的非必要責任和功能。

 

 

第一次作業複雜度分析:

由於Expr.print()方法基本採用“面向過程”的思路編寫,導致程式碼量長,同時方法複雜度也比較高。其次是Parser類中的各方法,由於解析過程情況比較複雜,也導致方法複雜度較高。這與上述分析一致。

 

第一次作業程式碼量:

次作業

因為“解析表示式”這個boss在第一次作業中已經拿下了,顯得第二次作業難度稍低於第一次作業。第二次作業需要新加入三角函式因子,以及函式和求和函式。對於三角函式,只需新建一個Trigo類並實現Factor介面即可。而處理函式和求和函式,主要分為了兩個流派,一是將函式視為一種特殊的表示式因子,解析表示式時對其進行處理。另一種方法是在讀取到字串的時候,對字串進行預處理,將函式表示式替換為普通表示式的形式。筆者採用了第二種方法,這種方法具有以下優點:

邏輯清晰,不破壞原程式的結構

處理方便,只需要進行簡單的正則表示式匹配和字串處理即可實現

將任務獨立出來,儘可能做到“高內聚,低耦合”,並且也讓debug的過程輕鬆一些

第二次作業UML圖如下所示,相較於第一次作業而言新增的類並不多。新增了一個Func類儲存讀入的函式形式;一個PreProcess類對讀入的表示式進行預處理,如去除空白字元,對函式和求和函式進行字串替換;一個Trigo類儲存三角函式因子。由於第一次所用計算方法比較巧妙迅速,但同時也存在著普適性較弱的問題,面對多變數的處理尚有一戰之力,但再加入三角函式後就有點力不從心了。於是筆者對計算方法進行了重構,顯得某些部分存在邏輯混亂的現象,也導致部分方法複雜度較高。

第二次作業複雜度分析:

可以看到,由於對第一次作業中的print()方法用面向物件的思路進行改良,儘管第二次作業處理難度大於第一次作業,方法整體複雜度依然有所下降。

第二次作業程式碼量:

次作業

第三次作業應該是整體難度最低的一次,但筆者卻問題頻出,主要原因還是這次沒有擺,對輸出結果進行優化了。這次作業增加了函式巢狀和三角函式內部巢狀表示式因子,前者對於我的架構而言需要修改的地方很少,因為第一次作業中的解析時採用遞迴梯度下降的方法,這次作業的巢狀括號並沒有什麼問題,對於巢狀函式而言,也僅需要在預處理替換函式進行遞迴呼叫即可。主要難點還是在三角函式內部巢狀表示式因子上,需要修改三角函式的內部屬性,以及一些用以比對兩三角函式型別是否相等方法。

在對形如sin((x-sin(x)+cos(x**2))) + sin((-x+sin(x)-cos(x**2))) = 0的優化中 ,由於筆者並未使用Hash類的容器儲存各個類的屬性,在判斷優化條件時需要消耗大量的時間對三角函式的內部表示式因子進行多次遍歷,讓本就跑得很慢的程式碼更是雪上加霜。於是在處理這個問題時,選擇了模仿hash表的思想,依照所定規則賦予三角函式內每個項一個權值,根據權值判斷此時的三角函式是否需要提出一個負號。但由於筆者所定規則沒有經過足夠的資料測試和數學證明,也沒有考慮處理雜湊衝突,這裡就不放出來誤導各位了。

第三次作業新增功能並不多,程式結構與第二次作業基本相同,僅更改了Trigo類屬性的型別。大部分新增程式碼都是在對計算結果進行長度優化。

第三次作業複雜度分析:

 

因為第三次作業新增了不少優化策略,並且三角函式的內部因子取消了對錶達式的限制,所以部分方法的複雜度相較於的第二次作業提高不少,之後可以對複雜度過高的方法進行功能分割,將其分為多個方法,負責不同職能。

第三次作業程式碼量:

 

二、分析自己程式的BUG

因為程式結構比較清晰,並且提交前也做了很多測試,三次作業中只有第三次互測時被發現了Bug,原因在處理求和函式中涉及到i**2時,因為沒考慮到數字也可以有次冪,沒有對常數的次冪進行處理。

雖然公測和互測中沒有發現太多bug,但自己測試的時候倒是發現了不少。

  1. 由於第二次作業中三角函式的因子不可能存在三角函式,所以對三角函式判同類型時,沒有對名字(即sincos)進行判斷,導致程式會認為sin(sin(x)) sin(cos(x))是同類型的三角函式。
  2. 括號匹配問題。由於第三次作業涉及大量括號巢狀,包括三角因子的括號巢狀,自定函式的括號巢狀,尤其是因為採用字串替換的方法處理自定函式,還會再引入大量括號,於是在解析三角函式的內部因子時發現了很多因括號匹配導致的bug
  3. 物件的淺拷貝問題。在合併同類項時,會涉及很多對物件屬性的直接賦值操作,如果不小心使用了淺拷貝,會導致在操作某物件時,對另一個物件也進行了賦值。為了避免這種情況,筆者為每個因子設計了copy()方法,返回的是與當前物件屬性完全相同的一個新建立的物件。但後面才知道可以重寫clone()方法。

 

三、hack策略以及發現的問題

本單元進行hack時基本採用如下策略:

  • 通讀對方的程式碼,檢查是否存在比較簡單發現的、大家共有的一些問題,如項的係數、求和函式的上下界不使用BigInteger存會存在爆int的風險,sin(-x)提出負號時有沒有歸併到統一的係數上(部分同學存在sin(-x)**2 = -sin(x)**2的bug)等。
  • 使用自己在測試時發現的幹掉自己的資料進行hack嘗試,這種方法在針對和自己架構相似的同學時效果很好。
  • 利用自動生成的資料進行轟炸,進行儘可能完全的覆蓋性測試。

不少同學都是在優化部分出現了bug,比如複雜情況下的cos(x)**2+sin(x)**2的優化,處理形如sin(x)**2*cos(x)**2+sin(x)**2*cos(x)**2的表示式時,有同學的優化方法在判定兩項均有sin(x)**2後,仍然會將後項的cos(x)**2和前項的sin(x)**2進行匹配,認為符合cos(x)**2+sin(x)**2的條件。我認為程式碼量越大,就越容易產生問題,尤其是當優化情況多、優化條件比較複雜時,一定要多對優化部分進行分析與測試,以免因小失大。

但測試時我也發現有的大佬甚至進行了二倍角優化、誘導公式的優化,而且簡單測試後也很難發現問題,在這裡膜一波ORZ。

四、感悟與體會

本單元作業對於面向物件小白來說是一次難度極高的挑戰,助教在實驗訓練中提供的遞迴梯度下降解析法示例,幾乎成了我第一次作業的救星,提供了一個很棒的解析思路,在此特別感謝助教組和課程組的不殺之恩。

在各次作業的迭代中,由於在新增功能時沒有考慮清楚前一次作業的已有功能是否滿足需求,導致產生了不少問題。在後續單元的作業中,最好還是先畫UML圖構建好整個作業的架構,這樣在後續的迭代中不至於發生遺忘前一次作業的細節的情況。同時,在第一次作業選擇計算方法時,儘管考慮到後續可能會不限制自變數個數,但還是沒有想到會引入三角函式,導致第一次作業的計算方法不再適用於後續作業(當然也可能是太菜了,沒有想到解決辦法),以後在設計時還是應該多結合後續迭代情況,避免連程式的基石都要重構的情況(真的不想重構辣)。

但人的預想和遠見畢竟是有限的,在遇到當前架構確實無法處理的情況時,還是應當該下手時就下手,及時進行重構。第二次作業時正是因為猶豫不決,導致決定重構時已經是週三晚上了,顯得時間非常緊,而且測試也不太夠,所幸在強測和互測時沒有暴露出問題。

希望之後的幾個單元同學們能順利通過~