面向對象設計與構造已進行了三次表達式求導的做業。是時候作個總結(告終)了。。。 java
一. 三次做業的程序結構分析python
第一次做業是簡單多項式求導,即只含底數爲x的指數函數相加減構成的表達式。第一步是要獲取輸入並對錯誤的輸入格式給出反饋。利用正則表達式能很容易解決這一內容,只不過對第一項要單獨考慮(但正則的使用有利有弊,見第二部分),這一部分我放在了MainClass中進行處理。相應正則以下,firstMatch爲第一項的正則,nextMatch爲其他項的正則,匹配完一項將它從源字符串中移除匹配下一項。正則表達式
1 firstMatch = "\\s*[-+]?\\s*[-+]?\\d+\\s*|\\s*[-+]?\\s*[-+]?\\d+\\s*\\*\\s*x\\s*(\\^\\s*[-+]?\\d+\\s*)?|\\s*[-+]?\\s*[-+]?\\s*x\\s*(\\^\\s*[-+]?\\d+\\s*)?" 2 nextMatch = "\\s*[-+]\\s*[-+]?\\d+\\s*|\\s*[-+]\\s*[-+]?\\d+\\s*\\*\\s*x\\s*(\\^\\s*[-+]?\\d+\\s*)?|\\s*[-+]\\s*[-+]?\\s*x\\s*(\\^\\s*[-+]?\\d+\\s*)?"
第二步在輸入正確的前提下我須要獲取每一項的係數及指數構建一個對象(Handle),成員變量即係數(coeff)和指數(power),並設置求導方法(handle),返回一個新的同類型對象。算法
第三步將全部求導後的對象放到Poly類中進行輸出,在輸出時須要優化掉係數爲0的項,以及合併指數相等的項。在合併同類項時,我採用的是Arraylist而不是HashMap,由於arraylist刪除更加方便。編程
如下爲第一次做業的類圖,能夠看出第一次做業仍是較爲簡單,一共3個類,14個方法就能完成。sass
如下是第一次做業方法複雜度,能夠看出有的方法的基本複雜度(ev)較高,該方法的非結構化程度較高,難以模塊化和維護。有的方法模塊設計複雜度較高(lv),意味着模塊耦合度高,模塊難於隔離,維護和複用。有的方法圈複雜度(v)高,說明程序易出錯。整體上看第一次做業雖然簡單,可是代碼風格不盡如人意,這大概是第一次接觸面向對象編程犯下的錯誤吧,這也是爲何我第二次做業徹底重寫的緣由,由於代碼可擴展性太差了。bash
第二次做業在第一次的基礎上新增了因子這個概念,即一項可有多個因子相乘,而且因子不只僅是指數函數,也包含三角函數。這就爲求導這一步增長了許多難度。ide
第一步正則匹配就不贅述了,只不過多了項的匹配方式變得複雜一些而已。下圖是一個因子的正則。模塊化
1 public static String factorPattern() { 2 String numFun = "[-+]?\\d+"; 3 String triFun = 4 "\\s*(sin|cos)\\s*\\(\\s*x\\s*\\)(\\s*\\^\\s*[-+]?\\d+)?"; 5 String powerFun = "\\s*x\\s*(\\^\\s*[-+]?\\d+)?"; 6 return "(" + powerFun + "|" + triFun + "|" + numFun + ")"; 7 }
第二步的求導部分困擾了我很久,如何能在正確求導的同時確保代碼的可擴展性,在苦思冥想一天以後,我最終放棄了可擴展性這一要求,直接將一項歸一化成三元組的形式:函數
a*x^b*sin(x)^c*cos(x)^d
而後按照求導法則獲得待輸出的項:
x^b*sin(x)^c*cos(x)^d+a*b*x*(b-1)*sin(x)^c*cos(x)^d+a*x^b*c*sin(x)^(c-1)*cos(x)^(d+1)-a*x^b*sin(x)^(c+1)*d*cos(x)^(d-1)
能夠看到一項求導後會產生四項,即四個對象
第三步的化簡我先合併了同類項,並對sin(x)^2+cos(x)^2=1產生的優化進行必定的考慮,只將含sin(x)^2和cos(x)^2的項檢查是否能夠合併,也是在優化的過程當中,產生了一個讓我付出慘痛代價的BUG(見第二部分)。
下圖是第二次做業的類圖,共4各種,50個方法,結構更復雜,較上一次做業有顯著提高。
如下是第二次做業方法複雜度分析
能夠看出,雖然方法數量變多了,但複雜度高的方法較第一次有顯著減小,主要複雜的方法集中在優化的方法中。例如Poly類中的simplify方法,將正號提早,以及Term中的buildOneTerm方法,將項的長度化到最短,以及MyHashMap中的simplifySin2PlusCos2,將sin(x)^2+cos(x)^2化簡,巧合的是,個人bug也正是發生在這個方法中。
第三次做業也是最難的一次做業,簡要的歸納一下就是包含表達式因子,以及三角函數嵌套因子。第一眼看這道題就離不開遞歸。遞歸判斷格式,遞歸求導,遞歸化簡。我完成了前兩步。
判斷格式,最重要的一步就是如何將表達式拆分紅項,顯然要從加減號下手,但並非全部加減號都是項與項之間的運算符,例如x^-1中的指數,以及sin((1-1))中表達式因子裏的減號。那我是如何判斷一個加減號是不是運算符呢?首先我將加減符號之間的空格去掉,將乘號,指數符號和加減號之間的空格去掉,能夠證實這種操做不會將正確(錯誤)的輸入判斷成錯誤(正確)。接下來知足以下判斷的就是項與項間的運算符
1.該符號以前左括號總數和右括號總數相等。
2.該符號左鄰居不是加減乘,乘方運算符。
至此能夠將表達式拆分爲項,將項拆分爲因子較爲簡單,只需按乘號進行拆分便可。判斷格式的一步在因子類(Factor)內部進行,每一種因子都有相應的格式以下,對於嵌套因子,表達式因子,則須要遞歸調用相應類的判斷格式方法。
1 String numFun = "[ \\t]*[-+]?\\d+[ \\t]*"; 2 String powerFun = "[ \\t]*x[ \\t]*(\\^[ \\t]*[-+]?\\d+)?[ \\t]*"; 3 Pattern polyFactorPattern = Pattern.compile("([ \\t])*\\((.*)\\)([ \\t]*)"); 4 Pattern nestPattern = Pattern.compile("([ \\t]*)(sin|cos)[ \\t]*" 5 + "\\((.*)\\)(([ \\t]*\\^[ \\t]*[-+]?\\d+)?)[ \\t]*");
判斷完格式以後進行求導,方法與判斷格式相似,只不過到Factor類以後返回求導後獲得的字符串,返回到Term類,Term類負責乘法公式,Poly類負責加法公式。
Term類求導方法:
public static String derivative(String str) { ArrayList<String> derivativeArray = fetchFactors(str); String result = ""; for (int i = 0; i < derivativeArray.size(); i++) { result = result + "+" + Factor.derivative(derivativeArray.get(i)); for (int j = 0; j < derivativeArray.size(); j++) { if (j == i) { continue; } else { result = result + "*" + derivativeArray.get(j); } } } return result.substring(1); }
Poly類求導方法:
1 public static String derivative(String str) { 2 ArrayList<String> derivativeArray = fetchTerms(str); 3 for(int i = 0 ;i< derivativeArray.size();i++) { 4 System.out.println(derivativeArray.get(i)); 5 } 6 String result = ""; 7 for (int i = 0; i < derivativeArray.size(); i++) { 8 result = result + "+" + Term.derivative(derivativeArray.get(i)); 9 } 10 return result.substring(1); 11 }
求導完後,我只對沒有括號的而且存在0因子的項進行移除,並將沒有括號的而且存在多個常數因子的項進行化簡。但一旦套了一層括號我就沒法化簡。例如(+++1),個人輸出是(0*+1*+1+0*+1*+1+0*+1*+1),但+++1輸出就是0。也就是說化簡這部分我沒有實現遞歸化簡。
如下是第三次做業的類圖,能夠看到此次類的數量不多,方法也不多,可想而知方法的複雜度必定很高。並且我類與類之間惟一的聯繫就是遞歸調用進行判斷格式和求導。
那麼方法複雜度有多高呢,來看看方法複雜度表。
能夠看出,方法很少,但標紅的數字很多,老實說,此次做業我有點拋棄面向對象的思想了,很大程度上都是面向過程在主導。畢竟一旦涉及到算法問題,設計結構層面很難作到完善,這也是我之後應該彌補的地方。
二. 三次做業產生的BUG分析
第一次做業惟一的BUG就是將沒有將\f,\v等非法空白字符排除,致使在強測滿分的狀況下互測被Hack了8次,還好都是同質BUG,5行就能改完。BUG產生緣由仍是沒有認真讀指導書,自覺得輸入的空白字符只能是空格和製表符。值得一提的是,在本次做業中的正則匹配環節,我是採用一項一項匹配,而沒有采用大正則匹配整個表達式。有的同窗採用大正則匹配,在輸入爲500個「+x」後會產生「StackOverflow」異常。雖然我沒有犯這個錯誤,但我也上網查了查這個錯誤的緣由。正則分爲三種模式,而咱們一般使用的貪婪模式的回溯特性會給棧空間形成大麻煩。具體如何使用這三種模式見下圖。
第二次做業的BUG無疑是致命的,致使我強測只有60出頭的分數,勉勉強強進入互測,而這個BUG也是我萬萬沒有想到的一個BUG。具體的說,一旦求導結果符合個人優化要求,在優化過程當中就會拋出異常
Exception in thread "main" java.util.ConcurrentModificationException
百度了一下發現我在迭代Arraylist過程當中,Arraylist的元素進行了刪除增長等操做。
1 for (Term term : terms)
1 this.remove(term); 2 this.add(newTermOne); 3 this.add(newTermTwo);
網上找了一下爲何這樣會有錯誤,CSDN上說Iterator 是工做在一個獨立的線程中,而且擁有一個 mutex 鎖。 Iterator 被建立以後會創建一個指向原來集合的單鏈索引表,當原來的集合數量發生變化時,這個索引表的內容不會同步改變,當索引指針日後移動的時候就找不到要迭代的對象,按照 fail-fast 原則 Iterator 會立刻拋出 java.util.ConcurrentModificationException 異常。
因此 Iterator 在工做的時候是不容許被迭代的對象被改變的,但可使用 Iterator 自己的方法 remove() 來刪除對象, Iterator.remove() 方法會在刪除當前迭代對象的同時維護索引的一致性。 然而我代碼中的remove()方法是Arraylist自帶的方法,故會產生異常。在這以前,我根本就沒有用過迭代器,更別說知道不能刪除元素這一原則,只不過是idea提醒我能將for循環簡化,我就照着作了,沒想到會這麼慘,下次不再敢用迭代器了。
第三次做業目前爲止只發現了一個BUG,是沒能將錯誤的輸入反饋「WRONG FORMAT!」。具體的就是輸入「1+++sin(x)」,但我會輸出「cos(x)」。找了找緣由,發現下圖代碼中的問題。
1 if (str.length() > 0) { 2 if (str.charAt(0) != '(') { 3 if (str.length() > 1) { 4 if (str.charAt(1) == '+') { 5 str = str.substring(0, 1) + "+1*" + str.substring(2); 6 } else if (str.charAt(1) == '-') { 7 str = str.substring(0, 1) + "-1*" + str.substring(2); 8 } 9 } 10 } 11 }
首先str是單獨的一項(不帶加減運算符),若是第零位不是'(',我就要對開頭的正負號進行處理,先判斷第一位是否是正負號,若是是進行下圖操做。我當時只考慮了++1這種項,卻忽視了++sin(x)這種項,前者會處理成「+1*+1*1」,然後者會處理成「+1*+1*sin(x)」,這樣就會判斷錯誤。事實上這段代碼沒有存在的意義。即便沒有這段代碼「++1」在個人程序中會處理成「+1*+1」,而「++sin(x)」會處理成「+1*+sin(x)」,這樣就能正確判斷格式了。不過還好此次互測不Hack"WRONGFORMAT!", 不然我又要成大禮包了。
三. 互測環節的策略
第一次做業的互測方法主要就是兩點。StackOverflow和\f問題,咱們組全部人也只有這兩個BUG。
第二次做業,因爲我被分到了C組,故同組人的BUG異常的多,但我HACK的點主要仍是WRONG FORMAT。例以下面這個測試點
+ + +1 + +1 * x ^ +1 * sin ( x ) ^ -1 * cos ( x ) ^ -1
這個測試點將全部可能存在空白字符的地方都放置了一個空格和一個製表符,並在末尾也加了一個空白字符(事實證實好多人都有這個BUG)。
第三次做業,因爲過於複雜,本身構造測試樣例,本身觀察輸出變得很不現實,故我開始採用自動評測的方法。將全部人的程序打包成.jar文件放在贊成目錄下,並在該目錄下寫my.sh腳本文件。
1 #!/bin/bash 2 #excute .jar(s) 3 find . -name "*.txt" -exec rm -rf {} \; 4 touch archer.txt 5 touch assassin.txt 6 touch berserker.txt 7 touch caster.txt 8 touch lancer.txt 9 touch rider.txt 10 touch saber.txt 11 for ((;;)) 12 do 13 python generate.py > tem.txt 14 15 cat tem.txt >> final.txt 16 cat tem.txt | java -jar archer.jar > archer.txt 17 echo " " >> archer.txt 18 cat tem.txt | java -jar assassin.jar > assassin.txt 19 echo " " >> assassin.txt 20 cat tem.txt | java -jar berserker.jar > berserker.txt 21 cat tem.txt | java -jar lancer.jar > lancer.txt 22 echo " " >> lancer.txt 23 cat tem.txt | java -jar saber.jar > saber.txt 24 cat berserker.txt >> archer.txt 25 cat berserker.txt >> assassin.txt 26 cat berserker.txt > tem.txt 27 cat tem.txt >> berserker.txt 28 cat berserker.txt >> lancer.txt 29 cat berserker.txt >> saber.txt 30 echo "archer and berserker:" >> final.txt 31 cat archer.txt | python OO.py >> final.txt 32 echo "assassin and berserker:" >> final.txt 33 cat assassin.txt | python OO.py >> final.txt 34 echo "berserker and himself:" >> final.txt 35 cat berserker.txt | python OO.py >> final.txt 36 echo "lancer and berserker:" >> final.txt 37 cat lancer.txt | python OO.py >> final.txt 38 echo "saber and berserker:" >> final.txt 39 cat saber.txt | python OO.py >> final.txt 40 done
其中generate.py是陳宇軒巨佬在討論區提供的自動生成測試樣例的程序,OO.py是我本身寫的用於比較兩個結果是否等價的程序。
OO.py代碼以下:
1 from sympy import * 2 from sympy.abc import x 3 4 input_str = input() 5 input_str_two = input() 6 input_str = "(" + input_str.replace("^","**") + ")" 7 input_str_two = "-(" + input_str_two.replace("^","**") + ")" 8 input_str_final = input_str + input_str_two 9 try: 10 if(simplify(input_str_final) == 0):#若是兩個結果等價 11 print("Same") 12 else: #不然 13 print("Different") 14 except: #一旦有一我的的程序產生異常,就輸出Different 15 print("Different")
自動化測試的優勢在於人變得輕鬆很多,但缺點就是找到的BUG大部分都是同質BUG,且BUG涵蓋面不廣。老實說,三次互測我都沒用認真看過一我的的代碼,主要仍是太耗時間且收益不大,再加上OS,離散概統數學建模的壓迫,不得已才採用如上的互測方法,事實證實效果不錯。
四. 建立型模式(Creation Pattern)
說實話,在寫博客以前,我都沒據說過這個術語,更別說Applying it了。若是非要講講的話。前兩次做業的建立實例方法相似於工廠模式。
而第三次做業就沒有模式可言了,由於整個程序中我一個對象都沒有創建,全是用類的靜態方法,不折不扣的面向方法編程。不過我之後不再敢了。
五. 心得與體會
學了一個月的java和麪向對象的思想,就我本人而言,仍是沒有感覺到與面向過程有多大的不一樣,即便有了類,對象的概念,但在我眼裏與C語言的結構體沒什麼兩樣。多是我思惟尚未轉換過來,這也是我在接下來的做業中要努力去改變的。但願你們一塊兒努力啊😄。