結對專案:一個自動生成小學四則運算題目的命令列程式(c++)
結對程式設計
這個作業屬於哪個課程 | 軟體工程 |
---|---|
這個作業要求在哪裡 | 作業要求 |
這個作業的目標 | 實現一個自動生成小學四則運算題目的命令列程式,以及對給定的題目檔案、答案檔案有統計對錯功能。 |
成員 | 3118005386 吳永力 、3118005382 王舜鑫 |
作業Github地址
- GitHub連結
- 可執行PrimaryArithMetic.exe已放在Release資料夾上
PSP表格
PSP2.1 | Personal Software Process Stages |
預估耗時(分鐘) | 實際耗時(分鐘) |
---|---|---|---|
Planning | 計劃 | 60 | 60 |
· Estimate | · 估計這個任務需要多少時間 | 60 | 60 |
Development | 開發 | 800 | 1125 |
· Analysis | · 需求分析 (包括學習新技術) | 120 | 120 |
· Design Spec | · 生成設計文件 | 60 | 60 |
· Design Review | · 設計複審 | 30 | 30 |
· Coding Standard | · 程式碼規範 (為目前的開發制定合適的規範) | 20 | 15 |
· Design | · 具體設計 | 60 | 180 |
· Coding | · 具體編碼 | 400 | 600 |
· Code Review | · 程式碼複審 | 50 | 60 |
· Test | · 測試(自我測試,修改程式碼,提交修改) | 60 | 60 |
Reporting | 報告 | 130 | 110 |
· Test Report | · 測試報告 | 60 | 60 |
· Size Measurement | · 計算工作量 | 10 | 20 |
· Postmortem & Process Improvement Plan | · 事後總結, 並提出過程改進計劃 | 60 | 30 |
· 合計 | 990 | 1295 |
效能分析
在-n 10000 -r 100 5 100 前提下進行測試
CPU時間佔比:
函式呼叫佔比:
可以看到主要時間都集中在計算式子計算過程不會出現負數,除以0以及生成的式子等價的函式上,甚至大量的string操作符上。
一開始用生成的式子與之前生成的式子進行等價判斷,導致了時間複雜度有O(n!),程式會卡很久才可以生成,後續優化了演算法就可以快速生成。(當答案相等時候才進行等價判斷)
設計實現過程
-
通用數類:觀察題目發現要表示分數以及所有整數也可以用分數來表示,即實現一個通用的類Frac就可以實現全部數一起用
class Frac { public: Frac(); Frac(long long molecule, long long denominator); Frac(std::string ex); ~Frac(); Frac operator+(const Frac& right) const; Frac operator-(const Frac& right) const; Frac operator*(const Frac& right) const; Frac operator/(const Frac& right) const; bool operator==(const Frac& right) const; bool operator!=(const Frac& right) const; //化簡 void Simplify(); //最大公因數 long long MaxDiviSor( long long a, long long b); //轉換為字串 std::string to_string() const; private: long long m_Up; //分子 long long m_Down; //分母 };
-
題目生成
-
隨機生成一定數量的操作符(+ - x ÷),數量範圍在1~3;
-
根據範圍引數生成以及操作符數量生成隨機指定數量的數(分成整數和真分數)
-
根據操作符數量來生成括號
- 操作符為1個:即不產生括號
- 操作符數量為2個:若生成的括號數量為1個時候,根據隨機位置來決定括號的位置(如:(1+2)+3以及1+(2+3) );若為0個,即不產生括號
- 操作符數量為3個:若生成的括號數量為2個時候,括號直接放在兩側(如:(1+2)+(3+4) );若為一個時候,即根據隨機數來決定放在右側還是左側;若為0個,也是不產生括號。
-
判斷式子是否有計算過程有負數以及除以0的情況;若存在,則重複上述步驟。
--根據式子的字尾表示式生成子表示式,再根據子表示式計算判斷是否有計算過程有負數以及除以0
-
判斷式子是否與之前生成的式子存在等價的情況;若存在,則重複上述步驟。
--根據式子的字尾表示式生成子表示式,再根據子表示式來進行判斷式子是否存在等價
-
若生成成功,即可以生成答案並把題目和答案存起來。
-
-
計算結果
- 首先把式子規整化,如空格剔除,以及把 ÷轉換為# (因為單字元匹配不了÷)以及補零(如:\(-2+3\) -> \(0-2+3\) ; \(1+(-2*3)\)-> \(1+(0-2*3)\))
- 然後把中綴表示式轉換為字尾表示式,若數值為真分數即把真分數直接當作數字存起來
- 最後根據字尾表示式來計算結果
關鍵函式
void CreateAriTitlesAndAnswer(int titleNums = 10, long long naturlrange = 10, long long fracrange = 5, long long downrange = 10);
- 引數:題目數量,自然數範圍,真分數範圍和分母範圍
- 具體功能:生成題目
bool Cmp_ExChild(std::vector<std::string> ex1, std::vector<std::string>ex2);
- 引數:子表示式1的陣列和子表示式2的陣列(陣列索引:0:運算元1 ;1:運算元2;2:運算子)
- 返回值:bool
- 具體功能:判斷兩個子表示式是否等價
bool IsSimilary(std::string scource,std::string destination);
- 引數:原表示式和要比較的目的表示式
- 返回值:bool
- 具體功能:判斷兩個表示式是否等價
std::string SuppleInfix(std::string infix);
- 引數:中綴表示式
- 返回值:字串
- 具體功能:規整化中綴表示式
bool ReversePolish(std::string infix);
- 引數:中綴表示式
- 返回值:bool(表示是否轉換成功)
- 具體功能:生成字尾表示式並存起來
std::string GetResult(std::string infix);
- 引數:中綴表示式
- 返回值:計算結果的字串
- 具體功能:輸入中綴表示式即可得到計算結果
std::vector<std::vector<std::string>> GetChildExpression(std::stack<std::string> reversePolish);
- 引數:字尾表示式
- 返回值:子表示式的二維陣列
- 具體功能:根據字尾表示式得到一個子表示式的二維陣列
關鍵程式碼展示
-
生成題目部分
void PriAriCreator::CreateAriTitlesAndAnswer(int titleNums, long long naturlrange, long long fracrange, long long downrange) { for (int i = 0; i < titleNums;) { int opNum = random_Int(1, mOpNumMax); std::vector<Frac> numbers; for (int i = 0; i <= opNum; ++i) { //產生隨機數(整數還是分數) int isCreate = random_Int(0, 1); if (isCreate == 0) { long long num_R = random_LL(1, naturlrange); Frac frac(num_R, 1); numbers.push_back(frac); } else if (isCreate == 1) { long long num_2 = random_LL(1, downrange); long long num_1 = random_LL(1, fracrange * num_2); Frac frac(num_1, num_2); numbers.push_back(frac); } } std::vector<std::string> ops; //隨機生成符號 for (int i = 0; i < opNum; ++i) { std::string op = random_OP(); ops.push_back(op); } int bracketNum = random_Int(0, opNum - 1); std::string infix = ""; if (opNum == 1) { infix += numbers[0].to_string() + ops[0] + numbers[1].to_string(); } else if (opNum == 2) { if (bracketNum == 1) { int position = random_Int(0, 1); if (position == 0) { infix += "(" + numbers[0].to_string() + ops[0] + numbers[1].to_string() + ")"; infix += ops[1] + numbers[1].to_string(); } else { infix += numbers[0].to_string() + ops[0]; infix += "(" + numbers[1].to_string() + ops[1] + numbers[2].to_string() + ")"; } } else if(bracketNum==0) { infix += numbers[0].to_string() + ops[0] + numbers[1].to_string() + ops[1] + numbers[2].to_string(); } } else if (opNum == 3) { if (bracketNum == 0) { infix += numbers[0].to_string() + ops[0] + numbers[1].to_string() + ops[1] + numbers[2].to_string()+ops[2]+numbers[3].to_string(); } else if (bracketNum == 1) { int position = random_Int(0, 2); if (position == 0) { infix += "(" + numbers[0].to_string() + ops[0] + numbers[1].to_string() + ")"; infix += ops[1] + numbers[1].to_string() + numbers[2].to_string() + ops[2] + numbers[3].to_string(); } else if (position == 1) { infix += numbers[0].to_string() + ops[0]; infix += "(" + numbers[1].to_string() + ops[1] + numbers[2].to_string() + ")"; infix += ops[2] + numbers[3].to_string(); } else if (position == 2) { infix += numbers[0].to_string() + ops[0] + numbers[1].to_string(); infix += ops[1] + "(" + numbers[2].to_string() + ops[2] + numbers[3].to_string() + ")"; } } else if (bracketNum == 2) { infix += "(" + numbers[0].to_string() + ops[0] + numbers[1].to_string() + ")"; infix += ops[1] + "(" + numbers[2].to_string() + ops[2] + numbers[3].to_string() + ")"; } } if (IsCreaateNegative(infix) || IsDownZero(infix)) //過程不能有負數以及除0 { continue; } std::string answer = mCalculator.GetResult(infix); bool flag = false; for (size_t i = 0; i < mAnswers.size(); ++i) { if (mAnswers[i]._Equal(answer) && IsSimilary(infix, mInfixs[i])) { flag = true; break; } } if (!flag) { mInfixs.push_back(infix); mAnswers.push_back(answer); ++i; } } SaveTitleToFile(); SaveAnswerToFile(); }
-
字尾計算式子結果
std::string Calculator::CalcuReversePolish() { if (number.empty()) { return ""; } std::stack<Frac>* temp = new std::stack<Frac>; std::string str; while (!number.empty()) { str = number.top(); number.pop(); if (!IsOp(str)) { Frac frac(str); temp->push(frac); } else { if (temp->size() > 1) { Frac first, second; first = temp->top(); temp->pop(); second = temp->top(); temp->pop(); char c_op = str.at(0); switch (c_op) { case '+': second = second + first; temp->push(second); break; case '-': second = second - first; temp->push(second); break; case '*': second = second * first; temp->push(second); break; case '#': if (first == Frac(0, 1)) { return std::string("X"); // X代表無結果 } second = second / first; temp->push(second); break; default: break; } } } } Frac result = temp->top(); temp->pop(); delete temp; return result.to_string(); }
-
判斷式子相似
bool PriAriCreator::IsSimilary(std::string scource, std::string destination) { //獲取字尾表示式 std::stack<std::string> RP1 = mCalculator.GetReversePolish(scource); std::stack<std::string> RP2 = mCalculator.GetReversePolish(destination); //得到子表示式 std::vector<std::vector<std::string>> ex1 = mCalculator.GetChildExpression(RP1); std::vector<std::vector<std::string>> ex2 = mCalculator.GetChildExpression(RP2); for (auto& p : ex1) { for (auto& v : ex2) { if (Cmp_ExChild(p, v)) { return true; } } } return false; }
效果展示
-
題目生成
在 -n 10000 -r 50 5 10情況下
部分題目:
1: 1 * 10 = 2: 19 ÷ 5 = 3: 40 * 3'3/4 = 4: 21 ÷ 22 = 5: 28 * 17 = 6: 5 ÷ 1/3 = 7: 5 + 3'3/10 + 4 ÷ 4 = 8: 38 + 2'3/8 = 9: 4'1/9 + 27 = 10: 46 * 3 ÷ 30 * 2'2/3 = 11: (4'2/7 ÷ 3) + (21 ÷ 11) = 12: (2 ÷ 14) + (21 + 4'1/3) =
以及答案:
1: 10 2: 3'4/5 3: 150 4: 21/22 5: 476 6: 15 7: 9'3/10 8: 40'3/8 9: 31'1/9 10: 12'4/15 11: 3'26/77 12: 25'10/21
-
驗證答案
因為全部正確,所以只貼一部分出來了:
Correct:10000(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,.......
單元測試
單元測試:對一定情況下的題目生成和自身驗證
共有以下組:
樣例 | |
---|---|
1 | -n 10 -r 10 |
2 | -n 100 -r 100 |
3 | -n 100 -r 10 |
4 | -n 1000 -r 1000 |
5 | -n 1000 -r 10 6 10 |
6 | -n 1000 -r 10 6 50 |
7 | 驗證正確的檔案(含錯誤答案) |
8 | 驗證不存在的檔案 |
9 | -n 10000 -r 10 |
10 | -n 10000 -r 100 |
部分程式碼:
TEST_METHOD(TestMethod9)
{
PriAriCreator* Par = new PriAriCreator();
Par->CreateAriTitlesAndAnswer(10000, 10);
Assert::IsTrue(Par->ReadTitilesAndAnswer("Exercises.txt", "Answers.txt"));
if (Par->ReadTitilesAndAnswer("Exercises.txt", "Answers.txt"))
{
Par->VerificationAnswer();
}
delete Par;
}
TEST_METHOD(TestMethod10)
{
PriAriCreator* Par = new PriAriCreator();
Par->CreateAriTitlesAndAnswer(10000, 100);
Assert::IsTrue(Par->ReadTitilesAndAnswer("Exercises.txt", "Answers.txt"));
if (Par->ReadTitilesAndAnswer("Exercises.txt", "Answers.txt"))
{
Par->VerificationAnswer();
}
delete Par;
}
測試結果:
不得不說:c++還是挺快的
專案小結
分享經驗,總結教訓
要仔細檢視題目理解要求,不要一概而論,不然容易產生二義性導致多餘重複的工作;
在實現某些演算法時候,應注意其的時間複雜度,更注意下有無更好的實現方法,所以對於演算法部分仍要加強學習;以及寫程式碼注意程式碼複審,可以減少一定的Debug時間。
這是第一次合作程式設計,我們通過程式碼和註釋表達我們對這次作業的想法,這是一種很好的交流方式。在開發種遇到問題,我們及時地交流反饋,認真聆聽對方的意見,這讓我們可以很好地解決困難。
成敗得失
- 未實現圖形介面(有想法用QT,但時間不夠放棄了)
- 還有有一定的程式碼耦合度,以後得繼續學習寫耦合度低的程式碼。
結對感受
結對感受 | 對彼此的閃光點或建議 | |
---|---|---|
吳永力 | 這次結對程式設計,有同夥的幫助,幫助讓我更好的理解了題目的要求以及更好地開發介面模組,有小夥伴(或團隊)有時候可讓開發效率更高。 | 有一定的學習能力以及題目理解能力。 |
王舜鑫 | 這次結對程式設計,有個厲害的同伴的幫助,讓我自身的能力有了一定的提高,也讓我節省了許多查詢資料的時間,對比之前,學習效率高了很多,團隊的力量就是如此強大。 | 學習能力強,很強的責任感,很強的團隊精神。 |
作者:Ligo丶
出處:https://www.cnblogs.com/Ligo-Z/
本文版權歸作者和部落格園共有,歡迎轉載,但必須給出原文連結,並保留此段宣告,否則保留追究法律責任的權利。