四則運算題目生成器項目筆記

本次做業是實現一個面向小學教師的四則運算題目生成程序,它的功能包括生成題目,以及根據題目計算結果並自動給同窗的做業打分。算法

預估時間及實際花費時間表格


從表格中能夠看出,用於設計和測試的時間較多。express

PSP2.1 Personal Software Process Stages Time
Planning 計劃
· Estimate · 估計這個任務須要多少時間 20h
Development 開發
· Analysis · 需求分析 (包括學習新技術) 1h
· Design Spec · 生成設計文檔 1h
· Design Review · 設計複審 (和同事審覈設計文檔) 1h
· Coding Standard · 代碼規範 (爲目前的開發制定合適的規範) 0.5h
· Design · 具體設計 2h
· Coding · 具體編碼 3h
· Code Review · 代碼複審 1h
· Test · 測試(自我測試,修改代碼,提交修改) 2h
Reporting 報告
· Test Report · 測試報告 3h
· Size Measurement · 計算工做量 0.5h
· Postmortem & Process Improvement Plan · 過後總結, 並提出過程改進計劃 1h
合計 16h

需求分析


根據老師的要求,所寫程序功能大致分爲兩部分:編程

  1. 根據參數生成知足要求的四則運算題目並同時生成答案
  2. 對同窗的答案進行評測

其中,第二個功能較爲簡單,就是一個經典的中綴表達式求值的問題,咱們能夠用兩個棧來隨便維護一下就好。數組

第一個功能中要求較多:數據結構

  1. 根據命令行參數決定生成題目的數量和題目中數值的大小
  2. 生成的全部題目都要保證在計算過程當中不能出現負數
  3. 每個題目中所含的運算符數不超過三個
  4. 生成的題目中的數字及結果若是爲分數,則必須爲真分數。
  5. 若是生成了多個題目,那麼這些題目應該是不一樣的。

不一樣定義爲:less

任何兩道題目不能經過有限次交換+和×左右的算術表達式變換爲同一道題目數據結構和算法

具體設計


首先分析第一個功能。函數

第一個要求很容易知足,經過控制一些參數便可實現。性能

第二個要求則須要對錶達式進行運算,若是出現了負數,則交換減號兩邊的表達式。(或採起其餘方式避免)學習

第三個要求至關於對題目的簡化。僅用考慮不超過三個運算符的表達式

第四個要求則須要咱們同時處理分數和整數。咱們考慮使用一個統一的Factor類來實現。

第五個要求是這些要求中較難的一個。它要求咱們生成的表達式不能有相同的計算順序。結合表達式的性質,咱們很容易就想到了樹的最小表示法

即用二叉樹的形式存放表達式,那麼只要兩棵樹的最小表示不一樣,則這兩個表達式必定是不一樣的。

而且因爲表達式中的運算符數量較少,將樹最小表示的過程能夠暴力的來作。

ps:剛開始沒有限制表達式的數量,那麼若是要對錶達式樹進行最小表示,則須要樹的每個節點均爲子表達式,而且須要對錶達式類重載小於運算符,實現起來較爲繁瑣。後來老師加上了對運算符的限制,則大大下降了編程複雜度。

類定義:

class Factor {
    int fraction;
    long long numerator, denominator;
}
class Expression {
    int opNum;
    Factor fac[FACTOR_SIZE];
    char op[FACTOR_SIZE];
}

上述代碼描述了本程序中關鍵的兩個類:factor和Expression。

Factor類實現了分數和整數的統一處理(包括四則運算、輸出等),其numerator屬性爲分子,denomination屬性爲分母,fraction屬性標記了其是否爲分數。

Expression類實現了表達式的處理,包括表達式的最小表示,處理出現負數和除數爲零的狀況等不合法狀況以及輸出。

有了上述基礎,表達式的生成就變得很是簡單了。

  1. 隨機一個1~3的變量,表示表達式中所含運算符的個數
  2. 隨機生成相應個數的運算符和數值。並初步構建表達式。
  3. 對錶達式的減法和除法中可能出現的問題進行檢測,若是有問題則對錶達式進行修改。
  4. 將表達式樹轉化爲最小表示,並判斷是否重複。
  5. 按照表達式樹的計算順序(即後序遍歷順序)生成帶括號的表達式字符串。
  6. 輸出該字符串。

至此,第一個功能就初步設計完畢了。

第二個功能則沒什麼好說的,就是讀入兩個文件,將兩個文件中保存的答案記錄下來並進行計算和比較,從而得出結果。須要注意的就是一些文件讀取的問題和文件內容不合法的判斷。

代碼實現中出現的問題


對於Factor和Expression類,我最初在每一個類中都定義了一個print()方法,用於輸出該類型的變量。然而以後發現因爲須要生成多個表達式,且每一個表達式生成後都要向兩個文件中寫入數據,那麼採用輸出重定向的方式就較爲不便。因而我將print()方法改成了與Java中相似的toString()方法,該方法會返回要輸出的字符串。這樣就能夠方便的使用文件流的方式來存取數據了。

對於將表達式進行最小表示後的判重問題,我剛開始的思路是開一個不少維的map來存表達式的全部運算符和數值。大概相似這個樣子:

map<Factor, map<Factor, map<Factor ,map<char, map <char, int> > > > > mp;

寫完以後發現編譯器給出了警告:

warning C4503: 'std::_Tree<std::_Tmap_traits<_Kty,_Ty,_Pr,_Alloc,false>>::erase' : decorated name length exceeded, name was truncated

查閱資料後發現,這是因爲模板展開後的標識符長度超過了編譯器限制。

那如何消除這個警告呢?後來我纔想到,既然要求表達式惟一,那麼直接將最小表示後的表達式字符串做爲key就行了嘛……

因此僅須要一個set<string>來存放不一樣的表達式便可。

對於輸入的參數中要求生成較多數量的表達式可是給出的表達式中數值得限制較小,就可能會出現湊不出要求的表達式數量的狀況。對於這種狀況,目前尚未想到完美的方法來保證全部可能的表達式均被生成。僅限定了一個閾值,若是在循環了這麼屢次後仍然沒有找到一個之前沒有出現的表達式,那麼就近似地認爲沒有更多的表達式了。

不過在實際應用中,應該不會出現這種極限的狀況。

代碼複審及測試


在代碼寫完後,我進行了Code Review和debug。

Code Review中,發現了兩處寫錯的地方,debug中,發現了三個細節問題(判斷除0錯誤時的邏輯問題、生成數值的時候fraction取值的問題以及文件讀寫的問題),並進行了改正。

在後續的測試中,發現了即便數值限制爲100,中間結果也有可能會超過int的範圍,所以必須採用long long類型的變量來存儲數值。

測試用例以下:

  • 生成題目TestCase1:
ExpressionGeneration.exe -r 10000 -n 1
pause
# generate Exercises.txt including a expression which values are less than 10000
# Answers.txt including the expression's answer
  • 生成題目TestCase2:
ExpressionGeneration.exe -n 5
pause
# input error,display the help message`
  • 生成題目TestCase3:
ExpressionGeneration.exe -n 5
pause
# input error,display the help message`
  • 生成題目TestCase3:
ExpressionGeneration.exe -n 5.1 -r 4
pause
# input invalid, display the help message
  • 生成題目TestCase4:
ExpressionGeneration.exe -n -r 4 5
pause
# input invalid, display the help message
  • 生成題目TestCase5:
ExpressionGeneration.exe -n 10000 -r 3
pause
# generate Exercises.txt including 10000 expressions which values are less than 3
# Answers.txt including the answers of all the expressions
  • 生成題目TestCase6:
ExpressionGeneration.exe -n 10000 -r 2
pause
# It doesn't have so many expressions

測試答案的測試用例因爲文件較大,在此再也不展現,圍繞文件讀取合法性、題目格式的正確性、答案格式的正確性、題目文件和答案文件是否匹配、答案正確的題目數量等方面設計測試用例便可。

性能測試


  • 生成10000道數值限制爲100的題目

pic

    • 從上圖中,咱們能夠看出,限制程序效率的的關鍵在於表達式的toString()方法。其次爲Factor(分數類)的toString()方法。因爲我沒有采用標準的樹結構來存儲表達式,而是採用了數組的形式,因此在轉化爲字符串的時候分類較多。此外,是用其中調用到的用於將數字轉化爲string的intToStr()方法中,屢次用到了對long long類型的數進行取模運算。
  • 生成10000道數值限制爲3的題目

pic

    • 從上圖中,咱們發現,排名第一的仍然是表達式類的toSting()方法,可是排名第二的是則是STL中set的相關方法。這是因爲數值限制很是小,從而使得出現重複表達式的機率較高,所以更多的表達式在set判重時被咔嚓掉了,從而致使set相關函數的運行時間較高。
  • 對10000道數值限制爲100的題目進行檢驗

pic

    • 檢驗題目時並無用到表達式類,而是直接讀取數據後使用兩個棧來維護,所以耗時排名第一的函數從expression的toString()方法變爲了Factor類的toString()方法。以後的就是一些STL庫函數了,這是用於我使用了STL中的棧。

不過總的來講,運行效率能夠接受。toString()方法也沒有太大的改進空間了。

總結與收穫


完成此次我的項目的過程當中,我進一步體會到作工程與解決數學問題的區別。

在實際項目中,需求每每不是可以僅從題目要求中讀出來的,更多的仍是要結合實際狀況進行分析。同時,作項目的目的是爲了服務生活,有時,咱們能夠根據生活中的實際狀況來對問題進行簡化。(好比此次項目中對運算符個數的限制),而不是一味地構想難解的問題。

此外,我也發現了數據結構和算法基礎在項目實現中的重要性。在本次項目中,若是可以想到平時常常用到的判斷樹同構問題的最小表示法的話,那麼這就是一個很是熟悉且優美的問題了。(不過個人代碼實現仍是不夠優美……)

相關文章
相關標籤/搜索