1. 程式人生 > 實用技巧 >結對專案:一個自動生成小學四則運算題目的命令列程式(c++)

結對專案:一個自動生成小學四則運算題目的命令列程式(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!),程式會卡很久才可以生成,後續優化了演算法就可以快速生成。(當答案相等時候才進行等價判斷)

設計實現過程

  1. 通用數類:觀察題目發現要表示分數以及所有整數也可以用分數來表示,即實現一個通用的類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;				//分母
    };
    
  2. 題目生成

    1. 隨機生成一定數量的操作符(+ - x ÷),數量範圍在1~3;

    2. 根據範圍引數生成以及操作符數量生成隨機指定數量的數(分成整數和真分數)

    3. 根據操作符數量來生成括號

      • 操作符為1個:即不產生括號
      • 操作符數量為2個:若生成的括號數量為1個時候,根據隨機位置來決定括號的位置(如:(1+2)+3以及1+(2+3) );若為0個,即不產生括號
      • 操作符數量為3個:若生成的括號數量為2個時候,括號直接放在兩側(如:(1+2)+(3+4) );若為一個時候,即根據隨機數來決定放在右側還是左側;若為0個,也是不產生括號。
    4. 判斷式子是否有計算過程有負數以及除以0的情況;若存在,則重複上述步驟。

      --根據式子的字尾表示式生成子表示式,再根據子表示式計算判斷是否有計算過程有負數以及除以0

    5. 判斷式子是否與之前生成的式子存在等價的情況;若存在,則重複上述步驟。

      --根據式子的字尾表示式生成子表示式,再根據子表示式來進行判斷式子是否存在等價

    6. 若生成成功,即可以生成答案並把題目和答案存起來。

  3. 計算結果

    1. 首先把式子規整化,如空格剔除,以及把 ÷轉換為# (因為單字元匹配不了÷)以及補零(如:\(-2+3\) -> \(0-2+3\) ; \(1+(-2*3)\)-> \(1+(0-2*3)\))
    2. 然後把中綴表示式轉換為字尾表示式,若數值為真分數即把真分數直接當作數字存起來
    3. 最後根據字尾表示式來計算結果

關鍵函式

void CreateAriTitlesAndAnswer(int titleNums = 10, long long naturlrange = 10, long long fracrange = 5, long long downrange = 10);
  1. 引數:題目數量,自然數範圍,真分數範圍和分母範圍
  2. 具體功能:生成題目
bool Cmp_ExChild(std::vector<std::string> ex1, std::vector<std::string>ex2);
  1. 引數:子表示式1的陣列和子表示式2的陣列(陣列索引:0:運算元1 ;1:運算元2;2:運算子)
  2. 返回值:bool
  3. 具體功能:判斷兩個子表示式是否等價
bool IsSimilary(std::string scource,std::string destination);
  1. 引數:原表示式和要比較的目的表示式
  2. 返回值:bool
  3. 具體功能:判斷兩個表示式是否等價
std::string SuppleInfix(std::string infix);
  1. 引數:中綴表示式
  2. 返回值:字串
  3. 具體功能:規整化中綴表示式
bool ReversePolish(std::string infix);
  1. 引數:中綴表示式
  2. 返回值:bool(表示是否轉換成功)
  3. 具體功能:生成字尾表示式並存起來
std::string GetResult(std::string infix);
  1. 引數:中綴表示式
  2. 返回值:計算結果的字串
  3. 具體功能:輸入中綴表示式即可得到計算結果
std::vector<std::vector<std::string>> GetChildExpression(std::stack<std::string> reversePolish);
  1. 引數:字尾表示式
  2. 返回值:子表示式的二維陣列
  3. 具體功能:根據字尾表示式得到一個子表示式的二維陣列

關鍵程式碼展示

  1. 生成題目部分

    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();
    }
    
  2. 字尾計算式子結果

    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();
    }
    
  3. 判斷式子相似

    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;
    }
    

效果展示

  1. 題目生成

    在 -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
    
  2. 驗證答案

    因為全部正確,所以只貼一部分出來了:

    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/

本文版權歸作者和部落格園共有,歡迎轉載,但必須給出原文連結,並保留此段宣告,否則保留追究法律責任的權利。