结对项目:一个自动生成小学四则运算题目的命令行程序(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/

本文版权归作者和博客园共有,欢迎转载,但必须给出原文链接,并保留此段声明,否则保留追究法律责任的权利。

原文地址:https://www.cnblogs.com/Ligo-Z/p/13805994.html