Github项目地址
链接: Github/Sheep11/calculator
写者id为:PanQuixote
题目要求
写一个能自动生成小学四则运算题目的软件,满足以下基本需求:
- 一次可以出一千道不相同的题目,把题目写入一个文件中。此处“不相同”的定义是:两道题目不能通过有限次交换 + 和 * 左右的算术表达式变换为同一个题目。
- 题目最多10个运算符,括号数量不限。
- 除了整数以外,要支持真分数的四则运算。
- 能让用户输入答案,并且判断答案对错。
- 支持乘方运算,乘方用 ^ 或者 ** 表示,由用户选择。
此外还有4个扩展方向,本小组选择的是第1个方向,此扩展方向要求如下:
- 把程序变成一个 Windows/Mac/Linux 电脑图形界面程序。
- 增加倒计时功能,每个题目要求在20秒内完成,完不成则记得分为0分并进入下一题。
- 增加历史记录功能,把做题的成绩记录下来,并且可以展示。
组内分工
写者负责实现基本需求,搭档负责实现扩展需求。
解题思路描述
- 生成题目:先生成一串合法的包含运算符和括号的string,再向其中添加数字。
- 求解题目:将题目转化成后缀表达式,再计算后缀表达式。
- 检查题目是否重复:将当前题目与之前的题目比较。先比较计算结果是否相同,如果不同则肯定不重复。如果相同则比较其表达式二叉树,如果两个表达式二叉树能通过交换 + 和 * 节点的子节点而转化为同样的树,则两个表达式是相同的。检查过程如下:将题目转化为一个表达式二叉树。将此题目的答案,与之前保存的答案比较,如果不同,则不重复;如果相同,将它的二叉树与之前的二叉树比较,如果不同,则不重复,否则重复。
算法设计
生成题目过程如下:
- 生成一个只含运算符和括号的string类。由于无法直接随机生成运算符和括号,于是采用间接方法。具体方法如下:生成一串数字,每个数字代表不同的符号,1代表 “+”,2代表 “-”,3代表 “*”,4代表 “/”,5代表 “^”,6代表 “(”,7代表 “)”。再将数字转化为符号。注意,此时的式子不一定是合法的,因为可能出现括号不匹配,或者两个不同的括号中间没有运算符的情况。
- 将生成的string规范化。采用的方法是,如果两个不同括号中间没有运算符,则随机添加一个运算符到中间;如果“)”没有与之匹配的“(”,则将其删除;如果“(”没有与之匹配的“)”,则增加一个“)”到串的末尾。
- 向string中运算符的前后添加随机数字。
求解题目过程如下:
- 将题目转化为后缀表达式。转化为后缀表达式的方法如下:(1)创建一个栈,用于存储运算符。(2)从左到右遍历表达式,如果遇到数字,直接输出;如果遇到左括号,将左括号压入运算符栈(3)如果遇到右括号,运算符栈不断出栈直到被弹出的为左括号。(4)如果遇到运算符:如果运算符栈为空,直接将其入栈;如果不为空,不断出栈直到当前运算符的算术级高于栈顶运算符的算术级。(5)表达式遍历完后,将操作符栈的所有元素出栈并输出。(6)按顺序保存以上操作的输出,即为后缀表达式。
- 计算后缀表达式。计算方法如下:(1)创建一个数字栈。(2)从左到右遍历后缀表达式,如果遇到的为数字,将其入栈。(3)如果遇到运算符,从数字栈弹出一个数字作为右运算数,再弹出一个数字作为左运算数,计算运算结果,将结果入数字栈。(4)表达式遍历完后,数字栈中的唯一元素即为最终的计算结果。如果数字栈中不只一个元素,则说明表达式有误。(注:由于可能会有分数出现,计算时将整数看做是分母为1的分数来计算)
检测题目是否重复的过程如下:
-
先比较当前式子和之前生成的式子的运算结果是否相同,如果不同,则当前式子不与之前的式子重复。如果相同,则要建立当前式子的表达式二叉树,与之前式子的表达式二叉树比较。建立表达式二叉树的过程如下:(1)创建一个二叉树节点栈。(2)将式子的后缀表达式中的数字、运算符都转化为二叉树节点。得到一个二叉树节点串形式的后缀表达式。(3)从左到右遍历后缀表达式。如果遇到的为数字节点,将其入栈。(4)如果遇到的为运算符节点,栈顶节点出栈,作为当前运算符的右节点。此时的栈顶节点再出栈,作为当前运算符的左节点。当前运算符入栈。
-
比较当前式子的表达式二叉树和之前式子的表达式二叉树。比较函数int compare_tree(bi_tree a, bi_tree b)的实现过程如下:(1)如果a为空或者b为空,返回1。(2)如果a、b其中一个为空,返回0。(3)如果a、b全为非空,如果a、b的值不同,返回0。(4)如果值相同,递归调用int compare_tree(a->left_child, b->left_child),如果返回值为1,再递归调用int compare_tree(a->right_child, b->right_child);如果返回值为0且a、b的值为 + 或 *,交换a的左右节点,递归调用int compare_tree(a->left_child, b->left_child),如果返回值为1,再递归调用int compare_tree(a->right_child, b->right_child)。最终实现的功能是,将两棵表达式树的根节点作为a、b传入函数,如果两棵树能通过有限次交换加号和乘号节点的左右节点转化为同一棵树,则认为两棵树相同,返回1,否则返回0。
数据结构设计
由于转化为后缀表达式时,表达式中既有数字又有符号,因而设计了一个类,名字为word,既可以存储数字,又可以存储符号。用于保存表达式中的元素。word的属性如下:
class word
{
int type;//type = 0则表示数字,等于1表示操作符,
//等于-1表示错误的结果(未初始化、出现除以0、表达式错误)
int num;//numerator分子
int de;//denominator分母
int oper;//操作符
}
此外,还要设计二叉树的节点。此节点中既要保存节点数据,还要有指向其左节点和右节点的指针。节点类的属性如下:
class bi_tree
{
word value;//当前节点的数据
bi_tree *l_child;//指向左节点的指针
bi_tree *r_child;//指向右节点的指针
}
在生成题目、计算出答案之后要将这些数据传给前端,因而要设计一个类用于保存一道题的题目和答案。此类的属性如下:
class formula
{
int id;//题目编号
string problem;//题目
string answer;//答案
}
设计了三个工具类,分别是计算工具类calculate_tool,转化工具类translate_tool,生成工具类generate_tool。三个类中的主要方法如下:
class calculate_tool
{
public:
//返回后缀表达式suffix的计算结果,如果表达式不正确,返回的result.type = -1
word calculate_suffix(std::queue<word> suffix);
};
class translate_tool
{
public:
//将exp转化为后缀表达式,存在队列里返回
queue<word> translate_into_suffix(const string exp);
};
class generate_tool
{
public:
//返回一个容器,里面存储着N个表达式及答案
vector<formula> generate_exp(int N = 1000, int max_number = 10, int max_oper_sum = 10, int show_way = 0);
};
设计了一个工具类,供前端使用。前端只需创建一个此类的对象,即可使用 生成题目、检测答案是否正确等功能。此类的定义如下:
class generator
{
private:
vector<formula> formula_vector;//存储题目和答案的容器
int sum;//存储的数量
int index;//没有使用过的第一个题目和答案的编号(编号数值等于下标+1)
public:
generator();//构造函数
//输出式子到文件path中,默认为problem_file.txt
void output_into_file(string exp, string path = "problem_file.txt");
//获取一个式子及答案,返回值为formula对象,当容器中没有未使用过的题目时,会自动生成新的题目
//默认显示乘方为“^”,如果需要切换为“**”,传入整型参数1
formula get_formula(int show_way = 0);
//检查答案是否正确。id为题号,u_answer为用户的答案
int check_answer(int id, string u_answer);
};
类图如下:
流程图
核心函数
表达式转化为后缀表达式
//将exp转化为后缀表达式,存在队列里返回
queue<word> translate_tool::translate_into_suffix(const string exp)
{
queue<class word>suffix;//存储后缀表达式的队列
stack<int>oper_stack;//暂时存储操作符的栈
for (int i = 0; i < exp.size(); i++)
{
int type = check_type(exp[i]);//当前字符的类型
if (type == 0)
continue;
if (type == -2)
{
while (suffix.size() != 0)
suffix.pop();
return suffix;
}
if (type == -1)//如果为数字
{
int num = exp[i] - '0';
for (i = i + 1; i < exp.size(); i++)
{
if (check_type(exp[i]) < 0)
num = num * 10 + (exp[i] - '0');
else
{
i--;
break;
}
}
word num_t(0, num);
suffix.push(num_t);//加入后缀式队列
continue;
}
else//如果为符号
{
//如果为括号
if (type == L_BRACKET)//如果是左括号,直接入操作符栈
{
oper_stack.push(L_BRACKET);
continue;
}
else if (type == R_BRACKET)//如果是右括号,不断将操作符栈的栈顶弹出到后缀式队列,直到遇到左括号(最后将左括号也出栈但不加入后缀式队列)
{
while (oper_stack.top() != L_BRACKET)
{
word tem_oper(1, oper_stack.top());
suffix.push(tem_oper);
oper_stack.pop();
}
oper_stack.pop();
continue;
}
//如果为运算符
if (oper_stack.empty())//如果操作符栈为空则直接入栈
{
oper_stack.push(type);
}
else//如果操作符栈不为空,当当前操作符不高于栈顶操作符的优先级时,不断出栈。最后将当前操作符入栈
{
//操作符栈非空且当前操作符的优先级不高于操作符栈顶的操作符
while ((!oper_stack.empty()) && cmp_priority(exp[i], change_oper(oper_stack.top())) <= 0)
{
word tem_oper(1, oper_stack.top());
suffix.push(tem_oper);
oper_stack.pop();
}
oper_stack.push(type);//当前操作符入后缀式队列
}
}
}
//将操作符栈中剩余的操作符全部弹出到后缀式队列
while (!oper_stack.empty())
{
word tem_oper(1, oper_stack.top());
suffix.push(tem_oper);
oper_stack.pop();
}
return suffix;
}
计算后缀表达式
//返回后缀表达式suffix的计算结果,如果表达式不正确,返回的result.type = -1
word calculate_tool::calculate_suffix(string exp)
{
translate_tool T_tool;
queue<word> suffix = T_tool.translate_into_suffix(exp);
word result;
class stack<word> num_stack;//数字栈
//后缀队列非空时
while (!suffix.empty())
{
word t_word;//临时存储从队列中弹出的元素
t_word = suffix.front();
suffix.pop();
if (t_word.type == 0)//如果为数字
{
num_stack.push(t_word);//入数字栈
}
else//如果为操作符
{
//从数字栈中弹出两个数字,计算结果,将结果入数字栈
word b = num_stack.top();
num_stack.pop();
word a = num_stack.top();
num_stack.pop();
word c = calculate(a, t_word, b);
if (c.type == -1)//如果计算结果出现错误
{
result.init(-1, 0, 0);
return result;
}
else
num_stack.push(c);
}
}
//如果最终数字栈里只有一个数字,说明表达式正确,否则错误
if (num_stack.size() == 1)
return num_stack.top();
else
{
result.init(-1, 0, 0);
return result;
}
}
转化后缀表达式为二叉树
//后缀表达式转换为二叉树
bi_tree generate_tool::translate_into_bi_tree(class queue<word> suffix)
{
stack<bi_tree> t_stack;//临时栈
//后缀队列非空时
while (!suffix.empty())
{
word t_word;//临时存储从队列中弹出的元素
t_word = suffix.front();
suffix.pop();
//bi_tree *a = new bi_tree();
if (t_word.type == 0)//如果为数字
{
bi_tree t_tree(t_word);
t_stack.push(t_tree);
}
else//如果为操作符
{
bi_tree* b = new bi_tree();
*b = t_stack.top();
t_stack.pop();
bi_tree* a = new bi_tree();
*a = t_stack.top();
t_stack.pop();
bi_tree* c = new bi_tree();
c->init(t_word, a, b);
t_stack.push(*c);
}
}
bi_tree return_value = t_stack.top();
return return_value;
}
比较两棵二叉树是否相同
//判断a,b的子树是否有交叉比较的必要(即是否a、b均为加号或乘号),如果有,返回1
int generate_tool::swap_flag(bi_tree a, bi_tree b)
{
//如果a,b存储的均为+或者*
if ((a.value.oper == PLUS && b.value.oper == PLUS) || (a.value.oper == MULTI && b.value.oper == MULTI))
return 1;
else
return 0;
}
//如果两棵树相同返回1,否则返回0
int generate_tool::compare_tree(bi_tree *a, bi_tree *b)
{
if (a == NULL && b == NULL)//如果a b均为叶子节点
return 1;
if ((a != NULL && b == NULL) || (a == NULL && b != NULL))//a b不全为叶子节点
return 0;
if (a && b)
{
if (cmp_word(a->value, b->value))//如果当前节点相同
{
if (compare_tree(a->l_child, b->l_child))//比较a、b的左节点
return compare_tree(a->r_child, b->r_child);//比较a、b的右节点
else
{
//如果有交叉比较的必要,交叉比较a、b的左右节点
if (swap_flag(*a, *b) && compare_tree(a->r_child, b->l_child))
return compare_tree(a->l_child, b->r_child);
}
}
}
return 0;
};
部分源码
生成题目工具类的部分方法
//生成N个表达式和答案,默认N为1000,存在栈中返回
//max_number为表达式中数字的最大值,默认为10
//max_oper_sum为表达式中最多含有的运算符的数量,默认为10
//show_way为显示乘方的模式,为0显示^,为1显示**,默认为0
vector<formula> generate_tool::generate_exp(int N, int max_number, int max_oper_sum, int show_way)
{
vector<formula> F_vector;//将要返回的容器
if (N > MAX_SIZE)
return F_vector;
srand(time(NULL));
string exp[MAX_SIZE];//表达式的字符串形式
bi_tree tree[MAX_SIZE];//表达式的二叉树形式
queue<word> suffix[MAX_SIZE];//后缀表达式
word result[MAX_SIZE];//表达式的计算结果
translate_tool T_tool;
calculate_tool C_tool;
generate_tool G_tool;
int n = 0;//当前的式子编号
while (n < N)
{
G_tool.add_oper_into_exp(exp[n], max_oper_sum);//添加操作符
G_tool.normalize_exp(exp[n]);//规范化
G_tool.add_number_into_exp(exp[n], max_number);//添加数字
suffix[n] = T_tool.translate_into_suffix(exp[n]);//转化为后缀表达式
result[n] = C_tool.calculate_suffix(suffix[n]);//计算后缀表达式的值
if (G_tool.restrict_result(exp[n], result[n]) == 0)//如果计算结果不符合要求,重新生成表达式
{
G_tool.clear_trail(exp[n], suffix[n], result[n], tree[n]);//清除记录
continue;
}
else//结果符合要求,检测是否与之前生成的表达式重复
{
tree[n] = G_tool.translate_into_bi_tree(suffix[n]);//由后缀表达式生成二叉树
int repeat_flag = G_tool.is_repeat(result, result[n], tree, tree[n], n);
if (repeat_flag == 1)//重复
{
G_tool.clear_trail(exp[n], suffix[n], result[n], tree[n]);//清除记录
continue;
}
}
//如果show_way = 1,把表达式中的^换成**
if (show_way == 1)
G_tool.change_show_way(exp[n]);
//表达式和答案存入容器中
formula F;
F.init(exp[n], result[n].str_word());
F_vector.push_back(F);
n++;
}
return F_vector;
}
前端工具类的部分方法:
//获取一个式子及答案,返回值为formula对象,当容器中没有未使用过的题目时,会自动生成新的题目
//默认显示乘方为“^”,如果需要切换为“**”,传入整型参数1
formula generator::get_formula(int show_way)
{
if (index > sum)//题目用尽,重新生成
{
generate_tool G_tool;
vector<formula> tem_vector = G_tool.generate_exp(1000, 10, 10);//生成1000道题目
formula_vector.insert(formula_vector.end(), tem_vector.begin(), tem_vector.end());//生成的题目加入容器中
sum += 1000;
}
//给题目和答案进行编号
formula_vector[index-1].id = index;
//如果显示模式为1
if (show_way == 1)
{
generate_tool G_tool;
G_tool.change_show_way(formula_vector[index-1].problem);//改变题目的显示模式
}
formula return_value;
return_value = formula_vector[index-1];//返回值
index++;
//输出到文件
output_into_file(return_value.problem);
return return_value;
}
//检查答案是否正确。id为题号,u_answer为用户的答案,正确返回1,错误返回0
int generator::check_answer(int id, string u_answer)
{
if (u_answer == formula_vector[id - 1].answer)
return 1;
//将分母的负号挪到分子上,再进行比较
for (int i = 0; i < u_answer.size(); i++)
{
if (u_answer[i] == '/')
{
if (u_answer[i + 1] == '-')
{
u_answer.erase(i + 1, 1);
u_answer.insert(0, "-");
if (u_answer == formula_vector[id - 1].answer)
return 1;
else
return 0;
}
}
}
return 0;
}
测试
使用的测试代码如下
#include "libraryfile_and_define.h"
#include "class.h"
int main()
{
generator G;//工具类
//生成5道题,输出,乘方显示为^
for (int i = 1; i <= 5; i++)
{
formula F = G.get_formula();
cout << "题号:" << F.id << endl;
cout << "题目:" << F.problem << endl;
cout << "答案:" << F.answer << endl << endl;
}
//生成5道题,输出,乘方显示为**
for (int i = 1; i <= 5; i++)
{
formula F = G.get_formula(1);
cout << "题号:" << F.id << endl;
cout << "题目:" << F.problem << endl;
cout << "答案:" << F.answer << endl << endl;
}
system("pause");
}
结果如下:
可以看出,实现了需求。