20373222李世昱第一單元總結
第一單元總結
第一部分 程式碼架構迭代邏輯
第一次作業類圖如下:
核心思路:仿照trainning中遞迴下降的做法,開三個對應結構的類:exp(表示式),term(項),factor(應該說最簡因子),對文字起分析的類如Lexer,parser等由於和trainning差距不多,思路也都較為固定,這裡不做展示。
1:如何解決化簡問題?
我們發現最終結果總可以表示為x的多項式,a0X**0+a1x**1+...這樣我們可以開一個hashmap,把x的指數作為鍵值,這樣每個表示式唯一對應一個hashmap(舉例來說:-x**2 +2*x就唯一對應{2:-1,1:2}),同時每次運算的時候就可以指數相同自動化到最簡。
->這樣我門確定了最終答案的合併方法:hashmap自然合併
2: 如何處理看起來很麻煩的因子?
三種因子,x冪函式,常數,表示式因子。我們可以把常數因子視為一種x的0次冪的冪函式,這樣和x冪函式一同作為基礎因子,而表示式因子則直接去new一個表示式型別的物件,那麼很顯然term裡面就會混有多種型別的物件,如果我們一個個把他們區分開再呼叫各自的方法顯然是一種很不美觀的做法,為次我想讓這幾個型別達成某種功能上的統一,這就有了類圖站在C位最顯眼的介面:Operator介面(這個介面後文稱為"運算元協議"或者"運算元介面")。那這個接口裡面要放什麼函式呢,對應函式又要如何實現,著我們就得仔細分析一下完整的流程。
->我們採用Operate介面來統一處理
3:運算元介面中要什麼功能?
我們先分析一下完整流程,我們拿到一個可愛的小表示式,先用詞法分析器一頓分析,得到了一個類樹的結構,上層的物件的容器中放著子物件。不論是什麼物件,我們要得到最終的hashmap,我們需要他把他的資料結構,也就是list容器所蘊含的結構資訊,轉換成hashmap,也就是我們要的答案。然後對於高階的物件,他需要讓所有子物件把他的hashmap準備好,term物件只要把這些相乘就好,exp物件則是相加,而對factor物件來說則不用處理。與此同時我們發現化簡的過程不必在意子物件究竟是什麼,僅僅要求子物件把他的hashmap乖乖準備好並且暴露出來,我就可以進行自己的化簡了。這樣我們確定了運算元協議所需的兩個函式:用於讓子類更新準備hashmap的update(),用於暴露出自己的hashmap的getHashmap()函式
->update();getHashmap();
4:迭代開發考慮?
可以從類圖上看到,用於具體化簡exp,term的具體運算方法並沒有寫入對應的物件中,而是提取出來另開一個Operators(之後稱為運算大類)類,主要是出於以下考慮:如果要加入新的結構型別(地位類似於exp,term等),這個型別的化簡可能要重複利用hashmap的加法乘法化簡,把計算方法獨立出結構中以便後續的重複利用。
至此,第一次作業已經可以輕鬆解決,第一單元我認為的難點在於第一次到第二次的迭代思路,我將在下文詳細一步一步推出新架構的架構方法
順帶一提,我的第二次作業不小心直接完成了第三次作業,所以只有一次迭代(
先不放類圖,我們嘗試從第一次架構迭代成第二次的架構:
先分析一下問題:
1.新加入了三角函式
2.新加入了自定義函式(這裡sum函式和自定義函式處理有類似之處,放在一個問題中)
3.變數XYZI,第一次的hashmap不能記錄記錄全部資訊,需要換一個結構。
我們先從第三個問題入手:
1.多變數如何化簡?
我們先不考慮sin,這裡兩個多項式x**2y**1z**3 z**3x**2y**1,,我們知道他倆能化簡的依據是變數一樣,對應相同變數的冪次也一樣,那麼我們稍加思考,我們如果把一種表示式型別進行規則化的表達,就可以簡單的進行化簡了。所謂規則化的表達,簡單來說就是輸入或者hashmap遍歷的無序不影響結果(這一點後面還會用到)。在這裡我們如果人為構造一個字串,先按照字典序把所有變數排序,再按照變數名字+變數次數的格式輸出,舉例來說上面兩個多項式,他們都會被化簡為x2y1z3,那麼我們可以通過這個量來作為化簡依據。
因此我們仍採用hashmap來進行自動化簡,在上一次我直接用指數,也就是一個bigint變數作為鍵,而在這次為了表達更多的資訊,我們新建一個類,這個類的唯一用處就是暴露出化簡的依據。這裡我給他起了個文藝點的名字-- “抗原”,如果你在後文看到“暴露出抗原”等字樣不要懷疑走錯了 。
具體實現中,抗原類內部仍有一個hashmap,型別為<string,Bigint>,表示名字為String的變數和他的次數。抗原需要顯示的暴露出合併的依據,這裡命名為hashStr,也就是上文提到的x2y1z2。此外他需要時刻更新他的hashStr,所以每次嘗試修改抗原類的hashmap時,都會呼叫一次自更新函式。抗原類作為鍵要提供equal和hashcode函式,這裡很顯然我們的合併依據就是hashStr ,所以直接呼叫hashStr的equal和hashcode就可以啦。
->我們新建一個“抗原類”,作為多變數化簡的依據
2.三角函式如何解決?
三角函式我們還是要給他新建一個類的,他有個唯一一個子類結構,也就是三角函式內部。為了之後的可迭代性著想,我們直接把難度升滿,三角函式內部什麼都可以,換言之裡面是個表示式的結構。如何讓三角函式融入體系?偉大的運算元協議教導我們:我化簡不關心你是誰,乖乖更新並暴露出hashmap就好。在此前的架構中我們的結構容器,也就是各個類儲存子物件的list中,用的都是運算元型別,這樣我們要是想加入新的類,僅僅需要讓他遵守運算元協議,其他的根本需要改。
三角函式如何化簡呢?我們這裡先做最簡單的化簡,也就是僅僅對sin指數相同並且內部完全一樣的三角函式進行合併,如sin(x+1)**2 + sin(x+1)**2。看到這裡,你是否想起了什麼?要是沒想起來建議重新讀一遍第一問 沒錯,如果我們把sin內部的規則化的表達 ,把sin連同內部作為一個嶄新的變數的名字(上式就把sin(x+1)看為新的變數,地位等同於x),指數作為這個變數的指數,sin非常自然的融進去了,我們根本不需要對其他類做任何處理!
不過凡事是由代價的,我們把三角函式視為一個變數,那麼他就失去了他作為三角函式的獨特性,因此採用這種方法會在進一步的三角化簡中帶來麻煩。由於時間原因本人最終程式碼中是沒有三角化簡的,不過有設計如何進行三角化簡,會在後文進行說明。
->視為嶄新的變數,不需要做額外處理
3.自定義函式?
先談談為什麼會涉及到克隆的問題。我們的自定義函式本質上是一個表示式物件,如果我們要多次呼叫,就要多次賦值,而用的都是一個表示式,顯然會造成錯誤的結果。所以我們每次呼叫函式要複製出一個一摸一樣的表示式。深克隆具體實現很簡單這裡不多說。自定義函式賦值的過程本質上就是最簡因子的替換,檢查抗原正確符合的因子並且把他刪除,把實參的克隆加入到項裡面就可以了,實現起來還是非常簡單的。
4.三角函式的進一步化簡?
這部分由於時間原因我程式碼中沒有實現,所以也不會過多介紹。一個很簡單的思路就是用正則表示式提取出所需要的抗原,然後進行簡單的字串的簡單替換形成新的hashStr,再反向生成抗原加入到hashmap中就可以了。舉例來說比如我們要化簡cos**2 + sin**2,我們用正則捕捉到最外層的cos()**2,然後把cos這部分去掉反向生成一個抗原,另外把cos字串替換為sin得到有一個抗原,把係數分別置為cos這項的係數的複製和相反數,就實現了cos**2 = 1-sin**2的變換。
以下就是第二次第三次作業的類圖:
(類圖是在寫程式碼之前做的,所以一些變數名字對不上,如feature是上文所述的hashStr)
至此程式碼架構邏輯已全部講述完畢。
總體而言,這個架構優點在於架構簡單思路清晰實現容易,如果不做優化只是做最簡單的合併只需要很少的程式碼量,程式碼中容器都是統一的Operator型別,減少了思考量;缺點在於過於歸一化的設計降低了不同銀子不同類的區分度,在三角函式化簡中增加了難度。
第二部分 程式碼度量
函式總共約有70多個,有十多個函式具體沒有用上(一鍵生成了)明顯標紅的幾個函式,可以看出來很多都與優化有關:
三角函式的update函式中內部做了正負號化簡處理.表示式的print函式,optimize(未實現注掉了),getpositive都是用於規格化並且簡化輸出,考慮的情況比較多,具體實現上也會更加複雜。但是詞法語法分析中我把sum 自定義函式等幾乎全寫入讀取因子函式中,導致讀取因子和耦合度過於高了,應該把不同型別的讀取抽離出來減少耦合度,這一點我處理的不好。
接下來看類的耦合度
這裡我有一些混用的地方,在上一次的架構中Operators的存在只是為了方法的複用,我圖方便把自定義函式容器也放入Operatoes中,導致耦合度過高了。三角函式和表示式其實處理起來很簡單的,都是把責任推給下家,但是由於寫了比較複雜的優化函式極大的增加了複雜度。對於不應該有如此高耦合度的地方我會吸取教訓在下次設計中改進。
第三部分 BUG
說來慚愧,這個程式碼在第三次強測中掛了一個點,並且被hack兩次。這個bug表現為sum中的終點數字如果為負數就會爆0,bug產生原因並不是結構上的問題,而是單純的複製貼上程式碼的時候(複製處理第一個數字的程式碼),忘記修改了一個量,這個bug在第二次沒被測出來,倒在了第三次強測下。我有做過自動測試和自動資料生成,但是不幸的是sum內部只考慮了生成正數的情況,導致未能測出bug,深感遺憾,這裡分享一下資料生成的邏輯。
資料生成也是按照遞迴下降的思路,表示式生成器隨機一個數字要求生成這麼多個項,項同理生成隨機多個因子乘號連線,因子再隨機生成表示式,自定義函式,三角函式,sum函式等,具體實現還是比較簡單的,這裡說一下測試的心得。我生成的第一次資料,2k條資料和別人對拍,對拍沒問題但是他被hack了,bug原因是表示式後面的空格會造成異常,並且我看我生成的資料大多都是很長的表示式,無法檢驗極端情況。為此我做出了一些改進,全域性一個布林值極限測試,當極限測試為true的時候,大幅度減少項和因子隨機數量的上限,產生的隨機數字以極高的概率出現 0 1 -1,並且有小概率出現intmax,這樣修改之後自動生成資料已經可以做到覆蓋很多極限情況,長度原因僅僅展示一小部分,實際中資料比展示的還要極限,我在檢查資料強度也看到了類似於sin(0)**0 +-(+-0)**0這種很極限的資料,整體而言資料強度還是很不錯的,雖然沒有測出我的sum函式bug,不過在我剛做完的時候以及互測環節還是測出來不少bug。
對拍程式則是參考了討論區的做法,用了python的庫,帶入十個點檢驗是否相等,不是原創這裡不多介紹。
有了這些,測試別人的bug我是直接選擇極限資料黑盒測試跟我自己程式碼對拍,由於資料強度還算可以,測試效果還是很顯著的,第三次互測中抓了別人五個bug(第一刀直接三殺)。
第四部分 心得體會
整體第一單元做下來還是感覺比較簡單的,第二次作業給之後迭代留的自定義名稱變數(變數名字不限於xyzi),任意巢狀(巢狀可以任意巢狀),以及預想了求導 e*x lnx 怎麼處理現在都沒有用上。我覺得能輕鬆完成第一次作業要歸結於以下兩點:第一點在於提前留好迭代視窗:也就是提前猜測一下後面會如何迭代,甚至可以順手直接實現。我身邊不少人第二次作業到第三次作業迭代中幾乎沒怎麼動程式碼,第三次新增的內容完全在第二次預留的迭代處理中,極大的減少了OO課程鎖佔用的時間。第二點我想在於提前架構好UML類圖再去實現程式碼,在架構好UML之後理順一遍完整的實現邏輯,打程式碼的時候幾乎不會停頓,時刻知道自己要去實現什麼,這個函式是為了什麼。第一次作業我是週三晚上八點架構完UML類圖,當天就完成了所有程式碼(bug是第二天懟的)。在架構設計中就要提前想好重要的函式的大致的實現過程,並且忽略不重要的細節如lexer實現等,並且給自己後續可能追加的優化留出地方,留出空間,如我在架構中設計了三角函式的優化,可以從抗原類的程式碼中中看出來預留給後續優化的部分(如傳入字串的建構函式,正常情況下是不需要字串構造的,僅僅在三角化簡中便於處理所以寫的這個生成函式)。總而言之,在寫程式碼之前設計好完整的架構是很重要的,不僅減少bug數量,還可以提高程式碼速度。
以上文章若有不正確或者處理不好的地方,還請老師助教同學們不吝賜教!