Suduku项目设计
一.Github项目地址及队友博客链接
Github地址:
https://github.com/feimo49/four-operations.
队友博客地址:
https://blog.csdn.net/weixin_40629184.
二.开发时间预估
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | |
· Estimate | · 估计这个任务需要多少时间 | 30 | |
Development | 开发 | 1930 | |
· Analysis | · 需求分析(包括学习新技术) | 90 | |
· Design Spec | · 生成设计文档 | 60 | |
· Design Review | · 设计复审(和同事审核设计文档) | 30 | |
· Coding Standard | · 代码规范(为目前的开发制定合适的规范) | 30 | |
· Design | · 具体设计 | 120 | |
· Coding | · 具体编码 | 1200 | |
· Code Review | · 代码复审 | 60 | |
· Test | · 测试(自我测试,修改代码,提交修改) | 240 | |
Reporting | 报告 | 190 | |
· Test Report | · 测试报告 | 120 | |
· Size Measurement | · 计算工作量 | 10 | |
· Postmortem & Process Improvement Plan | · 事后总结,并提出过程改进计划 | 60 | |
· | 合计 | 2150 |
三.解题思路
项目要求
对于本次结对项目,我们小组选择了实现四则运算题目生成项目。
该项目要求实现基于控制台的四则运算题目生成器,共分为三个阶段:
I.第一阶段:
- 可一次生成1000个不重复的题目并写入文件中。我们通过命令行参数 -i n实现(n为生成题目数量)
例:four_op.exe -i 1000。
-实现对四则运算表达式求解,运算符不超过10个。 - 支持真分数的四则运算
- 程序可接受用户输入答案,并判断对错,最后给出总的 对/错。
II.第二阶段
- 支持乘方运算,且乘方有两种表示方式,mode1:^;mode2:**,用户可通过设置选择模式。
III.第三阶段
对程序进行扩展,我们选择了第一个扩展方向,使用C#将程序变为一个Windows电脑图形界面的程序,要求:
- 增加倒计时功能。每道题应在20s内完成,否则得0分并开始下一题。
- 增加历史记录功能。将用户的做过的题目记录下来,并显示用户得分情况。
思考过程
程序的核心算法主要为两部分,生成运算题目和求解答案。
生成题目
生成题目的主要思路为生成随机运算数、生成随机运算符和括号、检测并调整生成式。
具体流程为首先生成一个随机的算式长度,之后逐个字符生成算式。当当前字符不是算式的最后一个字符时,生成一个随机数存入当前字符数组中,并在生成算式的过程中调整其格式,随机生成运算符;当当前字符是算式的最后一个字符时,增加了强制匹配右括号的机制。
在生成题目的过程中还需要考虑很多细节问题,在分析后改进如下:
- 随机生成的题目长度要合适。本程序通过正态分布函数使式子长度大多在5左右。
- 随机生成运算符时要考虑题目难度。本程序监测乘方数量,使其个数保持为1、2个,同时避免出现连续乘方出现。
- 除数不能为0,因此要检测除数是否为0。
- 考虑结果溢出。乘方后不应跟太大的数。本程序控制幂值不大于3。
- 括号的生成要合理,除号后的括号内结果不能为0,乘方后的括号内结果不能过大。本程序对除号和乘方进行监测,避免其后出现左括号来解决上述问题。另外若式中有未匹配的左括号需要进行强制匹配。
- 真分数的计算。本程序中定义了num类,整数和分数都用分数来表示,使用symbol变量对其进行区分。在类中设计了对分数的化简机制,并且对运算符进行了重载,num类的实例可直接进行运算。
求解题目
求解四则运算表达式以前就实现过,思路很简单,将表达式转换为逆波兰表达式:将操作数和运算符分别存储在两个栈中,按规则弹出操作数和运算符进行运算。
- 数字压入操作数栈
- 左括号压入运算符栈
- 右括号,运算符栈持续弹出直至左括号被弹出
- *定义运算符优先级,遇到运算符,则运算符栈持续弹出直至栈顶运算符优先级小于当前运算符或栈顶为左括号或栈空
- 每弹出一个运算符都弹出两个操作数,进行计算后将结果压入操作数栈
- 运算符栈为空时式子处理完毕,操作数栈栈顶操作数即为式子结果。
注:
1.操作数均为num类的实例(以分数形式表示),以上计算实际均为被重载的运算。
2.本程序在计算前对表达式进行了预处理,将表达式映射至一个整型数组(运算符分别映射至101-107的整数),在求解时处理的题目为预处理后的整型数组。
3.运算符的优先级定义如下(数字越小优先级越高):
\ | 运算符优先级 |
---|---|
^、( | 1 |
*、\ | 2 |
+、- | 3 |
) | 4 |
四.设计实现过程
程序流程图
类和模块说明
- num类
该类中定义了分子、分母、最大公约数几个属性,标记symbol和化简标志以及输出数字的方法,并对运算符进行了重载。
本程序中操作数均为num类的实例化。 - main.cpp
本模块为主模块,是程序的对外接口,包含主函数main(),主要实现命令的输入及解析、参数设定和模式选择,对不符合要求的命令报错,实现题目的生成及与用户的交互。 - Preprocessor.cpp
本模块为预处理模块,将算式转化为可处理的十进制串并存入一个整型数组中。
运算符与整数映射规则:
+:101; -:102; *:103; /:104; ^:105; (:106; ):107 - GenerateExp.cpp
本模块为生成表达式模块。主要函数为BuildExp()和PrintExp(),分别用于生成题目和写入文件(生成的题目写入ques.txt中)。 - Solver.cpp
本模块为求解题目的模块。主要函数为get_ans(),处理的题目为已经经过预处理的整型数组。根据运算规则计算结果并将其返回。 - Judge.cpp
本模块为判断正误模块。要求用户输入答案,并将用户输入的答案与正确结果相比较。主要函数为judge()和check(),分别用于判断答案是否正确和判断答案格式是否正确。
函数流程图
五.单元测试
- 单元测试用例设计如下
1.输入测试
主要测试程序的合法输入以及不合法输入情况,保证程序的安全性,其设计如下:
编号 | 输入格式 | 预期输出 |
---|---|---|
1 | -i 10 | 正常处理,随机生成10个算式 |
2 | Please input TWO parameters! | |
3 | -i | Please input TWO parameters! |
4 | -c 5 | Please input the correct form! |
5 | -i abc | Please input a number! |
2.运算符测试
检测四则运算器中每一种算符的运算正确性,该层检测正确才可以进行进一步的程序分析,其中对于^运算符分别进行正常整型运算、幂指数为0以及底数为分数三种情况讨论,其设计如下:
编号 | 操作数1 | 操作数2 | 运算符 | 预期结果 |
---|---|---|---|---|
1 | 1 | 1/2 | + | 3/2 |
2 | 1 | 1/2 | - | 1/2 |
3 | 3 | 1/2 | * | 3/2 |
4 | 3 | 1/2 | / | 6 |
5 | 3 | 2 | ^ | 9 |
6 | 3 | 0 | ^ | 1 |
7 | 1/2 | 1 | ^ | 1/2 |
3.题目查重测试
在四则运算中,由于存在加法交换律、乘法交换律以及左右结合律,故算式之间存在形式不同但逻辑运算相同的情况,在本项目中需要对该类情况进行测试,其设计如下:
编号 | 算式1 | 算式2 | 预期结果 |
---|---|---|---|
1 | 1+2+3 | 3+(1+2) | 重复 |
2 | 1+2+3 | 3+2+1 | 不重复 |
3 | 3*4 | 4*3 | 重复 |
4 | 123 | 321 | 不重复 |
5 | (1+2)*3 | 3*(1+2) | 不重复 |
6 | 123 | 321 | 不重复 |
7 | (1+2)*(3+4) | (3+4)*(1+2) | 不重复 |
8 | (2-1)/(5-3) | (5-3)/(2-1) | 不重复 |
9 | (3+6)/(5-3) | (6+3)/(5-3) | 重复 |
10 | (1/2+2/3)+3/4 | 1/2+(2/3+3/4) | 重复 |
11 | (1/2+2/3)* 3/4 | 3/4 *(1/2+2/3) | 重复 |
六.性能分析及改进
在性能分析阶段,由于在诊断过程中需要不断与用户进行交互,故其诊断会话时间较长,达到了1分钟20秒。
由分析报告可见,在程序中主函数main()占用的CPU比例最大,其主要原因是在主函数中进行了文件的打开和读操作,占用CPU比例较大;将用户输入答案与正确结果进行比较的judge()函数其次,在其中进行对于用户输入答案的比对与反馈工作,需要与用户界面进行交互;打印当前生成的算式的PrintExp()函数再次之,其实现打印算式的同时还需要将生成的算式写入文件ques.txt中,需要消耗较多的CPU资源。这三个函数为项目中消耗CPU最多的三个主要函数。
七.代码说明
主函数
在主函数中实现了对输入参数的解析与判断、模式选择以及主要的生成、求解、输出接口设计等,是程序与用户交互的接口。
int main(int argc, char * argv[])
{
if (argc < 3)
{
printf("Please input TWO parameters!\n");
system("pause");
return 0;
}
if (!strcmp(argv[1], "-i"))
{
srand((unsigned)time(NULL));
int n = atoi(argv[2]);
cout << "您希望使用哪种方式表示乘方?(输入1选择模式1,输入2选择模式2)mode-1:^/mode-2:**" << endl;
int m;
cin >>m;
getchar();
ofstream OutputFile("ques.txt");
int ac = 0;
for (int i = 0; i < n; i++)
{
char *t;
num useranswer;
int * save = BuildExp(3);
t = PrintExp(m);
OutputFile << t;
if(judge(get_ans(save)))
ac++;
}
printf("本轮题目正确率:%d/%d\n", ac, n);
}
if (strcmp(argv[1], "-i")) //输入格式不合理的情况
{
printf("Please input in the correct form!\n");
system("pause");
return 0;
}
int flag = 0;
for (int i = 0; i < strlen(argv[2]);i++)
{
if (argv[2][i]<'0' || argv[2][i]>'9')
flag = 1;
}
if (flag == 1) //输入非数字的情况
printf("Please input a NUMBER!\n");
system("pause");
return 0;
}
num类
class num {
private:
int numerator; //分子
int denominator; //分母
int gcd; //最大公约数
int symbol; //运算符
int flag; //设置化简标志,防止重复化简
void get_gcd(int x, int y) //求最大公约数
{
if (y == 0)
gcd = x;
else
get_gcd(y, x%y);
}
void reduction() //化简
{
if (numerator != 0) //分子不为0
{
symbol = symbol * (numerator / abs(numerator))*(denominator / abs(denominator));
numerator = abs(numerator);
denominator = abs(denominator);
get_gcd(numerator, denominator);
}
else //分子为0
{
denominator = 1;
gcd = 1;
symbol = 1;
}
flag = 1;
}
public:
num();
num(int x);
num(int x, int y, int sign);
void print();
void print(char * formula, ofstream & outtofile);
friend num operator +(num &a, num &b);
friend num operator -(num &a, num &b);
friend num operator *(num &a, num &b);
friend num operator /(num &a, num &b);
friend num operator ^(num &a, num &b); //保证b的分母为1
friend int operator == (num &a, num &b);
};
该类中定义了分子、分母、最大公约数几个属性,以及输出数字的方法,并对运算符进行了重载。
通过num类本程序中所有操作数都用分数表示,简化了计算。
随机生成题目代码
//随机化设计
default_random_engine generator(time(NULL));
normal_distribution<double> lendis(5, 3);
normal_distribution<double> numdis(5, 2);
auto lendice = bind(lendis, generator);
auto numdice = bind(numdis, generator);
int Exp[50];
int p = 0;
//mode=1 基础,mode=2 包含分数,mode=3,包含乘方。
//随机生成式子长度
int RandExpLen()
{
int randnum = lround(lendice());
if (randnum < 2)
randnum = 2;
else if (randnum > 10)
randnum = 10;
return randnum;
}
//随机生成算符
int RandSymbol(int mode)
{
int randnum;
if (mode == 1)
randnum = rand() % 4 + 101;
else if (mode == 2)
randnum = rand() % 4 + 101;
else if (mode == 3)
randnum = rand() % 5 + 101;
else if (mode == 4)
randnum = rand() % 2;
return randnum;
}
//随机生成式子中数字个数
int RandExpNum(int maxnum)
{
int randnum = lround(numdice());
if (randnum < 0)
randnum = 0;
else if (randnum > maxnum)
randnum = maxnum;
return randnum;
}
//随机生成一个1~3的数字
int GetEasy()
{
return rand() % 3 + 1;
}
//生成算式
int* BuildExp(int mode)
{
memset(Exp, 0, sizeof(Exp));
bool HavePow = false;
int expnum = RandExpLen();
int lastbracket = 0;
p = 0;
for (int j = 1; j <= expnum; j++)
{
if (j == expnum)//最后一个数字的判断
{
Exp[p++] = RandExpNum(10);
if (Exp[p - 2] == 105)
Exp[p - 1] = GetEasy(); //返回一个1~3的数
if (Exp[p - 2] == 104 && Exp[p - 1] == 0)//判断分母0
Exp[p - 1] = 1;
if (lastbracket != 0)//若有未匹配左括号,则最后一位强制添加右括号
Exp[p++] = 107;
break;
}
else
{
Exp[p++] = RandExpNum(10);//生成随机数
if (Exp[p - 2] == 105)
Exp[p - 1] = GetEasy();
if (Exp[p - 2] == 104 && Exp[p - 1] == 0)//判断分母0
Exp[p - 1] = 1;
if (RandSymbol(4) && lastbracket > 2)//右括号
{
Exp[p++] = 107;
lastbracket = 0;
}
Exp[p++] = RandSymbol(mode);//生成随机符号
{//检查乘方个数
if (Exp[p - 1] == 105 && HavePow)
Exp[p - 1] = RandSymbol(1);
else if (Exp[p - 1] == 105)
HavePow = true;
}
if (RandSymbol(4) && j < expnum - 1 && lastbracket == 0 && Exp[p - 1] < 104)//左括号
{
Exp[p++] = 106;
lastbracket = 1;
}
}
if (lastbracket != 0)
lastbracket++;
}
return Exp;
}
上述代码用于随机生成指定个数个运算表达式。
解运算式关键代码
extern int p;
//运算符和可处理十进制数之间的转换
num cal(num n1, num n2, int opera)
{
if (opera == 101)
return n1 + n2;
else if (opera == 102)
return n1 - n2;
else if (opera == 103)
return n1 * n2;
else if (opera == 104)
return n1 / n2;
else if (opera == 105)
return n1 ^ n2;
}
//将四则运算映射到一串十进制数,0-100为运算数
//其中101-104分别代表+,-,*,/,105为乘方,106为左括号,107为右括号
num get_ans(int * operation)
{
stack <int> operators;
stack <num> operand;
for (int i = 0; i < p; i++)
{
if (operation[i] >= 0 && operation[i] <= 100)
{
num temp(operation[i]);
operand.push(temp);
}
else if (operation[i] == 105 || operation[i] == 106) //左括号与乘方必定入栈
operators.push(operation[i]);
else if (operation[i] == 103 || operation[i] == 104) //乘除会弹出乘方与乘除
{
while (!operators.empty() && (operators.top() == 103 || operators.top() == 104 || operators.top() == 105))
{
int opera = operators.top();
operators.pop();
num n1 = operand.top();
operand.pop();
num n2 = operand.top();
operand.pop();
operand.push(cal(n2, n1, opera));
}
operators.push(operation[i]);
}
else if (operation[i] == 101 || operation[i] == 102) //加减可能弹出乘除与乘方
{
while (!operators.empty() && (operators.top() != 106 && operators.top() != 107))
{
int opera = operators.top();
operators.pop();
num n1 = operand.top();
operand.pop();
num n2 = operand.top();
operand.pop();
operand.push(cal(n2, n1, opera));
}
operators.push(operation[i]);
}
else if (operation[i] == 107) //右括号会一直弹出直至左括号
{
while (operators.top() != 106)
{
int opera = operators.top();
operators.pop();
num n1 = operand.top();
operand.pop();
num n2 = operand.top();
operand.pop();
operand.push(cal(n2, n1, opera));
}
operators.pop();
}
}
while (!operators.empty())
{
int opera = operators.top();
operators.pop();
num n1 = operand.top();
operand.pop();
num n2 = operand.top();
operand.pop();
operand.push(cal(n2, n1, opera));
}
return operand.top();
}
上述代码用求解题目。
八.程序扩展:four_operationsGUI
我们选择了第一个扩展方向,使用C#将程序变为一个Windows电脑图形界面的程序。
主界面效果如下:
点击START按钮开始答题。
答题界面如下:
功能介绍
开始答题后界面中央随机生成一道运算题目
- 用户可在题目下方的文本框中输入答案。输入中支持分数,且可以不进行约分;不支持小数(为了避免无法除尽的情况)。
- 屏幕左上角为计时器,用户需在20s内完成题目,否则计0分并提示时间到,进入下一题。
- 右上角为计分器,显示用户当前分数。
- SUBMIT按钮用提交用户的结果,计时停止,若结果正确,显示“bingo!”,得一分;否则显示“Wrong”,并给出正确答案。提交后自动生成下一道题目并重新开始计时
- History按钮用于显示历史记录。点击按钮,弹出历史记录窗口。
左侧为用户做过的题目及正确答案;右上角显示用户的正确率。点击BACK按钮返回答题界面。 - QUIT按钮用于退出游戏,用户点击则关闭窗口,退出答题。
具体设计
我们使用C#实现winform图形界面。
首先需要把之前c++的代码用c#进行重写并封装。在重写过程中,除了语法上需要修改,还有一些功能需要更进一步的整合和封装。由于C#中每一个文件就是一个类,所以我们把所有相同功能都封装到一个类中。
相关类重写完成后需要进行winform窗口的布局以及控件代码的编写。
核心代码实现如下:
- 提交按钮:
private void Submit_Click(object sender, EventArgs e)
{
if(Ans.Text=="")
{
Ans.Focus();
return;
}
Judge ans = new Judge();
Num correct_ans = solve.get_ans(save, Generate.p);
int ansflag = ans.judge(correct_ans, this.Ans.Text);
correct_ans_str = correct_ans.c_Tostring();
timu = Generate.C_Tostring();
f3.History_Add(timu, correct_ans_str);
if (ansflag==1)
{
timer1.Stop();
MessageBox.Show("Bingo!");
timer1.Start();
grade+=1;
correct_cnt++;
ggrade.Text = "Grade: "+grade.ToString();
Start_Click(null, null);
this.Ans.Text = "";
totaltime = 20;
}
else if(ansflag==0)
{
timer1.Stop();
MessageBox.Show("Wrong!\n" + "Correct Answer:" + correct_ans_str);
timer1.Start();
Start_Click(null, null);
this.Ans.Text = "";
totaltime = 20;
}
else
{
timer1.Stop();
MessageBox.Show("Error:Please input the correct form!");
timer1.Start();
this.Ans.Text = "";
this.Ans.Focus();
}
f3.Correct_Rate(correct_cnt, cnt-1);
}
- 历史记录按钮
//打开历史记录窗口
private void History_Click(object sender, EventArgs e)
{
f3.Show();
}
//每次生成题目时通过f3.History_Add(timu, correct_ans_str),将题目和正确答案传入f3
public void History_Add(String Text1,String Text2)
{
record.Text += Text1 + "=" + Text2+"\r\n";
}
public void Correct_Rate(int c_cnt, int cnt)
{
correct_rate.Text = "Correct Rate : " + c_cnt + "/" + cnt;
}
九.PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 35 |
· Estimate | · 估计这个任务需要多少时间 | 30 | 35 |
Development | 开发 | 1930 | 2005 |
· Analysis | · 需求分析(包括学习新技术) | 90 | 60 |
· Design Spec | · 生成设计文档 | 60 | 60 |
· Design Review | · 设计复审(和同事审核设计文档) | 30 | 30 |
· Coding Standard | · 代码规范(为目前的开发制定合适的规范) | 30 | 15 |
· Design | · 具体设计 | 120 | 120 |
· Coding | · 具体编码 | 1200 | 1345 |
· Code Review | · 代码复审 | 60 | 75 |
· Test | · 测试(自我测试,修改代码,提交修改) | 240 | 300 |
Reporting | 报告 | 190 | 175 |
· Test Report | · 测试报告 | 120 | 100 |
· Size Measurement | · 计算工作量 | 10 | 15 |
· Postmortem & Process Improvement Plan | · 事后总结,并提出过程改进计划 | 60 | 60 |
· | 合计 | 2150 | 2215 |
十.总结
通过本次结对项目的开发,我学习到了在多人共同开发项目时如何进行有效的分工合作。因为这次是和室友组队,有着一定的默契并且十分了解,我们在计划阶段共同讨论开发计划、制定设计文档,最终形成了较为成熟完整的方案。在开发过程中,经过讨论我们分别负责题目生成和题目求解部分,界面开发则共同完成,跟个人项目相比开发进度明显加快。但是过程中也出现了接口调用错误、封装不完整等问题,在经过检测后都一一解决。
另外在上次个人项目中主要还是使用的结构化设计方法,所以在整个项目的开发过程中遇到一些问题。这次吸取教训之后我们采用了面向对象设计方法,通过类来整合代码,使功能封装的更加好,后期测试和重写都十分方便。
关于winform图形界面的实现,我们先利用c#重写c++部分代码,对c#语言的了解更深一步,发现它在类的整合方面十分方便。因为时间关系,我们的界面还有很多可以优化的地方;另外在几次图形界面开发之后我感觉winform用于GUI开发还有很多不尽人意的地方,我决定以后学习以下基于Python图形界面的第三方库(例如tkinter、Qt、GTK等)的图形界面开发。
本次的结对项目开发总体开发还是十分顺利的,当然也还有许多可以改进的地方。在以后的学习或者工作过程中肯定还有很多需要合作进行开发的项目,有效的交流与合作可以产生1+1>2的效果,为了使开发更加顺利,需要严格按照标准的软件开发流程进行开发,编程时做好代码的封装,能够避免很多问题。