第一單元實驗總結 | TrickEye
第一單元實驗總結 | TrickEye
目錄基本情況部分
- 這篇帖子為什麼會在這?
- 這是北航計算機學院面向物件構造與設計2022春季課程第一單元的總結部落格。
- 本次作業的要求是什麼?
- 消除複雜中綴表示式的非必要括號,儘可能在恆等的前提下縮短表示式長度。
- 表示式含有的字符集為:{數字,自變數,+,-,*,^,cos,sin,自定義函式,求和表示式}
個人程式碼結構分析
個人的解決辦法基本上實現了輸入解析和計算化簡的解耦合。讀取和解析部分由軟體包expr完成並返回結果給main,計算部分由軟體包calc完成並返回給main。
以上又可以分為幾個子任務,每一個子任務之後都有一個可以被檢視的返回值提供給main(呼叫者)。這無疑方便了除錯過程。
- 圖1:完成作業要求的流程圖
- 首先呼叫官方包完成輸入(
Normal Mode
)。 - 由遞迴下降法解析得到一個最終的
Expr(expr.Expr)
類物件,這個物件代表了整個式子。 - 通過遞迴地呼叫
Expr, Term, Factor
的toParsed
方法,得到一列形如預解析模式輸入的字串,完成預解析職能。得到的一系列字串由parsed
容器儲存。 - 使用
process
方法,對parsed
中所有的字串建立表示式樹,返回最後的一個node
,作為表示式樹的根節點,代表整棵表示式樹。 - 使用
getValue
- 使用化簡和輸出方法完成輸出。
- 圖2:個人程式碼類圖(有刪改)
- 其中,輸入、解析、轉換成預解析形式的職能由expr包完成,計算、化簡、輸出職能由calc包完成,兩個包體之間完全解耦合
概念和方法解釋:
-
表示式樹
- 由觀察得到,本次作業的所有運算子都是至多二元的。抽象的說,每一個運算子通過某種規則操作兩個Expression,並通過數學含義,得到一個新的Expression。
- 因此將表示式抽象成為一棵樹是合理可行的。
- 這棵樹的每一個節點都代表一個運算子(非葉節點),或者一個不可再拆分的基礎項(葉節點),每個非葉節點有至少1個,至多2個孩子。
- 每個節點都應該有
getValue()
-
遞迴下降、符號處理和自動機
-
為什麼把這三個東西放到一塊呢?因為這仨恰好是我完成此次作業的時候遇到的迷茫、難點和解決方案。
-
第一單元課下實驗提供了一份使用遞迴下降法來處理表達式的程式碼,當時的
parser, lexer
模型是我遞迴下降的啟蒙,雖然第一次作業時我看不懂,採用了字串替換和逆波蘭表示式法來解析表示式,但是付出了被找出兩個bug的慘痛教訓。 -
因此第二次作業我就開始了用遞迴下降法重構程式碼的工程,而符號處理就是我緊接著遇到的問題。(也是我第一次討論課在榮老師討論課上提出來的問題)一個表示式前面可能有許多個連續的正負號(帶符號整數,項,表示式前面都可能有符號),我們不見得知道哪個符號是誰的,這對解析帶來了不小的麻煩。
-
但是感謝有限狀態自動機,感謝數學,後來我想清楚的一個道理是:符號歸屬於哪個因子不應該影響整個式子的數學含義和代數意義上的值。因此完全可以採用貪婪匹配法:只要正在解析的這個項有可能有符號,我們又遇到了符號,就認為這個符號是我們正在解析的這個項的符號。這樣的話,一個有限狀態自動機就初具雛形了。
-
圖3:用於解析表示式的有限狀態自動機,(有修改,功能不一定正確完整)
-
關於符號處理這方面,我觀察到有的同學在第三次作業仍然採用了替換連續出現的正負號。雖然這樣做在給定的規則下似乎是不可攻破的,但是我仍然不認為這是一個好的做法。這樣的作法一旦加入了錯誤格式判斷就會出大問題。(但是非常可惜最終還是沒有引入Wrong Format錯誤。
-
-
為什麼要預解析?
- 筆者從第一次作業開始,都是先完成了預解析模式的功能實現(也就是根據預解析模式建樹、計算),隨後再完成了從普通表示式轉換成預解析模式的功能實現,因此兩個功能部分解耦合。
- 使用預解析從某種程度上說,也算是採用了化歸的思想,將表示式中的所有元素都視作預解析模式中的一項,這在第二次作業加入了自定義函式、求和函式之後為筆者避免了諸多字串替換等不必要的麻煩。
- 預解析函式接收一個字串ArrayList容器,在執行過程中會直接更改這個容器,同時返回一個字串,代表要解析的這個項在預解析模式中的名稱,以後稱之為此項的形式名稱。總地來說,預解析的實現方法是:遞迴地呼叫該項中所有的子項的預解析方法,儲存這些子項的形式名稱,根據當前項的數學含義連結這些形式名稱,並返回這個項的形式名稱。
程式碼複雜度(基於第三次作業)
class |
Average Operation Complexity |
OCMax |
Weighed Complexity |
---|---|---|---|
homework.calc.Base |
1.529412 |
5 |
26 |
homework.calc.Complex |
2.6875 |
9 |
43 |
homework.calc.Expr |
2.588235 |
7 |
44 |
homework.calc.Node |
2.411765 |
12 |
41 |
homework.calc.Tri |
1.55 |
6 |
31 |
homework.expr.Expr |
2.333333 |
5 |
14 |
homework.expr.ExprFactor |
2 |
3 |
6 |
homework.expr.Function |
1 |
1 |
3 |
homework.expr.FunctionCallFactor |
2.333333 |
4 |
7 |
homework.expr.Number |
1 |
1 |
4 |
homework.expr.SumFactor |
2.333333 |
5 |
7 |
homework.expr.Term |
2.8 |
7 |
14 |
homework.expr.TriFactor |
2.666667 |
4 |
8 |
homework.expr.VariableFactor |
2 |
3 |
6 |
homework.Factory |
2 |
4 |
8 |
homework.Lexer |
2 |
4 |
8 |
homework.Main |
3 |
4 |
6 |
homework.Parser |
4 |
10 |
20 |
Total |
296 |
||
Average |
2.192593 |
5.222222 |
14.09524 |
- 可以看到,計算時的程式碼複雜度比解析的複雜度高多了,這是因為calc包中的一個類又承擔計算,又承擔化簡,如果把計算和化簡寫開,或許複雜度會好一些。
method |
Cognitive Complexity |
Essential Cyclomatic Complexity |
Design |
Cyclomatic |
---|---|---|---|---|
homework.Parser.parseFactor() |
19 |
6 |
9 |
10 |
homework.calc.Complex.combinable(Complex) |
16 |
9 |
7 |
9 |
homework.calc.Expr.shorten() |
16 |
1 |
7 |
8 |
homework.calc.Tri.toString() |
13 |
1 |
5 |
8 |
homework.expr.Term.toParsed(ArrayList) |
13 |
5 |
7 |
7 |
homework.calc.Complex.mulItem(Item) |
10 |
4 |
7 |
7 |
homework.calc.Complex.toString() |
10 |
5 |
8 |
9 |
homework.expr.Expr.toParsed(ArrayList) |
9 |
2 |
4 |
5 |
... |
... |
... |
... |
... |
Average |
1.7555555555555555 |
1.525925925925926 |
2.0148148148148146 |
2.325925925925926 |
- 這裡選取的是Cognitive Complexity最高的幾個方法,可以看到主要還是Parser類的解析方法(也就是上文中提到的狀態機的主要實現部分)
王婆賣瓜和自我批評
- 王婆賣瓜主要是針對第二三次作業。我認為:我的第二、三次作業的架構比較規範,一方面嚴格根據表示式文法建樹,另一方面也兼顧了準確性和化簡。第二、三次作業在正確性上沒有出任何問題,第二次作業強測100分,但是第三次作業優化沿用了第二次的邏輯,因此沒有把效能分拿全,略有遺憾。
- 自我批評方面,第一次作業寫的實在有些臭。沒有采用遞迴下降法,而是字串替換,這產生了一個隱形錯誤,(明明用遞迴下降就可以規避的)此外也犯錯誤採用了
parseInt
而不是BigInteger
導致用了BigInteger
也被爆資料了(悲
class |
Average Operation Complexity |
OCMax |
Weighed Complexity |
---|---|---|---|
Factory |
4.555555555555555 |
12.0 |
41.0 |
Item |
2.9 |
17.0 |
29.0 |
Main |
1.5 |
2.0 |
6.0 |
Node |
1.894736842105263 |
9.0 |
36.0 |
Poly |
3.0 |
6.0 |
36.0 |
Total |
148.0 |
||
Average |
2.740740740740741 |
9.2 |
29.6 |
- 以上是第一次作業的類複雜度。第一次作業寫的實在沒有任何被學習的價值,就自己留著吸取慘痛教訓,不放出來了嗷
Hack與被Hack
作業 | 情況 |
---|---|
hw1 |
發起0/7 受到6/23 嗚嗚嗚嗚嗚嗚 |
hw2 |
發起3/28 受到0/27 哈哈哈 |
hw3 |
發起1/7 受到0/29 哈 |
-
經驗總結
- 只有努力變強,才能沒有bug,才能找別人的bug
-
被找到的Bug
- 這裡主要還是就著第一次作業來講:
- 第一個Bug是大整數,上回說到,用了
parseInt
而不是BigInteger
的構造器(中測Debug
的時候也沒有改徹底),因此只要給我一個長整數我就傻了。不過這個Bug也還算好修 - 第二個Bug是字串替換。我的第一次作業策略是替換所有的
(+-)(\\d+)
為\(0(\1\2\)
,這本來是一個恆等變形的,但是錯誤的使用了String.replace
方法,並錯誤的以為這個方法是隻對首次匹配生效(實際是對所有匹配生效),因此被樣例-3+-345
卡住,替換成了(0-3)+(0-3)45
- 以上都不是什麼有共性的bug,所以我在提交的時候完全沒有考慮過測試這些點。
- 這倒也說明了只要你的程式碼有bug,就一定會被熱心網友們整的,不要心存僥倖。
-
找到的Bug
-
第二三次作業都有成功hack入賬,他們分別是:
-
hw2-1
沒有考慮三角函式後的指數 -
0 sin(-10)**+2
-
hw2-2
無腦替換了x**1為x -
0 x**10
-
hw2-3
這個不知道怎麼錯的,估計是0-x
處理失誤 -
0 (0-x)**+2
-
hw3-1
sum
含有BigInteger
-
0 sum(i, 9999999999999998, 9999999999999999, i)
-
策略方面,第二三次作業吸取了第一次作業被幹的教訓,明白了普普通通的資料是幹不了人的。因此寫了資料生成和測試指令碼,生成了許多有攻擊性的資料,在加以手工測試一些有意思的點,總的來說,hack還是很快樂的。
-
-
但是還是有別人找到我沒有找到的Bug,所以還要努力變強!
心得體會
作為與OOP的第一次接觸(不算pre,跟這個相比Pre跟玩似的),本單元作業從被幹爛的hw1開始,到最後基本沒有被發現bug,心裡還是比較爽的。
和麵向過程相比,OOP的架構設計顯得尤為重要,當年看到題目就開始寫程式碼的日子已經一去不復返咯,我們需要使勁琢磨資料的組織方式,處理方式,需要考慮各種需要考慮的複雜問題。
希望能夠活過今後的電梯月!