第一次作業 第一次作業需要完成的任務為簡單多項式導函數的求解。 思路 因為僅僅是簡單多項式的求導,所以求導本身沒有什麼可說的,直接套用冪函數的求導公式就行了,主要的精力是花在了正則表達式上。這裡推薦兩個網站: https://github.com/ziishaned/learn regex http ...
第一次作業
第一次作業需要完成的任務為簡單多項式導函數的求解。
思路
因為僅僅是簡單多項式的求導,所以求導本身沒有什麼可說的,直接套用冪函數的求導公式就行了,主要的精力是花在了正則表達式上。這裡推薦兩個網站:
https://github.com/ziishaned/learn-regex
前者可以用來學習正則表達式的語法,後者則提供實時的正則表達式匹配,方便進行調試和驗證。尤其是後者,能大幅提高編寫正則表達式的效率和正確性,省去了每次都得在IDEA里運行一遍的麻煩。
用正則時有一點需要註意,那就是大正則是不可取的。我一開始就是企圖用一大串正則匹配整個輸入,結果當時是爆棧了。合理的做法應當是每次只find()一個Term,以及通過Term與Term之間的連接關係判斷合法性。
在數據結構方面,我一開始用的是ArrayList,但在後來優化的時候發現合併非常麻煩,於是果斷放棄,學習並採用了HashMap。通過將指數作為Key,可以快速地找到指數相同的項,以實現合併同類項的目的。
程式結構分析
由於剛接觸Java語言,也不太懂什麼是面向對象思想,所以程式整體來說還是比較C type的。也可能是由於本次作業比較簡單,所以也並沒有用到繼承、介面之類的東西。從類圖中可以看到,一共也就兩個類,其中PolyDiff完成主函數、預處理、合法性判斷等任務,而Term則實例化出多項式中的每一項,保存繫數和指數,並提供diff()求導方法。
度量分析
可以看出個別方法複雜度過高,這很不OO。下次應當多註意抽象以及代碼重用,避免一個方法或者類的過於臃腫。
關於BUG
公測未被查出bug。在互測階段,我第一個發現的bug是自己的bug,其原因在於,我判斷合法性的做法是將匹配了的每一項用"#"替換掉,最後看除了" "、" \t"、"#"之外是否沒有其他的字元。然而,我忽略了輸入一開始就含有"#"的可能,但這種可能性非常小,以至於我認為只要不仔細閱讀我的代碼,基本上是不可能歪打正著的。遺憾的是,最終還是有一位十分認真的同學,(我猜)在一行一行通讀了我的整個代碼後,居然還真的發現了這個bug……我是服氣的,真的太強了orz
至於我查別人的bug,都是用的我寫作業時自測發現了bug的樣例,因為我相信這樣的樣例會具有一定的殺傷力和普適性。事實證明,我的確能用這種方法測出很多別人的bug,於是也就沒有再結合他人的代碼設計結構來設計更多的測試樣例。
第二次作業
第二次作業需要完成的任務為包含簡單冪函數和簡單正餘弦函數的導函數的求解。
思路
第二次作業新引入了正餘弦函數,乍看複雜了許多,但其實還是可以用套公式的辦法死做,然而代價就是毫無擴展性可言。
對於每一項及其導函數,都可以化為冪函數和正餘弦函數的冪次的乘積這種標準形式,那麼Term就相應擁有了繫數、冪函數及正餘弦函數的指數這四個屬性。除了正則表達式匹配和求導規則有一些變化,其餘與第一次作業並無太大區別。
值得註意的是,這次HashMap的Key我採用的是將冪函數及正餘弦函數的指數拼接成一個字元串,併在相鄰指數之間用"|"分隔。如此一來,便可省去重寫equals()和hashcode()的麻煩,然後像第一次作業一樣合併同類項。
程式結構分析
程式結構大體類似於第一次作業。
度量分析
合法性判斷的方法還是有些冗長了,可能是因為其中包含了一些對輸入進行預處理的操作,可以考慮將這部分單獨抽離出來。
關於BUG
在吸取了上一次作業的教訓後,我捨棄了替換捕獲組方法,而是根據本次匹配起點是否為上一次匹配的終點,以及最後一次匹配的終點是否為字元串的末尾,來判斷輸入的合法性。最終在公測和互測中都未發現bug。
發現別人程式的bug依然採用上一次作業的方法,果然又查出了不少bug,大多是對於非法的輸入沒有輸出WRONG FORMAT!
第三次作業
第三次作業需要完成的任務為包含簡單冪函數和簡單正餘弦函數的導函數的求解(支持因數嵌套在三角函數因數中)。
思路
正如之前所說的,套公式的做法是無法解決任意層嵌套的問題的,因此整個程式架構必須重構。在這裡我採用的是遞歸下降的方法,規定好每種函數、組合的求導法則,將嵌套的內容看做一個整體,留給下一層遞歸處理,每次只判斷當前層的合法性併進行相應的求導。
在優化方面,僅僅是剔除了冗餘的0、1、括弧、正負號等等,在同類項合併和恆等變換方面未能進行更多的處理,其原因和求導方法的返回值有關。由於我在一開始設計的時候就直接把每個因數的求導結果當作字元串保存,所以整個表達式求導的過程就相當於是字元串按一定規則轉化和拼接的過程。這樣做的好處在於簡答、直觀、不易出錯,但弊端也很明顯,那就是很難做進一步優化,因為對字元串中的內容是無法進行插入、刪除、替換、排序或合併等操作的,除非像讀取輸入那樣再一次對字元串進行分析。由於時間有限,我沒有那樣做。其實更好的方法是將求導的結果也作為某一個類的實例,方便對其做進一步的處理。
程式結構分析
如圖所示,本次作業我用到了面向對象中的繼承,有必要這樣做的原因有三:
- 每個子類都需要toString()方法,以返回該項因數自身的字元串。由於這個方法對於每種因數都一樣,所以應該在父類中統一實現。
- 每個子類都需要實現各自的diff()方法,因為每種函數或組合都有自己的求導規則。
- 每個子類都繼承於Factor類使得不同類型的因數可以放在一個ArrayList中一起處理。
至於Term,它並不是因數,只是Expr的組成部分,所以單獨成為一類。
通過繼承這種面向對象的思維方式,我們程式的結構更加清晰,功能更加強大,最重要的是,擁有了可擴展性。
度量分析
果然越複雜的程式,要做到均衡控制每個方法的複雜度就越難。主要的問題依然出在輸入分析、預處理以及合法性判斷上,儘管這次我將他們分離了開來。至於有沒有必要以及如何進一步拆解,還有待研究。
關於BUG
本次作業依然沒有被查出任何bug。
在查別人的bug方面,為瞭解放勞動力,這次我選擇寫腳本進行批量測試。我用到了Python中一個強大的第三方包——SymPy,可以進行複雜表達式的求導運算以及判斷兩式是否等價,這也是我的"簡化版評測機"的主要原理,即將Java程式的輸出與SymPy的輸出進行比對。顯然,該評測機無法對非法輸入給出評判。而測試用例本身依然是我自測時手動構造的測試集,按行讀取便可進行批量測試,威力極大,效率奇高。
總結與建議
第一單元總體還是比較入門的,重點在於熟悉Java語法特性、建立面向對象思維,為日後的多線程編程打好基礎。
對於我個人而已,學習了Java正則表達式、BigInteger類、HashMap、重寫與重載、繼承與介面等等技術,並且慢慢開始理解了層次化架構和麵向對象技術的優越性。總體感覺Java還是比C/C++要方便的多,基本上只要是你能想到的功能,總有類或者第三方包能為你提供解決方案。而這就需要我們有查找資料自學的能力,無論是看技術博客還是閱讀官方文檔,再加上自己多動手實踐,相信很快就能掌握一項新的技術,併在作業中發揮其功能。
此外,通過Checkstyle對代碼風格進行規範也是比較有效的措施,基本上不會再寫出會令人抓狂的代碼了。但是其中每行最多80個字元的限制我實在覺得不太合理,尤其是涉及正則表達式的時候,往往是硬生生地把一句邏輯連貫的代碼拆成多行。個人認為在閱讀代碼時,莫名其妙的換行比一行稍長的代碼體驗更糟。建議將每行的字元數上限改為100~120。
對於面向對象的理解可能還是只是停留在表層,甚至會有一些誤解,這就需要進一步的學習和練習,在一次次實踐中總結經驗與教訓。希望以後能寫出更加OO的程式。