BUAA-OO-2022-Unit1 部落格總結
BUAA-OO-2022-Unit1 部落格總結
本單元的任務為表示式化簡,經過3次作業迭代後支援常數、冪函式、三角函式、求和函式、自定義函式、括號巢狀以及函式巢狀。
架構分析
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Lexer.Lexer(String) | 0 | 1 | 1 | 1 |
Lexer.Lexer(String, HashMap<String, HashMap<Character, Integer>>) | 0 | 1 | 1 | 1 |
Lexer.getCurToken() | 0 | 1 | 1 | 1 |
Lexer.getDefinedFunc() | 18 | 5 | 13 | 13 |
Lexer.getDefinedFuncs() | 0 | 1 | 1 | 1 |
Lexer.getEndPos() | 6 | 1 | 4 | 6 |
Lexer.getNextToken() | 0 | 1 | 1 | 1 |
Lexer.getNum() | 4 | 2 | 3 | 3 |
Lexer.getPos() | 0 | 1 | 1 | 1 |
Lexer.getPowerFunc() | 7 | 3 | 4 | 4 |
Lexer.getSumFunc() | 4 | 2 | 3 | 3 |
Lexer.getTrigonoFunc() | 16 | 2 | 8 | 8 |
Lexer.getTrigonoFuncFactor(String) | 15 | 7 | 8 | 11 |
Lexer.next() | 1 | 1 | 2 | 2 |
Lexer.toString() | 0 | 1 | 1 | 1 |
MainClass.main(String[]) | 1 | 1 | 2 | 2 |
Parser.Parser(Lexer) | 0 | 1 | 1 | 1 |
Parser.Parser(Lexer, HashMap<String, HashMap<Character, Integer>>) | 0 | 1 | 1 | 1 |
Parser.getTermSign() | 4 | 2 | 3 | 3 |
Parser.parseExpression() | 2 | 1 | 3 | 3 |
Parser.parseFactor() | 21 | 7 | 8 | 11 |
Parser.parseTerm() | 2 | 1 | 3 | 3 |
Pretreater.Pretreater(String) | 0 | 1 | 1 | 1 |
Pretreater.Pretreater(String, HashSet |
0 | 1 | 1 | 1 |
Pretreater.deleteBlank() | 0 | 1 | 1 | 1 |
Pretreater.deleteRedundantSign() | 0 | 1 | 1 | 1 |
Pretreater.getDefinedFuncs() | 10 | 1 | 5 | 5 |
Pretreater.pretreat() | 0 | 1 | 1 | 1 |
Pretreater.toString() | 0 | 1 | 1 | 1 |
Printer.Printer(Expr) | 0 | 1 | 1 | 1 |
Printer.addFactorString(StringBuilder, Factor) | 8 | 1 | 7 | 7 |
Printer.addNumString(StringBuilder, Num) | 0 | 1 | 1 | 1 |
Printer.addPostiveTermString(StringBuilder, HashMap<HashSet |
3 | 3 | 3 | 3 |
Printer.addPowerFuncString(StringBuilder, PowerFunc) | 7 | 1 | 4 | 4 |
Printer.addTermCoefficient(StringBuilder, HashSet |
30 | 4 | 12 | 13 |
Printer.addTermString(StringBuilder, HashSet |
13 | 1 | 8 | 9 |
Printer.addTrigonoFuncString(StringBuilder, TrigonoFunc) | 5 | 1 | 5 | 6 |
Printer.cloneMap(HashMap<HashSet |
1 | 1 | 2 | 2 |
Printer.cloneSet(HashSet |
1 | 1 | 2 | 2 |
Printer.getEndPos(String) | 6 | 1 | 4 | 6 |
Printer.getExpr() | 0 | 1 | 1 | 1 |
Printer.getExprString() | 5 | 2 | 4 | 4 |
Printer.getSimplifyExprString(String) | 0 | 1 | 1 | 1 |
Simplifier.Simplifier(Expr) | 0 | 1 | 1 | 1 |
Simplifier.addFactor(HashSet |
8 | 1 | 5 | 5 |
Simplifier.cloneSet(HashSet |
1 | 1 | 2 | 2 |
Simplifier.compareOtherFactor(HashSet |
29 | 5 | 10 | 14 |
Simplifier.compareSet(HashSet |
14 | 4 | 4 | 6 |
Simplifier.deleteRedundantTrigonoFunc(HashMap<HashSet |
7 | 1 | 5 | 5 |
Simplifier.getSimplifiedExpr() | 0 | 1 | 1 | 1 |
Simplifier.getTrigonoFuncIndex(HashSet |
4 | 3 | 5 | 5 |
Simplifier.mergeTrigonoFunc(HashMap<HashSet |
26 | 1 | 12 | 12 |
Simplifier.symplifyIndexZeroTrigonoFunc(HashMap<HashSet |
14 | 1 | 8 | 8 |
Simplifier.symplifyTerms(HashMap<HashSet |
0 | 1 | 1 | 1 |
Simplifier.symplifyTrigonoFunc(HashMap<HashSet |
39 | 10 | 10 | 13 |
Simplifier.symplifyTrigonoFuncNumFactor(HashMap<HashSet |
13 | 1 | 8 | 8 |
Simplifier.symplifyTrigonoZero(HashMap<HashSet |
15 | 3 | 9 | 11 |
expression.DefinedFunc.DefinedFunc(String, String, String, String, HashMap<String, HashMap<Character, Integer>>) | 3 | 1 | 3 | 3 |
expression.DefinedFunc.clone() | 0 | 1 | 1 | 1 |
expression.DefinedFunc.equals(Factor) | 0 | 1 | 1 | 1 |
expression.DefinedFunc.getExpreesionString() | 23 | 3 | 11 | 11 |
expression.Expr.Expr() | 0 | 1 | 1 | 1 |
expression.Expr.Expr(HashMap<HashSet |
0 | 1 | 1 | 1 |
expression.Expr.addTerm(Term) | 31 | 5 | 10 | 10 |
expression.Expr.clone() | 0 | 1 | 1 | 1 |
expression.Expr.cloneExpr(HashMap<HashSet |
3 | 1 | 3 | 3 |
expression.Expr.compareHashSet(HashSet |
14 | 6 | 4 | 6 |
expression.Expr.equals(Factor) | 22 | 5 | 6 | 8 |
expression.Expr.getTerms() | 0 | 1 | 1 | 1 |
expression.Expr.mergeHashSet(HashSet |
44 | 1 | 14 | 14 |
expression.Expr.multiplyExpr(HashMap<HashSet |
13 | 5 | 6 | 6 |
expression.Expr.multiplySelf(BigInteger) | 4 | 1 | 3 | 3 |
expression.Expr.toString() | 5 | 2 | 4 | 4 |
expression.Num.Num(BigInteger) | 0 | 1 | 1 | 1 |
expression.Num.clone() | 0 | 1 | 1 | 1 |
expression.Num.equals(Factor) | 1 | 1 | 2 | 2 |
expression.Num.getCoefficient() | 0 | 1 | 1 | 1 |
expression.Num.toString() | 0 | 1 | 1 | 1 |
expression.PowerFunc.PowerFunc(String, BigInteger) | 0 | 1 | 1 | 1 |
expression.PowerFunc.clone() | 0 | 1 | 1 | 1 |
expression.PowerFunc.equals(Factor) | 1 | 1 | 3 | 3 |
expression.PowerFunc.getIndex() | 0 | 1 | 1 | 1 |
expression.PowerFunc.getVariable() | 0 | 1 | 1 | 1 |
expression.PowerFunc.toString() | 0 | 1 | 1 | 1 |
expression.SumFunc.SumFunc(String, BigInteger, BigInteger, String) | 0 | 1 | 1 | 1 |
expression.SumFunc.clone() | 0 | 1 | 1 | 1 |
expression.SumFunc.equals(Factor) | 0 | 1 | 1 | 1 |
expression.SumFunc.getExpreesionString() | 13 | 2 | 9 | 9 |
expression.SumFunc.getFactorString() | 0 | 1 | 1 | 1 |
expression.SumFunc.getLoopVariable() | 0 | 1 | 1 | 1 |
expression.SumFunc.getLowerLimit() | 0 | 1 | 1 | 1 |
expression.SumFunc.getUpperLimit() | 0 | 1 | 1 | 1 |
expression.Term.Term(String) | 0 | 1 | 1 | 1 |
expression.Term.addExpr(Expr) | 20 | 6 | 7 | 7 |
expression.Term.addFactor(Factor) | 4 | 1 | 5 | 5 |
expression.Term.addNum(Num) | 2 | 1 | 2 | 2 |
expression.Term.addPowerFunc(PowerFunc) | 12 | 1 | 5 | 5 |
expression.Term.addTrigonoFunc(TrigonoFunc) | 22 | 1 | 9 | 9 |
expression.Term.compareHashSet(HashSet |
14 | 6 | 4 | 6 |
expression.Term.getFactors() | 0 | 1 | 1 | 1 |
expression.Term.getSign() | 0 | 1 | 1 | 1 |
expression.Term.mergeHashSet(HashSet |
44 | 1 | 14 | 14 |
expression.TrigonoFunc.TrigonoFunc(String, Factor, BigInteger) | 0 | 1 | 1 | 1 |
expression.TrigonoFunc.clone() | 0 | 1 | 1 | 1 |
expression.TrigonoFunc.equals(Factor) | 1 | 1 | 4 | 4 |
expression.TrigonoFunc.getFactor() | 0 | 1 | 1 | 1 |
expression.TrigonoFunc.getIndex() | 0 | 1 | 1 | 1 |
expression.TrigonoFunc.getTrigonoType() | 0 | 1 | 1 | 1 |
expression.TrigonoFunc.toString() | 0 | 1 | 1 | 1 |
Class | OCavg | OCmax | WMC | |
Lexer | 3.27 | 10 | 49 | |
MainClass | 2 | 2 | 2 | |
Parser | 3.17 | 10 | 19 | |
Pretreater | 1.57 | 5 | 11 | |
Printer | 3.71 | 12 | 52 | |
Simplifier | 5.36 | 11 | 75 | |
expression.DefinedFunc | 3 | 7 | 12 | |
expression.Expr | 4.67 | 13 | 56 | |
expression.Num | 1 | 1 | 5 | |
expression.PowerFunc | 1 | 1 | 6 | |
expression.SumFunc | 1.62 | 6 | 13 | |
expression.Term | 4.9 | 13 | 49 | |
expression.TrigonoFunc | 1 | 1 | 7 | |
Package | v(G)avg | v(G)tot | ||
4.28 | 244 | |||
expression | 3.17 | 165 | ||
Module | v(G)avg | v(G)tot | ||
Unit-1 | 3.75 | 409 | ||
Project | v(G)avg | v(G)tot | ||
project | 3.75 | 409 |
對於面向物件程式設計,筆者認為有兩點至關重要,一是事先對專案架構的分析與設計,二是實現過程中對實現難度與拓展性的權衡。一個沒有經過精心設計的架構,只能在早期應付相對簡單的任務,當功能需求增加、功能複雜度提高時,迭代開發會使專案拓展性不斷減低,導致後續功能的新增變得尤為困難;而不同模組間的耦合度很可能也會同時增加,進而破壞封裝原則,使得專案除錯難度陡增,更容易產生設計上的疏忽。
相反,在面對冗雜需求時,良好的設計模式與架構可以幫助程式設計人員將複雜問題縮小,逐個解決。並且設計模組之間互不影響,彼此僅通過介面聯絡,而不需要關注對方的實現細節。因此當一個模組出現問題時,我們僅需要修改其內部實現,而不需要考慮該模組的修改對其他模組的功能影響。
所謂實現難度與拓展性的權衡,就是我們在新增簡單功能與bug修復時作出的選擇,有時因為實現簡單,我們會採取一種“打補丁”的方式對程式或專案進行開發或修復,但如果“補丁”越來越多,可能會導致專案拓展性的嚴重下降。因此在理想情況下,我們希望無論是功能新增還是$bug$修復,都能夠優雅地進行,儘可能維持專案的拓展性。但現實情況是,有時針對簡單問題的強行解耦略顯突兀,可能帶來開發難度的大幅提升,因此在這其中我們就需要作出權衡,後文會體現這一點。
輸入
在本單元中,筆者對待解析表示式從輸入到輸出的各個環節進行了解耦,建立了Pretreater
、Lexer
、Parser
、Simplifier
、Printer
等多個類,各司其責。而由於本單元輸入較為簡單,筆者選擇直接在MainClass
主類中進行,將輸入得到的自定義函式字串與待解析表示式字串傳遞給Pretreater
類,其中自定義函式字串可以儲存在一個ArrayList
結構中。但通過課上老師的講解,我認為針對輸入還是應該建立一個Input
類,這是因為目前我們僅是從標準控制檯輸入,而如果以後有從檔案輸入的需求,我們僅需要修改Input
類而不需要對主類作出修改。
本模組僅需要以任意方式得到輸入的字串並傳遞給預處理模組,而不需要任何的處理、解析行為。
預處理
由於輸入的字串目前未經過解析模組,因此我們的預處理只能基於某些規則和規律進行,而無法在語義層面對字串進行處理。
通過觀察,筆者認為可以做如下預處理:
待解析表示式
處理順序有時會對正確性造成影響,後文優化部分有相應例子。
-
去除表示式中所有的空白符與製表符
this.expr = this.expr.replaceAll("[ \t]", "");
-
將連續的加號化簡為一個加號
this.expr = this.expr.replaceAll("\\+\\+", "+");
-
將連續的加號和減號化簡為一個減號
this.expr = this.expr.replaceAll("(\\+)?-(\\+)?", "-");
-
將連續的減號化簡為一個加號
this.expression = this.expression.replaceAll("--", "+");
-
將連續的星號和加號化簡為一個星號
this.expression = this.expression.replaceAll("\\*\\+", "*");
這一步同時去除了乘法運算與乘方運算中的多餘符號。
-
如果字串第一個字元是加號,則替換為空字元
this.expression = this.expression.replaceAll("^\\+", "");
自定義函式
該模組我們需要將輸入類讀入的函式定義字串進行預處理,去除其中的空白符與製表符,然後將相應資訊儲存進某種利於後續函式呼叫的資料結構,筆者選擇的資料結構是HashMap<String, HashMap<Character, Integer>>
,外層HashMap
通過經預處理的函式定義字串進行索引,而內層HashMap
記錄函式定義時形參相應的位置。
比如
f(y,x)=x+2*y
處理後得到的<String,HashMap<Character,Integer>>
為<f(y,x)=x+2*y, {<y,1>, <x,2>}>
。
解析
該模組是我們要考慮的重中之重,也是我們真正開始解析表示式的第一個階段,筆者通過分別建立Lexer
類和Parser
解決。在第一次作業中,由於限制括號巢狀,所以可能可以通過正則表示式實現解析,但筆者並沒有嘗試,這是由於先前提到的事先對專案架構的分析與設計,注意到如果後期需要解決巢狀括號,那麼筆者在後續開發大概率需要進行重構,因此筆者在第一次作業便採取了一種名為遞迴下降
的方法來對錶達式進行解析,後期證明其大有裨益。
遞迴下降
以筆者粗淺的理解,遞迴下降的核心在於將多層次問題轉化為單層問題,這降低了我們考慮問題的難度。並且遞迴下降提高了專案魯棒性,這是由於我們只要正確地解決了單層問題,那麼就可以確信我們能夠正確地解決巢狀的多層次問題,即使這個表示式是由很多層複雜的因子與項組合起來的。
具體來說,站在表示式
這個層次來觀察表示式
,它可能由多個+
和-
進行連線,我們將+
和-
連線的部分稱為項
;站在項
這個層次來觀察項
,它可能由多個*
進行連線,我們將*
連線的部分稱為因子
;那麼因子
可能為什麼?在第一次作業中,因子可能為帶符號的整數
、變元的冪
以及由括號包裹的表示式
;在第二次與第三次作業中,因子除了上述情況,還可能為三角函式
、求和函式
以及自定義函式
,我們稍後依次進行分析。
因子介面
為後續在處理時可以統一管理因子類,筆者選擇事先宣告Factor
介面,並要求所有實現Factor
的類複寫public boolean equals(Factor other)
、public Factor clone()
以及public String toString()
方法。
因子類
老師曾在課上提到,類
可以看作是我們作出的一種約定,而通過new
得到的物件
,則是真正儲存在記憶體中的資料。對於外部來說,物件的屬性並不重要,因此為了契合面向物件的封裝思想,課程組要求我們在現階段將所有類的屬性定義為private
型別,內部與外部通過public
型別的方法進行通訊,方法返回的是這個型別的狀態
,物件的狀態可以反映物件的屬性,但不能說物件的屬性決定物件的狀態。這就是說,我們在訪問一個物件的時候,不需要考慮其內部真正的實現方式以及其對屬性的儲存的儲存方式,我們只需要關注其返回給外部的狀態。因此在介紹儲存
模組之前,筆者僅說明該類需要實現什麼功能,返回什麼狀態,而暫時不關注類內部的實現方式。
Num
該類需要能夠刻畫一個帶符號的整數
,因此可以定義方法public BigInteger getCoefficient()
。
PowerFunc
該類需要能夠刻畫一個變元的冪
,因此可以定義方法public String getVariable()
和public BigInteger getIndex()
,在本單元任務中,變元被限定為x
。
TrigonoFunc
該類需要能夠刻畫一個三角函式
,三角函式內部為一個因子
,因此可以定義方法public String getTrigonoType()
、public Factor getFactor()
、public BigInteger getIndex()
,在本單元任務中,三角函式的種類被限定為sin
和cos
。
Expr
該類既可以視為因子類
,也可以視為頂層類
,是我們遞迴下降的難點。它需要能夠等價刻畫一個複雜的表示式,並利於索引,所以我們需要設計某種資料結構,筆者選擇的資料結構是HashMap<HashSet<Factor>,BigInteger>
,原因將在 儲存
模組詳細介紹,我們暫時先在該類定義方法public HashMap<HashSet<Factor>, BigInteger> getTerms()
。
SumFunc
該類需要能夠刻畫一個求和函式
,我們注意到SumFunc
展開後可以得到Expr
,然後複用解析Expr
的方法解析展開後的SumFunc
,因此我們可以定義方法public String getLoopVariable()
、public BigInteger getLowerLimit()
、public BigInteger getUpperLimit()
、public String getFactorString()
,在本單元任務中,迴圈變數
被限定為i
。而事實上,外部更需要的是展開後的SumFunc
,所以我們還需要定義方法public String getExpreesionString()
,或者直接複寫public String toString()
。
那麼如何得到展開後的SumFunc
呢,我們只需要通過一個迴圈,在每次迴圈中將求和因子
中的迴圈變數
附加括號後進行替換,然後對求和因子
累加即可。
事後筆者認為,我們完全可以將
i
視為與x
類同的變元
,而最初設計的因子類
和Expr資料結構
並不需要作出調整,所以我們可以不採取用String
型別儲存求和因子
這樣略顯刻意的方法,直接複寫因子類
的public String toString()
方法即可。
DefinedFunc
該類需要能夠刻畫一個自定義函式
,筆者希望繼續採取類似處理SumFunc
的思路,即只需要對外返回等價代入後的Expr
字串,再複用Expr
的解析方法。而由於我們選擇將代入的工作放入該類內部進行,所以每次生成物件時,需要將記錄有函式定義的資料傳入這個物件。我們在該類定義方法public String getExpreesionString()
,其實現原理是根據自定義函式的種類選擇對應的函式定義字串,然後從第一個字元開始進行遍歷,當遇到形參x
、y
或z
時則替換為相應位置的實參,同時可以在實參兩端附加一對括號。
項類
由於巢狀括號的存在,Term
類需要具有與Expr
類相同的資料結構,即使用HashMap<HashSet<Factor>,BigInteger>
來表示內部的資料。另外,筆者為Term
類定義了sign
屬性來表示物件的正負,方便Expr
類合併Term
物件。Term
類最主要的方法是public void addFactor(Factor factor)
,而為了將方法解耦,筆者選擇僅在該類中判斷Factor
例項的種類,然後選擇對應的add
方法:
public void addFactor(Factor factor) {
if (factor instanceof Num) {
addNum((Num) factor);
} else if (factor instanceof PowerFunc) {
addPowerFunc((PowerFunc) factor);
} else if (factor instanceof TrigonoFunc) {
addTrigonoFunc((TrigonoFunc) factor);
} else if (factor instanceof Expr) {
addExpr((Expr) factor);
}
}
由於在筆者的實現中,內層HashSet<Factor>
的元素僅可能為變數的冪
以及三角函式
,故筆者選擇在第一次addFactor
時預設事先在外層HashMap
中放入一個HashSet
,其中包含一個次數為0
的變數,比如x**0
,確保外層HashMap
中元素個數不為0。不然,如果第一次add``Factor
因子種類為Num
,處理起來稍有麻煩。
-
public void addNum(Num num)
:外層HashMap
中預設存在元素,故我們只需要更新每一個key
對應的value
。 -
public void addPowerFunc(PowerFunc powerFunc)
:內層HashSet
預設包含變數的冪
,我們只需要更其指數。 -
public void addTrigonoFunc(TrigonoFunc trigonoFunc)
:我們需要遍歷每一個內層HashSet
,如果發現存在相同種類的三角函式
,則更新其指數,否則將trigonoFunc
直接放入。 -
public void addExpr(Expr expr)
:我們同樣需要遍歷每一個內層HashSet
,實現類多項式的相乘
,對於第一次addFactor
因子種類為Expr
的情況特判即可。
由於筆者選擇將
SumFunc
以及DefinedFunc
展開為Expr
後再進行解析,故我們只需要定義addExpr(Expr expr)
方法即可處理這兩種函式。
詞法分析器實現
該類主要負責進行詞法分析,直觀上講可以將其視為一個遊標,不斷移動來分析待解析表示式的每一個字元,因此需要實現public void next()
、public char getCurToken()
、public char getNextToken()
以及public int getPos()
等方法,利於對外描述目前遊標所指向的位置以及該位置的字元。同時,該類需要從待解析表示式中獲取例項化因子類資訊,因此筆者在該類實現了public Num getNum()
、public PowerFunc getPowerFunc()
、public TrigonoFunc getTrigonoFunc()
、public SumFunc getSumFunc()
以及public DefinedFunc getDefinedFunc()
等方法來處理不同種類的因子,方法的呼叫受Parser
模組的控制。
由於
三角函式
內部包含一個因子
,所以筆者同時在Lexer
內部設計public Factor getTrigonoFuncFactor(String s)
方法,在通過public TrigonoFunc getTrigonoFunc()
獲取字串形式
的因子
後,可以通過呼叫該方法例項化對應種類的因子並返回。該方法的實現思路是遞迴呼叫Lexer
模組的其他get
方法,通過返回值是否為null
來判斷因子種類。另外,筆者在第一次作業中主要通過正則表示式來獲取相應資訊,但是由於第二次作業
SumFunc、DefinedFunc
以及第三次作業TrigonoFunc
內部因子限制的解除,正則表示式不再能夠滿足題目的需求,這是因為因子可能有多層括號巢狀,正則表示式的設計變得較為困難,容易有紕漏。筆者當時選擇的方法是在第二次作業與第三次作業Lexer
模組中設計public int getEndPos()
方法來維護一個括號棧,解決了正則表示式提取錯誤的問題。但筆者反思時認為,我們應儘可能不使用正則表示式,而要儘量從語義角度獲取對應資訊,除非特徵資訊非常簡單。
解析模組實現
該類在遞迴下降中發揮關鍵作用,但主要僅包含三個方法,分別是public Expr parseExpression()
、public Term parseTerm()
以及public Factor parseFactor()
。
parseExpression()
通過未包含於括號中的+與-
將待解析表示式拆開,逐一對每個部分呼叫parseTerm()
,並將解析結果返回。
public Expr parseExpression() {
Expr expression = new Expr();
expression.addTerm(parseTerm());
while (lexer.getCurToken() == '+'
|| lexer.getCurToken() == '-') {
expression.addTerm(parseTerm());
}
return expression;
}
public Term parseTerm()
通過未包含於括號中的*
將待解析項拆開,逐一對每個部分呼叫parseFactor()
,並將解析結果返回。而由於筆者在Term
類定義了sign
屬性,故還需要設計public String getTermSign()
方法,不過筆者後面認為該方法更適合出現在Lexer
模組當中。
public Term parseTerm() {
Term term = new Term(getTermSign());
term.addFactor(parseFactor());
while (lexer.getCurToken() == '*' && lexer.getNextToken() != '*') {
lexer.next();
term.addFactor(parseFactor());
}
return term;
}
public String getTermSign() {
if (lexer.getCurToken() == '-') {
lexer.next();
return "-";
} else {
if (lexer.getCurToken() == '+') {
lexer.next();
}
return "+";
}
}
public Factor parseFactor()
類似於Lexer
模組public Factor getTrigonoFuncFactor(String s)
的實現思路,對於Expr
以外的因子,僅需要通過呼叫Lexer
模組的get
方法並判斷返回值是否為null
來確定因子種類;而對於Expr
我們只需要遞迴呼叫public Expr parseExpression()
方法即可。關於表示式的冪次,筆者選擇的方法是在Parser
類獲取指數
,並呼叫Expr
內部的public void multiplySelf(BigInteger index)
方法。另一種實現思路是在Expr
類定義index
屬性,這樣或許可以在Lexer
模組獲取指數
,在功能上更契合Lexer
模組與Parser
模組應有的作用。
儲存
第一次作業
在第一次作業
中,筆者採用了Expr→Term→Factor
的解析思路,同時筆者注意到Expr
與Term
的通項均可以表示為一個多項式,因此我們可以通過一個HashMap<String, BigInteger>
來對Expr
和Term
進行儲存,其中多項式每一項a*x**b
中的a和b可分別作為HashMap
的value
和key
。
第二次作業
在第二次作業
中,由於三角函式的引入,筆者在考慮Expr
與Term
的通項時遇到了困難。首先Expr
和Term
的通項由第一次作業的$\sum ax**b$拓展為$\sum axb_{i1}\Pi(\sin (f(x))b_{i2})\Pi(\cos (g(x))b_{i3})$,並可以進一步化歸為$\sum\lambda\Pi G(f(x))\alpha$,其中:
$G(f(x))\to x;|;TrigonoFunc$
$TrigonoFunc\to\sin f(x);∣;cosf(x)$
$f(x)\to PowerFunc;∣;Num$
$PowerFunc\to x**\alpha,;\alpha\in N^*$
$Num\to帶符號的整數$(正數和零前無符號,負數前有一個負號)
Num→帶符號的整數Num→帶符號的整數(正數和零前無符號,負數前有一個符號"-")
因此筆者希望設計一種便於檢索合並的資料結構來對此通項進行儲存。
我們提出的方式:[兩層HashMap
巢狀]
- 外層
HashMap
:HashMap<(Inner HashMap), BigInteger>
,其中BigInteger
儲存前置係數$\lambda$。 - 內層
HashMap
:HashMap<String, BigInteger>
,其中String
儲存三角表達式或冪函式底數x
的字串形式,BigInteger
儲存三角表達式或x
的冪次。
具體解析策略
為方便起見,我們定義一個HashMap
可以表示為{$(a_1,b_1),(a_2,b_2),\cdots,(a_n,b_n)$}。本策略採用邊解析邊合併的方式進行儲存:
- 解析
Num
時,我們需更新外層HashMap
每一項元素對應的value
。 - 解析
PowerFunc
時,我們需更新外層HashMap
所有key
對應的HashMap
中key
為x
的元素的value
。 - 解析
TrigonoFunc
時,我們需更新外層HashMap
中所有kkey
對應的HashMap
,如果先前已儲存過相同的TrigonoFunc
,則只需要更新內層HashMap
中key
為該TrigonoFunc
的元素對應的value
;若先前未儲存過,則可直接push
元素。 - 解析求和函式、自定義函式或表示式時,將其對應的外層
HashMap
與此Term
已有的外層HashMap
合併。 - 解析第一個
Factor
時,預設在內層HashMap
中put
一個key
為x
的元素。
例:解析表示式sin(x)*x*2*-3*(1+x+2*sin (x*2)**3)
根據上述法則,逐步解析得到的外層
HashMap
如下:第一步:{({("sin(x)",1),("x",0)},1)}
第二步:{({("sin(x)",1),("x",2),1)},1)}
第三步:{({("sin(x)",1),("x",2)},−3)}
第四步:{({("sin(x)",1),("x",2)},−3),({("sin(x)",1),("x",3)},−3),({("sin(x)",1),("x",3),("sin(x∗∗2)",3)},−6)}
6.後期對兩個簡單項進行合併時(每個簡單項分別對應於一個內層HashMap
),我們首先比較兩個內層HashMap
的size()
是否相等;若相等,則對其中一個內層HashMap
進行遍歷,如果此時遍歷的key
同時出現在另一個內層HashMap
中,且value
相等,則繼續比較,否則可以提前判斷這兩個簡單項不可合併。若兩個簡單項可合併,則只需要將內層HashMap
對應的外層HashMap
的value
相加。
這樣做的好處:儲存結構簡明、對不同項只需要反覆呼叫HashMap
、輸出較易。
第三次作業
由於三角函式內部因子限制的解除,內層HashMap
繼續使用String
型別作為key
不能滿足巢狀的需求,故我們將String
更換為Factor
,並且此時筆者發現,因子的次數
資訊已包含Factor
中,故筆者在第三次作業
中使用了HashMap<HashSet<Factor>,BigInteger>
作為儲存的資料結構,解析與儲存的策略仍然類似於第二次作業
,實際改動並不大。
化簡
切莫壓點提交,並且要先保證正確性,再考慮效能上的優化。
該類主要負責完成Term
層面的優化,具體內容將在優化策略中介紹。
輸出
該類主要負責完成輸出
層面的優化,並返回解析後表示式的字串,具體內容同樣將在優化策略中介紹。
優化策略
Term優化
筆者作出的優化有:
- 將
0次冪因子
化簡為1
,事實上可以直接刪除。 - 將冪次為奇、內部因子種類為
Num
且Num
為負數的三角函式符號外提,判斷是否可與其他項
合併。 - 將
sin(0)
化簡為0
,將cos(0)
化簡為1
。 - 對於$sin(f(x)){a_{1}}cos(f(x)){b_1}G(x)+sin(f(x)){a_2}cos(f(x)){b_2}G(x)$,若滿足$|a_1-a_2|=2&&(a_1-a_2)(b_1-b_2)=-4$,則可進行平方和合並。
優化順序將影響正確性,筆者在第三次作業中誤選擇先去除包含
sin(0)
的項,再化簡0次冪因子
,導致筆者將sin(0)**0
輸出為0
。
輸出優化
筆者作出的優化有:
- 對於
1*G(x)
輸出G(x)
,-1*G(x)
輸出-G(x)
。 - 對於
G(x)**1
輸出G(x)
。 - 優先輸出
係數為正
的項。 - 對於
x**2
輸出x*x
,在第三次作業
中其可能導致負優化,比如sin(x**2)
長度短於sin((x*x))
。
易錯點分析
HashMap與HashSet
- 對於
HashMap
,如果put
元素的key
先前已存在,則其會覆蓋之前的資料。 - 對於
HashMap
,如果選擇Object
型別作為key
,即使兩個Object
表達的狀態相同,也不能通過一個Object
索引另一個Object
的value
. - 不要邊遍歷邊修改
HashMap
和HashSet
中的元素,這會導致錯誤遍歷,甚至死迴圈。
深拷貝與淺拷貝
由於淺拷貝的存在,物件的不當賦值可能導致物件之間相互影響,因此筆者重寫了所有因子類的public Factor clone()
方法,來保證每次新物件的生成都是原有物件的深拷貝,確保物件之間不會相互影響。
正則匹配
對於巢狀括號,正則表示式的書寫變得尤為困難,可以考慮:
f(sin(x))+f(sin(x))
f(sin(x),sin(x))+g(sin(x))
f(sin(x),sin(x),sin(x))+g(sin(x))
故我們應儘可能從語義角度進行分析。
遞迴解析
多次預處理
由於筆者在處理SumFunc
與DefinedFunc
時選擇對外返回待解析表示式字串,故我們需要重新對字串進行預處理
,否則可能導致解析錯誤。
物件比較
筆者重寫了所有因子類的public boolean equals(Factor other)
方法,比較策略是:
- 例項型別是否相同,不相同返回
false
。 - 狀態是否相同,不相同返回
false
。如果返回的狀態型別為HashMap
或HashSet
,則需要逐一對內部元素的狀態進行比較。 - 上述條件滿足,返回
true
。
資料構造
在對自己程式進行驗證或對其他人進行Hack
時,首先需要仔細閱讀指導書,確保自己對於指導書中的細節完全瞭解,避免被強測背刺,比如筆者在第二次作業中,疏忽了在函式定義中可以存在空格,導致解析錯誤。
對於黑盒測試,筆者認為可採取兩種方式對程式進行驗證和Hack
,一是資料生成器隨機轟炸,這種可以找出程式中基本的錯誤;二是對一些極端情況的資料構造,比如因子0次冪
、指標溢位
。
對於白盒測試,主要通過程式碼分析來找出程式中的漏洞,比如如果對方採取了正則表示式解析,則可以重點關注其表示式的書寫是否有紕漏;如果對方進行復雜的優化,則可以重點關注優化部分的邏輯是否正確,是否可能發生TLE。
感受
通過本單元的學習,筆者對面向物件的思想有了初步的認識,並且有了一定程式碼風格的培養,最後,感謝課程組的辛勤奉獻!