OO第一單元

繁華丶人間發表於2022-03-25

OO第一單元總結

前言

本次OO第一單元總結將從如下幾個部分展開:

1.三次作業迭代開發思路

2.整體架構分析

3.自動化生成資料及自動化測評實現思路

4.自我程式bug分析及測試手段

5.他人程式bug分析

6.hack別人程式bug策略

7.心得體會

8.鳴謝

複雜度分析利用IDEA的MetricsReloaded外掛,程式碼規格統計利用IDEA的static外掛

第一次作業

HW1基本思路

​ 雖然筆者在寒假較為充分地預習了JAVA,但在面對第一次作業時仍然感到有一點無從下手,感到難度巨大,尤其是在面對一種叫做“遞迴下降”的前所未聞的方法時,筆者一度認為筆者過不了中測。可是事實證明課程組對作業的安排和設定是合理的,在潛下心來研究遞迴下降法後,筆者發現這種方法並沒有想象中的這麼難,只是一種基於文法的遞迴呼叫。當要解析一個表示式的時候,可以遞迴下降解析表示式中的每一個項;當要解析項的時候,可以遞迴下降每一個因子。解析每一個因子的時候,根據當前的token解析分類解析每一個因子(冪函式,常數,表示式因子)。這樣一來,利用遞迴下降便可以很自然地處理括號巢狀的情況。因此,從第一次作業到第三次作業,筆者都能狗處理巢狀括號的情況(自己有空玩玩(1+(1+x)**2)**2也挺爽的)

​ 在理解了這一點後,筆者又遇到了一個難點,應該採取什麼資料結構來表示每個物件的狀態?由於當時筆者仍然是程式導向思維(現在似乎也沒有改善多少oao),簡單地採取了採用HashMap<Integer, BigInteger>來儲存每個x的指數和係數,雖然對於第一次作業這是完全沒有問題的,但是對於第二次作業便捉襟見肘,不得不重構了。

UML類圖

OO1

PowFunc、Expr、ConstantNum實現Factor介面便於實現多型進行遞迴呼叫,parser類依賴Lexer進行文法解析

程式碼規模

HW1static

一共寫了474行程式碼,似乎並不算多?

複雜度分析

方法複雜度

先解釋一下ev(G)、 iv(G)、 v(G)的含義

ev(G)是基本複雜度,是用來衡量程式非結構化程度的,非結構成分降低了程式的質量,增加了程式碼的維護難度,使程式難於理解。因此,基本複雜度高意味著非結構化程度高,難以模組化和維護。實際上,消除了一個錯誤有時會引起其他的錯誤。

Iv(G)是模組設計複雜度,是用來衡量模組判定結構,即模組和其他模組的呼叫關係。軟體模組設計複雜度高意味模組耦合度高,這將導致模組難於隔離、維護和複用。模組設計複雜度是從模組流程圖中移去那些不包含呼叫子模組的判定和迴圈結構後得出的圈複雜度,因此模組設計複雜度不能大於圈複雜度,通常是遠小於圈複雜度。

v(G)是圈複雜度,用來衡量一個模組判定結構的複雜程度,數量上表現為獨立路徑的條數,即合理的預防錯誤所需測試的最少路徑條數,圈複雜度大說明程式程式碼可能質量低且難於測試和維護,經驗表明,程式的可能錯誤和高的圈複雜度有著很大關係。

from: https://blog.csdn.net/weixin_30346033/article/details/97099084

Method CogC ev(G) iv(G) v(G)
ConstantNum.ConstantNum(String) 0 1 1 1
ConstantNum.changeForNeg() 3 1 3 3
ConstantNum.getPowAndCoe() 0 1 1 1
ConstantNum.setNegative(boolean) 0 1 1 1
Expr.Expr() 0 1 1 1
Expr.addTerm(Term, boolean) 0 1 1 1
Expr.changeForNeg() 3 1 3 3
Expr.getPowAndCoe() 0 1 1 1
Expr.getStrFunc(int, BigInteger) 11 3 7 8
Expr.merge() 3 1 3 3
Expr.mergeByPow(int) 7 1 5 5
Expr.setNegative(boolean) 0 1 1 1
Expr.toString() 8 1 5 5
Lexer.Lexer(String) 0 1 1 1
Lexer.getNum() 2 1 3 3
Lexer.getPowX() 5 1 4 4
Lexer.getSignedNum() 0 1 1 1
Lexer.isPow() 1 1 2 2
Lexer.isSignedNum() 1 1 3 3
Lexer.moveBlank() 3 2 2 4
Lexer.next() 8 2 6 7
Lexer.peek() 0 1 1 1
Main.main(String[]) 3 3 2 3
Parser.Parser(Lexer) 0 1 1 1
Parser.parseExpr() 8 1 6 6
Parser.parseFactor() 11 4 6 6
Parser.parseTerm() 4 1 4 4
PowFunc.PowFunc(String) 2 1 2 2
PowFunc.changeForNeg() 3 1 3 3
PowFunc.getPowAndCoe() 0 1 1 1
PowFunc.setNegative(boolean) 0 1 1 1
Term.Term() 0 1 1 1
Term.addFactor(Factor, boolean) 0 1 1 1
Term.getPowAndCoe() 0 1 1 1
Term.merge() 14 1 7 7
Term.setNegetive(boolean) 0 1 1 1
Total 100.0 45.0 93.0 98.0
Average 2.78 1.25 2.58 2.72
分析

可以看到,整體的複雜度並不高。但可以發現其中Parser類的方法複雜度較高,這是因為parser類作為遞迴下降的分析器,大量呼叫了Expr,Term,Factor的方法(將某一項設定為負數)。這一點是當初寫程式碼的時候考慮得不夠周全的地方,應該把每一項設定為負數和parser類解耦。

類複雜度

同樣先解釋一下OCavg、OCmax、 WMC的含義

OCavg是類平均圈複雜度, 繼承類不計入

OCmax應該是類最大圈複雜度

WMC是類總圈複雜度

Class OCavg OCmax WMC
ConstantNum 1.5 3 6
Expr 3 7 27
Lexer 2.33 7 21
Main 3 3 3
Parser 4 6 16
PowFunc 1.75 3 7
Term 2.2 7 11
Total 91.0
Average 2.53 5.1 13.0
分析

同樣可以看到,parser類的複雜度比較高,理由同上。

優化策略

第一次作業由於只包含多項式,因此優化較為簡單,筆者採用HashMap<Integer,Biginteger>作為每個因子,項,表示式的基本項,儲存指數和係數。

化簡時根據HashMap的指數和係數來進行合併同類項,由於HashMap存的是基本型別,因此也就不存在深拷貝和淺拷貝的問題。

可以做的優化有x**2->x*x,把正項提到前面。

但由於筆者太懶了,沒有把第一項前面的"+"去掉,導致答案為1的時候會輸出+1,因此強測碰到這個點效能分一下子就全沒了= =

第二次作業

HW2基本思路

第二次作業在第一次作業的基礎上增加了三角函式因子,自定義函式,求和函式等內容。

這樣一來採用HashMap<Integer,Biginteger>作為每個因子,項,表示式的基本項的思路就完全行不通了,只能進行重構,筆者本來想用一個四元組作為基本項,每一個表示式,項,因子儲存形如a*x**b*cos(x)**c*sin(x)**d的基本項,但這樣一看就很不OO,並且在仔細閱讀了指導書後,筆者發現還會有類似a*x**b*cos(x)**c*sin(x)**d*cos(x**e)**f*sin(x**g)**h這樣的項,因此基本項的長度是不定的,所以這個方案也被否決了。為了更好地從第一次作業迭代到第二次作業,筆者最後決定新建一個類BasicTerm作為每個表示式,項,因子的基本項。

BasicTerm

其中每個基本項有三個列表,儲存冪函式,cos,sin因子,在BasicTerm中實現“BasicTerm相加運算”、“BasicTerm相乘運算”最後便可以類似作業一中使用HashMap<BasicTerm, BigInteger>化簡(其中BigInter為每一個基本項的係數)。

對於文法解析器Parser和Lexer,由於第一次作業到第二次作業文法基本沒有變化,所以不需要重構,只需要在Lexer增加對新增加的因子的解析,以及在Parser中實現對新增因子的遞迴下降即可。

對於新增的自定義函式,筆者的做法是字串替換,即將對應的實參新增括號後替換到函式定義式中得到待解析的表示式,利用Parser和Lexer解析表示式,最後將解析完成的表示式返回。

對於新增的求和函式,筆者的做法也是字串替換,先設定一個總表示式,然後將每個求和表示式中的i換成當前進行運算的數字,然後將"+替換後的表示式"加入到總表示式中,最後類似自定義函式對總表示式進行解析(現在看來似乎可以邊代入邊解析,而不必轉化成一個總表示式再進行解析),然後將解析完成的表示式返回。

可以看到,在筆者的實現中,自定義函式和求和函式這兩個因子其實最後在輸入表示式中的地位就是一個表示式因子,只不過解析的方法與表示式因子有所不同。同時,要實現筆者對自定義函式和求和函式的解析方法,要求程式能支援對巢狀括號的處理,而對於遞迴下降法,這一點也就是自然而然的了。

最後,關於結果的輸出方法,筆者是在頂層Expr的toString遞迴呼叫每個Term的toString,每個Term遞迴呼叫因子的toString這樣遞迴輸出

UML類圖

OOHW2

其中Expr, Term, ConstantNum, PowFunc, Cos, Sin實現了Factor介面,由於自定義函式和求和函式只是返回的是表示式因子,而本質上只是一種“解析方法”,故不作為Factor介面的實現。

同時Term依賴Simplification類的靜態方法進行化簡。

程式碼規模

HW2static

對比於第一次作業的474行程式碼,第二次作業總共為1251行程式碼,幾乎是第一次作業的三倍,可以看出HW1到HW2是一個巨大的飛躍。

如果說第一次作業是從0到1,那第二次作業就是從1到n

BasicTerm.java 215 199 0.9255813953488372 1 0.004651162790697674 15 0.06976744186046512

在所有類中BasicTerm類的行數最多,共有215行,原因是它作為基本項,需要實現乘、加、相等這些基本運算,因此程式碼行數較高

複雜度分析

方法複雜度

Method CogC ev(G) iv(G) v(G)
BasicTerm.BasicTerm(PowFunc, Cos, Sin) 3 1 4 4
BasicTerm.deepClone() 0 1 1 1
BasicTerm.equals(Object) 4 3 4 6
BasicTerm.equalsOfCosList(ArrayList) 10 6 4 7
BasicTerm.equalsOfPowerList(ArrayList) 10 6 4 7
BasicTerm.equalsOfSinList(ArrayList) 10 6 4 7
BasicTerm.getCosxList() 0 1 1 1
BasicTerm.getPowerList() 0 1 1 1
BasicTerm.getSinxList() 0 1 1 1
BasicTerm.hashCode() 0 1 1 1
BasicTerm.mul(BasicTerm) 0 1 1 1
BasicTerm.mulOfCos(ArrayList) 15 2 6 7
BasicTerm.mulOfPower(ArrayList) 15 2 6 7
BasicTerm.mulOfSin(ArrayList) 15 2 6 7
ConstantNum.ConstantNum(String) 0 1 1 1
ConstantNum.equals(Object) 3 3 2 4
ConstantNum.getBasicTerm() 0 1 1 1
ConstantNum.getNum() 0 1 1 1
ConstantNum.hashCode() 0 1 1 1
ConstantNum.mulOfNegOne() 0 1 1 1
ConstantNum.toString() 0 1 1 1
Cos.Cos(int, Factor) 4 1 4 4
Cos.addPow(int) 0 1 1 1
Cos.equals(Object) 4 3 3 5
Cos.getBasicTerm() 0 1 1 1
Cos.getFactor() 0 1 1 1
Cos.getPow() 0 1 1 1
Cos.hashCode() 0 1 1 1
Cos.toString() 3 3 2 3
DefinedFunc.DefinedFunc() 0 1 1 1
DefinedFunc.addDefinedFunc(String) 1 1 2 2
DefinedFunc.useDefinedFunc(String) 1 1 2 2
Expr.Expr() 0 1 1 1
Expr.addTerm(Term, boolean) 0 1 1 1
Expr.cosPowerIsZero(ArrayList) 3 3 2 3
Expr.equals(Object) 3 3 2 4
Expr.getBasicTerm() 0 1 1 1
Expr.getStrFunc(BasicTerm, BigInteger) 20 3 13 13
Expr.hashCode() 0 1 1 1
Expr.merge() 3 1 3 3
Expr.mergeByPow(int) 7 1 5 5
Expr.powFuncPowerIsZero(ArrayList) 3 3 2 3
Expr.sinPowerIsZero(ArrayList) 3 3 2 3
Expr.toString() 17 1 6 7
Lexer.Lexer(String) 0 1 1 1
Lexer.getDefinedFunc() 5 1 2 4
Lexer.getNum() 2 1 3 3
Lexer.getPowX() 5 1 4 4
Lexer.getSignedNum() 0 1 1 1
Lexer.getSum() 5 1 2 4
Lexer.isCos() 1 1 2 2
Lexer.isDefinedFunc() 1 1 2 2
Lexer.isPow() 1 1 2 2
Lexer.isSignedNum() 1 1 3 3
Lexer.isSin() 1 1 2 2
Lexer.isSum() 1 1 2 2
Lexer.moveBlank() 3 2 2 4
Lexer.next() 12 2 10 11
Lexer.peek() 0 1 1 1
Main.main(String[]) 1 1 2 2
Parser.Parser(Lexer) 0 1 1 1
Parser.addDefinedFunc(DefinedFunc) 0 1 1 1
Parser.parseCosFactor() 3 1 3 3
Parser.parseExpr() 8 1 6 6
Parser.parseExprFactor() 3 1 3 3
Parser.parseFactor() 10 8 8 8
Parser.parseSinFactor() 3 1 3 3
Parser.parseTerm() 4 1 4 4
PowFunc.PowFunc(String) 2 1 2 2
PowFunc.addPow(int) 0 1 1 1
PowFunc.equals(Object) 4 3 3 5
PowFunc.getBasicTerm() 0 1 1 1
PowFunc.getPow() 0 1 1 1
PowFunc.getVarType() 0 1 1 1
PowFunc.hashCode() 0 1 1 1
PowFunc.toString() 3 3 2 3
Simplification.simplyPow(HashMap<BasicTerm, BigInteger>) 1 1 2 2
Sin.Sin(int, Factor) 7 1 5 5
Sin.addPow(int) 0 1 1 1
Sin.equals(Object) 4 3 3 5
Sin.getBasicTerm() 0 1 1 1
Sin.getFactor() 0 1 1 1
Sin.getPow() 0 1 1 1
Sin.hashCode() 0 1 1 1
Sin.toString() 3 3 2 3
SumFunc.SumFunc() 0 1 1 1
SumFunc.sumOfFunc(String) 2 2 2 3
Term.Term() 0 1 1 1
Term.addFactor(Factor) 0 1 1 1
Term.cloneMap(HashMap<BasicTerm, BigInteger>) 1 1 2 2
Term.equals(Object) 4 3 3 5
Term.getBasicTerm() 0 1 1 1
Term.hashCode() 0 1 1 1
Term.merge() 14 1 7 7
Term.setNegative(boolean) 0 1 1 1
Total 272.0 151.0 229.0 270.0
Average 2.863157894736842 1.5894736842105264 2.4105263157894736 2.8421052631578947
分析

對比第一次作業的

Total 100.0 45.0 93.0 98.0
Average 2.78 1.25 2.58 2.72

可以看到第二次作業的Iv(G)模組設計複雜度相對於第一次作業有所下降,但基本複雜度圈複雜度都有所增加。

筆者認為原因在於第二次作業增加了新的因子,Parser和Lexer需要實現對新因子的解析,正是這兩個類的對因子的解析造成了複雜度的提高。

類複雜度

Class OCavg OCmax WMC
BasicTerm 3.71 7 52
ConstantNum 1.29 3 9
Cos 1.88 4 15
DefinedFunc 1.67 2 5
Expr 3.5 11 42
Lexer 2.47 11 37
Main 2 2 2
Parser 3.5 8 28
PowFunc 1.62 3 13
Simplification 2 2 2
Sin 2 5 16
SumFunc 2 3 4
Term 2.12 7 17
Total 242.0
Average 2.5473684210526315 5.230769230769231 18.615384615384617
分析

Expr、Lexer、Parser類的複雜度較高。其中Lexer、Parser類的複雜度原因上文已經分析,現在分析一下Expr的複雜度。

在重新看了看程式碼後,筆者發現筆者在Expr類裡實現的toString方法需要判斷基本項中的每個列表中的因子指數是否為0,因此與cos、sin、powFunc三個類的耦合較為緊密。

筆者感覺應該可以再新建一個類,專門實現這些基本方法的判斷,與Expr類解耦,降低複雜度。

優化策略

由於第二次作業新增了三角因子,因此可以做的優化變得很多。

其中較難實現的是三角函式的平方和cos**2+sin**2=1優化。

筆者也嘗試做過平方和的優化,可惜能力有限並未成功,每次在評測機上跑一兩百組資料就會出錯,於是只能把平方和優化註釋掉了。。。但除了平方和以外,能做的小優化還是很多的,除了第一次作業的優化以外,筆者還實現了比如cos(0)=1,sin(0)=0,sin(-1)=sin(1),cos(-1)=cos(1)等優化。

第三次作業

HW3基本思路

第三次作業可以稱得上是OO第一單元最簡單的一次作業了。

相比與第二次作業,第三次作業增加的內容有:

1.巢狀括號

2.三角函式因子巢狀

3.自定義函式巢狀呼叫自定義函式

4.求和函式的求和表示式因子種類增加

可以看出,第三次作業相比與第二次作業其實沒有增加什麼東西。與前兩次作業最大的不同就是第三次作業增加了對巢狀因子,巢狀括號的處理,而這也是筆者覺得第三次作業主要想考察的地方。然鵝,對於遞迴下降的解析方法來說,處理巢狀因子,巢狀括號本來就是自然而然的事情,因此從第二次作業到第三次作業,筆者幾乎不用改動,這也是筆者第一次體會到好的架構設計帶來的好處。

當然,幾乎不用改動並不是不需要改動。第三次作業由於需要實現自定義函式巢狀呼叫自定義函式,筆者在第二次作業簡單的字串替換變無法處理了。但解決的辦法也很簡單,對於自定義函式的呼叫複用遞迴下降的辦法,對每一個呼叫因子進行解析,再把解析完成的表示式代入函式表示式中,最後替換完因子後得到一個新的表示式,再對這個表示式進行解析。除此之外,沒有需要大改的地方了。

UML類圖

OO3

可以看到,第三次作業的UML圖與第二次作業的UML圖幾乎沒有變化,新增的DefinedLexer類繼承了Lexer類,重寫了next方法,實現對自定義函式的文法解析

程式碼規模

HW3static

對比於第二次作業的1251行程式碼,第二次作業總共為1529行程式碼,主要是增加了DefinedLexer類。其實這次實際的程式碼只有1287行,為什麼呢?因為一大堆失敗的優化程式碼被筆者註釋掉了(0.0)

複雜度分析

方法複雜度

Method CogC ev(G) iv(G) v(G)
BasicTerm.BasicTerm(PowFunc, Cos, Sin) 3 1 4 4
BasicTerm.deepClone() 0 1 1 1
BasicTerm.equals(Object) 4 3 4 6
BasicTerm.equalsOfCosList(ArrayList<Cos>) 10 6 4 7
BasicTerm.equalsOfPowerList(ArrayList<PowFunc>) 10 6 4 7
BasicTerm.equalsOfSinList(ArrayList<Sin>) 10 6 4 7
BasicTerm.getCosxList() 0 1 1 1
BasicTerm.getPowerList() 0 1 1 1
BasicTerm.getSinxList() 0 1 1 1
BasicTerm.hashCode() 0 1 1 1
BasicTerm.mul(BasicTerm) 0 1 1 1
BasicTerm.mulOfCos(ArrayList<Cos>) 15 2 6 7
BasicTerm.mulOfPower(ArrayList<PowFunc>) 15 2 6 7
BasicTerm.mulOfSin(ArrayList<Sin>) 15 2 6 7
ConstantNum.ConstantNum(String) 0 1 1 1
ConstantNum.equals(Object) 3 3 2 4
ConstantNum.getBasicTerm() 0 1 1 1
ConstantNum.getNum() 0 1 1 1
ConstantNum.hashCode() 0 1 1 1
ConstantNum.toString() 1 2 2 2
Cos.Cos(int, Factor) 0 1 1 1
Cos.addPow(int) 0 1 1 1
Cos.equals(Object) 5 3 4 6
Cos.getBasicTerm() 0 1 1 1
Cos.getFactor() 0 1 1 1
Cos.getPow() 0 1 1 1
Cos.hashCode() 0 1 1 1
Cos.simply() 6 2 5 5
Cos.toString() 21 8 13 14
DefinedFunc.DefinedFunc() 0 1 1 1
DefinedFunc.addDefinedFunc(String) 1 1 2 2
DefinedFunc.getBasicTerm() 0 1 1 1
DefinedFunc.getDefinedArgument(String) 1 1 2 2
DefinedFunc.useDefinedFunc(String) 1 1 2 2
DefinedLexer.DefinedLexer(String) 0 1 1 1
DefinedLexer.next() 13 2 11 12
Expr.Expr() 0 1 1 1
Expr.addTerm(Term, boolean) 0 1 1 1
Expr.cosPowerIsZero(ArrayList<Cos>) 3 3 2 3
Expr.equals(Object) 3 3 2 4
Expr.getBasicTerm() 0 1 1 1
Expr.getStrFunc(BasicTerm, BigInteger) 20 3 13 13
Expr.hashCode() 0 1 1 1
Expr.merge() 3 1 3 3
Expr.mergeByPow(int) 7 1 5 5
Expr.powFuncPowerIsZero(ArrayList<PowFunc>) 3 3 2 3
Expr.sinPowerIsZero(ArrayList<Sin>) 3 3 2 3
Expr.toString() 17 1 6 7
Lexer.Lexer(String) 0 1 1 1
Lexer.getDefinedFunc() 5 1 2 4
Lexer.getInput() 0 1 1 1
Lexer.getNum() 2 1 3 3
Lexer.getPos() 0 1 1 1
Lexer.getPowX() 5 1 4 4
Lexer.getSignedNum() 0 1 1 1
Lexer.getSum() 5 1 2 4
Lexer.isCos() 1 1 2 2
Lexer.isDefinedFunc() 1 1 2 2
Lexer.isPow() 1 1 2 2
Lexer.isSignedNum() 1 1 3 3
Lexer.isSin() 1 1 2 2
Lexer.isSum() 1 1 2 2
Lexer.moveBlank() 3 2 2 4
Lexer.next() 12 2 10 11
Lexer.peek() 0 1 1 1
Lexer.setPos(int) 0 1 1 1
Lexer.setToken(String) 0 1 1 1
Main.main(String[]) 1 1 2 2
Parser.Parser(Lexer) 0 1 1 1
Parser.addDefinedFunc(DefinedFunc) 0 1 1 1
Parser.parseCosFactor() 3 1 3 3
Parser.parseExpr() 8 1 6 6
Parser.parseExprFactor() 3 1 3 3
Parser.parseFactor() 10 8 8 8
Parser.parseSinFactor() 3 1 3 3
Parser.parseTerm() 4 1 4 4
PowFunc.PowFunc(String) 2 1 2 2
PowFunc.addPow(int) 0 1 1 1
PowFunc.equals(Object) 4 3 3 5
PowFunc.getBasicTerm() 0 1 1 1
PowFunc.getPow() 0 1 1 1
PowFunc.getVarType() 0 1 1 1
PowFunc.hashCode() 0 1 1 1
PowFunc.toString() 6 3 3 4
Simplification.simplyPow(HashMap<BasicTerm, BigInteger>) 1 1 2 2
Sin.Sin(int, Factor) 0 1 1 1
Sin.addPow(int) 0 1 1 1
Sin.equals(Object) 5 3 4 6
Sin.getBasicTerm() 0 1 1 1
Sin.getFactor() 0 1 1 1
Sin.getPow() 0 1 1 1
Sin.hashCode() 0 1 1 1
Sin.simpliy() 14 2 7 7
Sin.toString() 18 6 10 11
SumFunc.SumFunc() 0 1 1 1
SumFunc.getBasicTerm() 0 1 1 1
SumFunc.sumOfFunc(String) 2 2 2 3
Term.Term() 0 1 1 1
Term.addFactor(Factor) 0 1 1 1
Term.cloneMap(HashMap<BasicTerm, BigInteger>) 1 1 2 2
Term.equals(Object) 4 3 3 5
Term.getBasicTerm() 0 1 1 1
Term.hashCode() 0 1 1 1
Term.merge() 14 1 7 7
Term.setNegative(boolean) 0 1 1 1
Total 334.0 173.0 276.0 318.0
Average 3.1809523809523808 1.6476190476190475 2.6285714285714286 3.0285714285714285
分析

這一次作業的複雜度相較於前兩次均有所增加,從上表可以看到方法複雜度較高的有Sin.simpliy(),Sin.toString(),Cos.toString(),BasicTerm.equalsOfPowerList(ArrayList<PowFunc>),Expr.getStrFunc(BasicTerm, BigInteger),Expr.toString()等。

觀察上述方法,可以發現這些方法大多與輸出有關,其中cos和sin的toString方法更是iv(G)異常的大。

在重新閱讀筆者的程式碼後,筆者發現這是因為筆者在sin和cos的toString方法中大量使用了if-else來進行輸出的化簡,比如利用正規表示式匹配x**2,替換成x*x。

現在看來,似乎可以這些化簡處理抽象成一個類,專門交給這個類去處理,與cos和sin解耦。

類複雜度

Class OCavg OCmax WMC
BasicTerm 3.71 7 52
ConstantNum 1.5 3 9
Cos 2.78 11 25
DefinedFunc 1.6 2 8
DefinedLexer 6.5 12 13
Expr 3.5 11 42
Lexer 2.16 11 41
Main 2 2 2
Parser 3.5 8 28
PowFunc 1.75 4 14
Simplification 2 2 2
Sin 2.78 9 25
SumFunc 1.67 3 5
Term 2.12 7 17
Total 283.0
Average 2.6952380952380954 6.571428571428571 20.214285714285715
分析

如果你仔細地看了上面的表格,相信聰明的你不難發現有一個類的複雜度特別的大,就是DefinedFunc

DefinedLexer 6.51.6 122 138

這是為什麼呢?因為這個類是繼承了Lexer類用於解析自定義函式因子,自然需要用到Lexter類中的pos,token等成員變數。

但課程組要求我們每一個類的成員變數應該是private的,連protected都不能用,於是我只能為Lexter類實現了getpos,setpos,gettoken等方法。這看起來似乎不是一個好辦法,將Lexer的成員暴露了,Lexter本應該只對外部暴露他的狀態。只可惜我沒想到太好的辦法,只能出此下策。

同時BasicTerm,Expr,Term等類的複雜度也較高,原因在HW2已經闡述,這裡就不再贅述了。

優化策略

相比與第二次作業,這次作業支援了三角函式裡面新增表示式因子巢狀,因此在此基礎上可以做二倍角優化2*cos(x)*sin(x)=sin((2*x)),但經過上一次優化失敗的教訓後筆者也不敢再優化了,實在是太容易出bug了,於是也沒做二倍角優化。這次作業的優化和HW2相同。

聽說有人做了二倍角和平方和優化,並通過比較不同優化的長度,搜尋到最長的輸出,如果超時就及時熔斷。但筆者覺得除非特別厲害,否則這樣就是在刀尖上起舞(事實證明的確是這樣,詳見下文)。

整體架構分析

從第一次作業到第三次作業,我的程式緊緊圍繞著一個基本項展開,在第一次作業中他是儲存有指數和係數的HashMap<Integer, BigInteger>;第二、三次作業中,他是一個類BasicTerm。但這樣的設計我覺得並不好,因為並不利於迭代開發。一個基本項顯然只能涵蓋較為簡單的情況,比如三角函式、冪函式這樣的,一旦表示式中的函式因子多了起來,這個基本項也會變得極為龐大和複雜,顯然與OO的精神違背。

同時我的化簡運算merge()也是各自在term類和Expr類中實現的,term中是乘,Expr中是加。這樣我也感覺並不好,因為term現在只有乘的運算,但如果有取模和除法運算呢?想必我就要修改原來的term類了。而這種運算並不是term類應該具有的能力,這種運算應該交給“運算類”專門去做,而不是在term類中實現。

現在想來,一個好的設計應該是表示式中每一種因子,每一種運算,每一項,都應該成為一個類,即使需求變了,也只需要增添新的運算類和因子類即可,無需或者很少需要改動原來已經設計好的程式碼。

資料生成及自動化評測

在OO第一單元的每次作業中,筆者都使用Python及其xeger庫寫了資料生成器,並在第二次作業和第三次作業實現了自動化評測機

第一次作業

資料生成

根據指導書中的設定的形式化表述

  • 表示式 → 空白項 [加減 空白項] 項 空白項 | 表示式 加減 空白項 項 空白項
  • 項 → [加減 空白項] 因子 | 項 空白項 * 空白項 因子
  • 因子 → 變數因子 | 常數因子 | 表示式因子
  • 變數因子 → 冪函式
  • 常數因子 → 帶符號的整數

我們可以自底向上構造測試用例,首先構造因子中的變數因子常數因子,通過這兩個因子構造沒有表示式因子的項,再通過沒有表示式因子的項,構造沒有括號的表示式,通過把沒有括號的表示式加上括號,我們就得到了表示式因子,再用三種因子構造含有表示式因子的項,最後用含有表示式因子的項生成表示式

其中,變數因子常數因子的構造可以使用Python的xeger庫(根據正規表示式隨機生成字串)

再通過調節不同因子的生成概率和程度,可以得到不同強度的資料

缺點:對於邊界資料難以生成,可以考慮新增邊界資料常量池,在生成表示式時隨機新增

程式碼示例

展開程式碼
from xeger import Xeger
import random

powFun = 'x( ){0}(\*\*( ){0}\+0{0}[0-4])?'
const = '(\+|-)0{0}[1-9]{1}'

x = Xeger(limit=100)
x._cases['any'] = lambda x: '.'
x._alphabets['whitespace'] = ' '

def generTermNotExp(x,notpre):
    n = random.randint(1,1)
    m = random.randint(1,3)
    s = ""
    if not notpre:
        if m==1:
            s+="+"
        elif m==2:
            s+='-'
    for i in range(n):
        if random.random()>0.5:
            s+=x.xeger(powFun)
        else:
            s+=x.xeger(const)
        if i < n - 1:
            s+="*"
    return s


def generExpNotBrac(x,notpre):
    n = random.randint(1,1)
    m = random.randint(1,3)
    s = ""
    if not notpre:
        if m==1:
            s+="+"
        elif m==2:
            s+='-'
    s+=generTermNotExp(x, False)
    for i in range(n):
        if random.random()>0.5:
            s+="+"
        else:
            s+="-"
        s+=generTermNotExp(x,True)
    return s

def generTermWithBracket(x, notpre):
    n = random.randint(2,3)
    m = random.randint(1,3)
    s = ""
    if notpre:
        if m==1:
            s+="+"
        elif m==2:
            s+='-'
    for i in range(n):
        tmp = random.random()
        if tmp > 0.80:
            s+=x.xeger(powFun)
        elif 0.60 < tmp <= 0.80:
            s+=x.xeger(const)
        elif tmp<=0.60:
            s+="("+generExpNotBrac(x,False)+")"
            s+="**"+"+"+str(random.randint(1,2))
        if i < n - 1:
            s+="*"
    return s

def generExpWithBracket(x):
    n = random.randint(2,2)
    m = random.randint(1,3)
    s = ""
    if m==1:
        s+="+"
    elif m==2:
        s+='-'
    for i in range(n):
        s+=generTermWithBracket(x,True)
        if i < n - 1:
            if random.random()>0.9:
                s+="+"
            else:
                s+="-"
    return s

for i in range(100):
    s = generExpWithBracket(x)
    print(s)

自動化評測

本次作業未實現自動化評測?

第二次作業

資料生成

本次資料生成器根據第一次作業的資料生成器迭代開發,大體上思路不變。根據第二次作業的指導書新增的三角因子,自定義函式,求和函式的因子生成,其中自定義函式參考了討論區大佬的方法。

缺點:同作業一一樣,對於邊界資料難以生成,可以考慮新增邊界資料常量池,在生成表示式時隨機新增

程式碼示例
展開程式碼
from xeger import Xeger
import random

#powFun = 'x( ){0}(\*\*( ){0}\+0{0}[0-4])?'
powFun = 'x'
const = '(\+|-)0{0}[1-9]{1}'
power = '( ){0}(\*\*( ){0}\+0{0}[0-4])?'
# fun1,fun2,fun3分別是函式引數為1個,2個,3個的函式,而且函式名分別為f,g,h
list_fun1 = ["f(x)=x", "f(y)=sin(y)", "f(z)=z*z", "f(y)=y*y"]
list_fun2 = ["g(x,y)=x+y", "g(y,z)=sin(y)*cos(z)", "g(x,z)=x*z+x-z", "g(z,y)=y*y-z+2"]
list_fun3 = ["h(x,y,z)=x+y-z", "h(x,z,y)=sin(x)+y+cos(z)", "h(x,z,y)=x+y*(z-1)", "h(x,y,z)=y*y*x*(4-z)"]
list_fun = [list_fun1, list_fun2, list_fun3]

# 用於替換函式引數的變數,即若函式為f(y)=sin(y),則從list_var中隨機一個當作y填入函式
list_var = ["1", "2", "x", "x**2", "x**3", "x**0"]

fun_java = []  # 隨機出了哪些函式,記錄一下,java輸入時要用到
fun_record = []  # 函式名不能重複,也記錄一下
x = Xeger(limit=100)
x._cases['any'] = lambda x: '.'
x._alphabets['whitespace'] = ' '

def generate_function():
    std_java = ""
    p = random.randint(1, len(list_fun1))  # 型別(每個函式列表中的第幾個)
    num = random.randint(1, 3)  # 引數個數/函式名
    while num in fun_record:
        # 不能重複
        num = random.randint(1, 3)
        if len(fun_record) >= 3:
            break
    fun_record.append(num)      # 記錄用過的函式名

    fun_java.append((num, p))   # 記錄隨機出的函式
    std_java += list_fun[num - 1][p - 1].split("=")[0][0:2]
    # 假如函式為 g(x,y)=x+y ,按等於符號split,第0項是g(x,y),取 g(

    arr = []  # 記錄實際表示式中的引數列表
    for i in range(0, num):
        arr.append(list_var[random.randint(0, len(list_var) - 1)])  # 從引數列表隨機
        std_java += arr[i]
        if i < num - 1:
            std_java += ","
    std_java += ")"
    return std_java

def generSin(x):
    m = random.randint(1, 2)
    if m == 1:
        return "sin" + " " * random.randint(0, 0) + "(" + x.xeger(powFun) + ")" + x.xeger(power)
    else:
        return "sin" + " " * random.randint(0, 0) + "(" + x.xeger(const) + ")" + x.xeger(power)


def generCos(x):
    m = random.randint(1, 2)
    if m == 1:
        return "cos" + " " * random.randint(0, 0) + "(" + x.xeger(powFun) + ")" + x.xeger(power)
    else:
        return "cos" + " " * random.randint(0, 0) + "(" + x.xeger(const) + ")" + x.xeger(power)

def generSum(x):
    begin = random.randint(-10,10)
    end = begin+random.randint(-4,4)
    s = "sum"+"("+"i"+","+str(begin)+","+str(end)+","
    factor = "("+generExpFactorNotDefinedFuncAndSum(x,False)+")"
    factor.replace("x","i")
    s+=factor+")"
    return s

def generExpFactorNotDefinedFuncAndSum(x,notpre):
    n = random.randint(1, 1)
    m = random.randint(1,3)
    s = ""
    if not notpre:
        if m == 1:
            s += "+"
        elif m == 2:
            s += '-'
    s += generTermNotExpDefinedFuncAndSum(x, False)
    for i in range(n):
        if random.random() > 0.5:
            s += "+"
        else:
            s += "-"
        s += generTermNotExpDefinedFuncAndSum(x, True)
    return s

def generTermNotExpDefinedFuncAndSum(x, notpre):
    n = random.randint(1, 2)
    m = random.randint(1, 3)
    s = ""
    if not notpre:
        if m == 1:
            s += "+"
        elif m == 2:
            s += '-'
    for i in range(n):
        if random.random() > 0.8:
            s += x.xeger(powFun)
        elif 0.6 < random.random() <= 0.8:
            s += x.xeger(const)
        elif 0.3 < random.random() <= 0.6:
            s += generSin(x)
        else:
            s += generCos(x)
        if i < n - 1:
            s += "*"
    return s

def generTermNotExp(x, notpre):
    n = random.randint(1, 1)
    m = random.randint(1, 3)
    s = ""
    if not notpre:
        if m == 1:
            s += "+"
        elif m == 2:
            s += '-'
    for i in range(n):
        ran = random.random()
        if ran > 0.7:
            s += x.xeger(powFun)
        elif 0.6 < ran <= 0.7:
            s += x.xeger(const)
        elif 0.3 < ran <= 0.6:
            s += generSin(x)
        else:
            s += generCos(x)
        if i < n - 1:
            s += "*"
    return s


def generExpNotBrac(x, notpre):
    n = random.randint(1, 1)
    m = random.randint(1, 3)
    s = ""
    if not notpre:
        if m == 1:
            s += "+"
        elif m == 2:
            s += '-'
    s += generTermNotExp(x, False)
    for i in range(n):
        if random.random() > 0.5:
            s += "+"
        else:
            s += "-"
        s += generTermNotExp(x, True)
    return s


def generTermWithBracket(x, notpre):
    n = random.randint(3, 3)
    m = random.randint(1, 3)
    s = ""
    if notpre:
        if m == 1:
            s += "+"
        elif m == 2:
            s += '-'
    for i in range(n):
        tmp = random.random()
        if tmp > 0.90:
            s += x.xeger(powFun)
        elif 0.80 < tmp <= 0.90:
            s += x.xeger(const)
        elif 0.6 < tmp <= 0.8:
            s += generSin(x)
        elif 0.4 < tmp <= 0.6:
            s += generCos(x)
        elif tmp <= 0.4:
            s += "(" + generExpNotBrac(x, False) + ")"
            s += "**" + "+" + str(random.randint(1, 2))
        if i < n - 1:
            s += "*"
    return s


def generExpWithBracket(x):
    n = random.randint(3, 3)
    m = random.randint(1, 3)
    s = ""
    if m == 1:
        s += "+"
    elif m == 2:
        s += '-'
    for i in range(n):
        s += generTermWithBracket(x, True)
        if i < n - 1:
            if random.random() > 0.9:
                s += "+"
            else:
                s += "-"
    return s
print(0)
s = generExpWithBracket(x)
print(s)

自動化評測

在看了cnx大佬的評測思路和討論區的評測思路後,基於cnx大佬的程式碼實現了一個簡易的評測機。

利用Python的subprocess庫進行java程式的輸入輸出,利用eval函式對java程式的輸出進行代值運算,如果有錯誤,就將輸入資料和輸出結果輸入到檔案中

評測機輸入從資料生成器獲得

程式碼示例
展開程式碼 ```python import subprocess from math import * import os import data

def getinput():
s = data.generdata()
while len(s) > 200:
s = data.generdata()
return s

cmd_my = r'' # 其中cmd代表IDEA執行時的控制檯最上面的那一串路徑

cmd2 = r''

cmd3 = r''

cmd4 = r''

cmd6 = r''

cmd7 = r''

cmd8 = r''

testcmd2 = cmd2
testcmd3 = cmd3
testcmd4 = cmd4
testcmd5 = cmd5
testcmd6 = cmd6
testcmd7 = cmd7
testcmd8 = cmd8
for i in range(60000):
java_input = getinput()
print("----input----")
print(java_input)
print("-----------")

proc = subprocess.Popen(cmd_my, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                        encoding='gb2312')
stdout, stderr = proc.communicate(java_input)
out1 = stdout.split("\n")[1]
print(out1)

proc = subprocess.Popen(testcmd2, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                        encoding='gb2312')
stdout, stderr = proc.communicate(java_input)
out2 = stdout.split("\n")[1]
print(out2)

proc = subprocess.Popen(testcmd3, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                        encoding='gb2312')
stdout, stderr = proc.communicate(java_input)
out3 = stdout.split("\n")[1]
print(out3)

proc = subprocess.Popen(testcmd4, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                        encoding='gb2312')
stdout, stderr = proc.communicate(java_input)
out4 = stdout.split("\n")[1]
print(out4)

proc = subprocess.Popen(testcmd5, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                        encoding='gb2312')
stdout, stderr = proc.communicate(java_input)
out5 = stdout.split("\n")[1]
print(out5)

proc = subprocess.Popen(testcmd6, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                        encoding='gb2312')
stdout, stderr = proc.communicate(java_input)
out6 = stdout.split("\n")[1]
print(out6)

proc = subprocess.Popen(testcmd7, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                        encoding='gb2312')
stdout, stderr = proc.communicate(java_input)
out7 = stdout.split("\n")[1]
print(out7)

proc = subprocess.Popen(testcmd8, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                        encoding='gb2312')
stdout, stderr = proc.communicate(java_input)
out8 = stdout.split("\n")[1]
print(out8)

x = 2.25
r1 = eval(out1)
r2 = eval(out2)
r3 = eval(out3)
r4 = eval(out4)
r5 = eval(out5)
r6 = eval(out6)
r7 = eval(out7)
r8 = eval(out8)
print("my_out :", r1)
print("saber  :", r2)
print("rider  :", r3)
print("lancer  :", r4)
print("caster  :", r5)
print("Assassin  :", r6)
print("Archer  :", r7)
print("8_out  :", r8)
eps = 0.00000000001
if r1 == 0 and r2 == 0 and r3 == 0 and r5 == 0:
    print("-------AC---------")
elif r1 == 0 and (r2 != 0 or r3 != 0 or r5 != 0):
    print("-------WA---------")
else:
    if abs((r1 - r2) / r1) < eps and abs((r1 - r3) / r1) < eps and abs(
            (r1 - r5) / r1) < eps:
        print("-------AC---------")
    else:
        print("-------WA---------")
        with open("hackerror2.txt", 'a', encoding='utf-8') as f:
            f.write(java_input)
            f.write("\n")
            f.write(out1)
            f.write("\n")
            f.write(out2)
            f.write("\n")
            f.write(out3)
            f.write("\n")
            f.write(out5)
            f.write("\n")
            f.write("my_out :" + str(r1))
            f.write("\n")
            f.write("saber  :" + str(r2))
            f.write("\n")
            f.write("rider   :" + str(r3))
            f.write("\n")
            f.write("caster   :" + str(r5))
            f.write("\n")
            f.write("out6   :" + str(r6))
            f.write("\n")
            f.write("out7   :" + str(r7))
            f.write("\n")
            f.write("out8   :" + str(r8))
            f.write("\n\n\n")
print("\n\n")

</details>

### 第三次作業

#### 資料生成

第三次作業資料生成思路與第二次作業大體上相同,但是第三次作業支援三角因子內巢狀任意因子,以及括號巢狀,同時sum表示式裡不能有自定義函式和求和函式。

因此將三角函式因子分為有求和函式、自定義函式,和沒有求和函式自定義函式、求和函式兩種,sum因子生成時如果要用到三角函式則採用後者。

通過遞迴生成每一項時對項、因子、表示式新增括號生成巢狀括號。

自定義函式的遞迴呼叫通過常量池的方式手動新增。

##### 程式碼示例

<details>
    <summary>展開程式碼</summary>
```python
from xeger import Xeger
import random

# powFun = 'x( ){0}(\*\*( ){0}\+0{0}[0-4])?'
powFun = 'x'
const = '(\+|-)0{0}[0-9]{1}'
power = '( ){0}(\*\*( ){0}\+0{0,1}[2-3])?'

# fun1,fun2,fun3分別是函式引數為1個,2個,3個的函式,而且函式名分別為f,g,h
list_fun1 = ["f(x)=x", "f(y)=sin(y)", "f(z)=z*z", "f(y)=y*y", "f(z)=sin(cos(z))"]
list_fun2 = ["g(x,y)=x+y", "g(y,z)=sin(y)*cos(z)", "g(x,z)=x*z+x-z", "g(z,y)=y*y-z+2", "g(z,x)=cos((z*x))"]
list_fun3 = ["h(x,y,z)=x+y-z", "h(x,z,y)=sin(x)+y+cos(z)", "h(x,z,y)=x+y*(z-1)", "h(x,y,z)=y*y*x*(4-z)",
             "h(z,x,y)=sin(x)*y+x"]
list_fun = [list_fun1, list_fun2, list_fun3]

# 用於替換函式引數的變數,即若函式為f(y)=sin(y),則從list_var中隨機一個當作y填入函式
list_var = ["1", "2", "x", "x**2", "x**0", "f(g(x**1,cos(x)))", "g(f(0),g(x,1))", "h(1,1,1)", "0", "cos(-0)", "sin(-0)",
            "sum(i, -2, 1, i*cos(x))","sin(sin(sin(0)))","cos(sin(0))","cos(cos(0))","sum(i, 0, 2, i)","g(h(sum(i,-2,-1, i), f(cos(sin(x**1))),sum(i,2,2,(i*cos(i))) ),sum(i,-2,2,sin(i)) )"]

fun_java = []  # 隨機出了哪些函式,記錄一下,java輸入時要用到
fun_record = []  # 函式名不能重複,也記錄一下

x = Xeger(limit=100)
x._cases['any'] = lambda x: '.'
x._alphabets['whitespace'] = ' '


def generate_function():
    std_java = ""
    p = random.randint(1, len(list_fun1))  # 型別(每個函式列表中的第幾個)
    num = random.randint(1, 3)  # 引數個數/函式名
    while num in fun_record:
        # 不能重複
        num = random.randint(1, 3)
        if len(fun_record) >= 3:
            break
    fun_record.append(num)  # 記錄用過的函式名

    fun_java.append((num, p))  # 記錄隨機出的函式
    std_java += list_fun[num - 1][p - 1].split("=")[0][0:2]
    # 假如函式為 g(x,y)=x+y ,按等於符號split,第0項是g(x,y),取 g(

    arr = []  # 記錄實際表示式中的引數列表
    for i in range(0, num):
        arr.append(list_var[random.randint(0, len(list_var) - 1)])  # 從引數列表隨機
        std_java += arr[i]
        if i < num - 1:
            std_java += ","
    std_java += ")"
    return std_java


def generSin(x):
    return "sin" + " " * random.randint(0, 0) + "(" + gengerFactor(x) + ")" + x.xeger(power)


def generCos(x):
    return "cos" + " " * random.randint(0, 0) + "(" + gengerFactor(x) + ")" + x.xeger(power)


def generSinNotSumAndDefined(x):
    return "sin" + " " * random.randint(0, 0) + "(" + generFactorNotSumAndDefined(x) + ")" + x.xeger(power)


def generCosNotSumAndDefined(x):
    return "cos" + " " * random.randint(0, 0) + "(" + generFactorNotSumAndDefined(x) + ")" + x.xeger(power)


def generSum(x):
    begin = random.randint(-5, 5)
    end = begin + random.randint(-2, 2)
    s = "sum" + "(" + "i" + "," + str(begin) + "," + str(end) + ","
    factor = "(" + generExpFactorNotDefinedFuncAndSum(x, False) + ")"
    factor = factor.replace("x", "i")
    s += factor + ")"
    return s

def generExpFactorNotDefinedFuncAndSum(x, notpre):
    n = random.randint(1, 2)
    m = random.randint(1, 3)
    s = ""
    if not notpre:
        if m == 1:
            s += "+"
        elif m == 2:
            s += '-'
    s += generTermNotExpDefinedFuncAndSum(x, False)
    for i in range(n):
        if random.random() > 0.5:
            s += "+"
        else:
            s += "-"
        s += generTermNotExpDefinedFuncAndSum(x, True)
    return s


def generTermNotExpDefinedFuncAndSum(x, notpre):
    n = random.randint(1, 2)
    m = random.randint(1, 3)
    s = ""
    if not notpre:
        if m == 1:
            s += "+"
        elif m == 2:
            s += '-'
    for i in range(n):
        if random.random() > 0.8:
            s += x.xeger(powFun)
        elif 0.6 < random.random() <= 0.8:
            s += x.xeger(const)
        elif 0.3 < random.random() <= 0.6:
            s += generSinNotSumAndDefined(x)
        else:
            s += generCosNotSumAndDefined(x)
        if i < n - 1:
            s += "*"
    return s


def gengerFactor(x):
    s = ""
    ran = random.random()
    if ran > 0.8:
        s += x.xeger(powFun)
    elif 0.7 < ran <= 0.8:
        s += x.xeger(const)
    elif 0.5 < ran <= 0.7:
        s += generSin(x)
    elif 0.3 < ran <= 0.5:
        s += generCos(x)
    else:
        s += generSum(x)
    return s


def generFactorNotSumAndDefined(x):
    s = ""
    ran = random.random()
    if ran > 0.8:
        s += x.xeger(powFun)
    elif 0.6 < ran <= 0.8:
        s += x.xeger(const)
    elif 0.3 < ran <= 0.6:
        s += generSinNotSumAndDefined(x)
    else:
        s += generCosNotSumAndDefined(x)
    return s


def generTermNotExp(x, notpre):
    n = random.randint(1, 3)
    m = random.randint(1, 3)
    s = ""
    if not notpre:
        if m == 1:
            s += "+"
        elif m == 2:
            s += '-'
    for i in range(n):
        ran = random.random()
        if ran > 0.8:
            s += x.xeger(powFun)
        elif 0.7 < ran <= 0.8:
            s += x.xeger(const)
        elif 0.45 < ran <= 0.7:
            s += generSin(x)
        elif 0.2 < ran <= 0.45:
            s += generCos(x)
        else:
            s += generSum(x)
        if i < n - 1:
            s += "*"
    return s


def generExpNotBrac(x, notpre):
    n = random.randint(2, 3)
    m = random.randint(1, 3)
    s = ""
    if not notpre:
        if m == 1:
            s += "+"
        elif m == 2:
            s += '-'
    s += generTermNotExp(x, False)
    for i in range(n):
        bracket = random.random()
        if bracket > 0.0:
            s = "(" + s + ")"
        if random.random() > 0.5:
            s += "+"
        else:
            s += "-"
        s += generTermNotExp(x, True)
    return s


def generTermWithBracket(x, notpre):
    n = random.randint(1, 3)
    m = random.randint(1, 3)
    s = ""
    if notpre:
        if m == 1:
            s += "+"
        elif m == 2:
            s += '-'
    for i in range(n):
        tmp = random.random()
        if tmp > 0.85:
            s += x.xeger(powFun)
        elif 0.80 < tmp <= 0.85:
            s += x.xeger(const)
        elif 0.6 < tmp <= 0.8:
            s += generSin(x)
        elif 0.4 < tmp <= 0.6:
            s += generCos(x)
        elif tmp <= 0.4:
            s += "(" + generExpNotBrac(x, False) + ")"
            s += "**" + "+" + str(random.randint(1, 2))
        if i < n - 1:
            s += "*"
    return s


def generExpWithBracket(x):
    n = random.randint(2, 3)
    m = random.randint(1, 3)
    s = ""
    if m == 1:
        s += "+"
    elif m == 2:
        s += '-'
    for i in range(n):
        s += generTermWithBracket(x, True)
        if i < n - 1:
            if random.random() > 0.5:
                s += "+"
            else:
                s += "-"
    return s


"""
print(3)
print(random.choice(list_fun1))
print(random.choice(list_fun2))
print(random.choice(list_fun3))
s = generExpWithBracket(x)
print(s)
"""


def generdata():
    global x
    s = "0\n" + generExpWithBracket(x)
    return s

自動化評測

本次測評機與HW2相同,故不再贅述

自我程式bug分析及測試手段

自我程式bug分析

第一次作業

在寫完程式碼進行自我測試時主要的bug是lexer解析的問題,忽略了一些符號的優先順序

本次作業強測和互測都未被找出bug

第二次作業

這次作業的bug令我☹️

bug出在化簡,將cos(0)化成了0,出現這個bug的原因是因為cos的化簡程式碼是我從sin那複製過來的,而sin(0)=0,忘記改了?

導致強測WA3個點互測被hack一次

痛,太痛了

這也再一次提醒了我,永遠不要複製程式碼!!!!!!!!!!

第三次作業

本次作業強測和互測均未被找出bug

測試手段

第一次作業

由於未編寫評測機,故只能手工進行對拍。

步驟:

1.將程式輸入改為迴圈

2.從資料生成器複製輸出的資料

3.將資料輸入自己的程式

4.將資料輸入到他人程式

5.將他人程式輸出輸入到自己程式

6.使用文字差異比較器對比第3步和第4步的輸出

可以看出,沒有評測機下想要對拍是極為困難且麻煩的,這也堅定了當時我要搭建評測機的決心

第二次作業

搭建了評測姬以後第二次作業測試較為順利,只需要利用評測機和資料生成器自動化評測即可。筆者利用評測機幫助同學測出了幾個bug,十分有成就感。

但是,由於一系列歷史原因,筆者的資料生成器竟然忽略了0的生成,導致無法生成cos(0)這樣的資料,也是筆者第二次作業翻車的根本原因。同時也說明了不能過度依賴評測機,還是有必要重複閱讀自己的程式碼的。

第三次作業

和第二次作業測試過程基本相同,同樣也幫助幾位同學找出了程式的bug,成就感滿滿~

他人程式bug分析

第一次作業

本次作業找到同房間兩個bug

1.第一個人的bug是-1會只輸出個-號

2.第二個人的bug是5161*x會輸出516*x,推測是正規表示式出的鍋

第二次作業

本次作業找到同房間4個bug

1.第一個bug是-cos(2)*cos(x)-cos(x)會輸出-2*cos(x),推測是三角函式相乘沒有處理好

2.第二個bug是sin(-1111111111111111111111)會爆int,看了程式碼以後發現這位同學hashcode使用了parseInt來解析BigInter

3.第三個bug是(cos(x)**2+sin(x)**2)**2會輸出cos(x)**3+cos(x)**2*sin(x)+sin(x)**2*cos(x)+sin(x)**3,原因應該是平方和優化出了問題,這也說明一定要在保證正確性的基礎上進行優化

4.第四個bug是x**11輸出x,推測是這位同學沒有仔細閱讀指導書,x**10以上的指數沒有處理

第三次作業

本次作業找到同房間個bug

1.第一個bug是sin((-x))**2輸出sin(-x)**2,推測是沒有處理好三角函式內因子的輸出,導致WF

2.第二個bug是+3+-cos(sum(i,2,1,0))**+2會輸出4,推測是平方和和二倍角優化出現的問題,這再一次提醒我們,一定要在保證正確性的基礎上進行優化。在這裡,不得不提一下,這位同學的優化出現了相當多的問題,導致我hack了三次以後就不敢再hack了。測評機每跑十幾條資料就會WA,但這樣仍能進入A房,由此可見強測的資料不一定強。

3.第三個bug是cos(sin(cos(sin(cos(sin(cos(sin(cos(x))))))))),有第二個bug的同學的程式在輸入這條資料後會直接TLE,可能是優化做得太狠,並且沒有及時熔斷。

4.第四個bug是-sin(x)**+2+-sin(x)*cos(x)**+2++cos(x)*x會輸出x*cos(x)-sin(x),推測也是三角函式乘三角函式合併時沒有處理好

5.第五個bug是sum(i,0,0,(-i+cos(sin(sin(-9)))*cos(i)-cos(-5)))會輸出cos((0))*cos(sin(sin((-9))))0-cos((-5)),可以發現中間輸出多了個0,看了程式碼以後發現應該是sum處理時候出現的問題

6.第六個bug是sum(i,-1,1,i**2)會輸出0,觀察程式碼後發現是沒有處理好i作為冪函式自變數的情況

7.第七個bug是sum(i,123456789123456780,123456789123456789,i),觀察程式碼後發現是sum的上下界使用了int儲存,因此遇到大資料會爆int。同時發現有些人上下界是拿long存的,可惜課程組互測限制太死,無法構造出爆long的資料

hack別人程式bug策略

第一次作業

由於未編寫評測機,故只能根據資料生成器採用測試手段中第一次作業的測試方法,手動構造了一些邊界資料。成功hack到兩個人。

第二次作業

由於搭建了評測機,我的策略是檢視他人程式碼,看看有沒有地方會爆int什麼的,然後先手動輸入一些可能有bug的邊界資料,測試完邊界資料以後交給評測機自動化評測(高工能留給OO的時間真的不多),然後去幹別的事情(實現並行),隔一段時間看看檔案是否有WA輸出

第三次作業

hack方法同第二次作業

hack策略分析

雖然通過閱讀他人程式碼+評測機隨機轟炸能覆蓋到絕大多數點,但仍有一些邊界資料是難以被構造出來的,並且評測機生成的資料有時過於複雜,不利於hack輸入的提交,簡而言之,就是難以構造強而簡單的資料。

這樣會導致雖然跑了很多組資料,但是仍有bug的情況發生。可以說,評測機只是幫助測試的一種手段,並不能完全覆蓋所有情況,不能嚴格證明程式碼的正確性。

心得體會

在OO第一單元的學習中,我有如下幾個體會和收穫

  • 初步學習了遞迴下降演算法並運用在表示式解析中,再一次領略到了遞迴的美妙
  • 通過閱讀字數頗多且沒有廢話的指導書,我提升了快速從一份篇幅較長的資料中獲取有用資訊及需求的能力
  • 通過指導書,接觸到了形式化語言
  • 第一次利用python的xeger和subprocess模組寫出了屬於自己的資料生成器和評測姬,實現了上學期計組沒有實現的夢想
  • 學會了使用IDEA及其外掛,並利用checkstyle使程式碼風格變得更好
  • 瞭解到了黑盒測試、白盒測試、迴歸測試等測試方法
  • 體會到了好的架構的重要性
  • 理解到了重構的必要性和學會了應該如何進行重構
  • 認識了許多6系的同學,收穫了跨院系的友誼

從作業難度上來看,我認為第一次作業≈第二次作業>>第三次作業,因為第一次作業是從無到有的過程,需要學習遞迴下降演算法且完整地把程式碼寫出來。而第二次作業雖然有了第一次作業的架構,但因為第一次作業的架構有部分不適合第二次作業,因此需要進行重構,因此也有一定難度。而第三次作業就是福利大放送了。

從物件導向的思維來看,雖然在設計過程中我努力地讓自己“物件導向”,可是寫出來的程式碼還是處處“程式導向”。物件導向是一種思維,但有了這種思維將其實現還是有一定難度的,更不要說我還沒有這種思維

而且設計模式我也不瞭解多少,只知道裝飾者模式、觀察者模式、策略模式、工廠模式啥的,而且這些模式也是我開學以後抽空看的,想在作業中運用這些模式的時候架構已經成型,不敢隨便亂改了,令人有點遺憾。因此我建議課程組可以在pre的時候提供給同學們一些資料或者書籍,幫助同學們瞭解一些設計模式,以便在第一單元更好地運用。雖然設計模式不是看了就會的,但我覺得看了心裡有個譜,起碼比啥都不瞭解就開始寫物件導向的程式碼還是要強一點的

鳴謝

感謝wzm同學和我討論架構,以及分享測試資料

感謝cnx提供的評測機思路

感謝為我提供程式進行對拍的同學

相關文章