数据结构课程设计——计算器
目录(可跳转)
计算器,广泛应用于商业交易中,是必备的办公用品之一,也被称作“第一代电子计算机”。与计算机相同,它的内部也由逻辑电路组成,其功能实现也离不开对各条语句的分析,从而解析复杂的表达式,按照计算规则得到正确的结果。
我们平时最常接触的计算表达式就是“中缀表达式”,然而机器要解析中缀表达式并不容易,尤其是在有括号和各种不同优先级的运算符同时存在的时候,通过对中缀表达式进行转换,使其变成”后缀表达式“,则可以用栈来实现计算的过程。
本项目就是一个用栈来实现表达式计算的简易计算器。
基础功能
项目附加功能(测试)
本项目拟用栈来实现计算过程,由于没有限制表达式的长度大小,故采用”链式栈“的储存方式,其中,”链式栈类(LinkedStack.h和LinkedStack.cpp)“均直接引用项目”勇闯迷宫“里的类的声明与定义。
项目除了”链式栈类“之外,还有”计算器类(即Calculator)“,用来实现对计算器各种功能的封装,提高可复用性和可读性。
Calculator类的声明
class Calculator {
private:
LinkedStack<char> s_input_; //从键盘读入的,同时含有操作数和操作符的栈
LinkedStack<char> s_char_; //处理过后,只含有操作符的栈
LinkedStack<double> s_double_; //处理过后,只含有操作数的栈
public:
Calculator();
~Calculator();
double getRes(); //中缀转后缀并返回答案
bool Input(); //从键盘读入一个中缀表达式,存入栈中
bool isLegal(); //判断中缀表达式是否合法的函数
bool Calculate(char); //计算后缀表达式结果
int charToDouble(char); //字符转整型函数,调用后接下来出现的数字都参与转换
int getIsp(char); //返回一个操作符的栈内优先级,返回-1表示是非法字符
int getIcp(char); //返回一个操作符的栈外
优先级,返回-1表示是非法字符
};
解释:程序不认为回车所控制的”转行“为输入终止条件,程序仅认=符号为输入完毕标志
2.代码展示(ps:略去了部分声明和系统提示)
bool Calculator::Input() {
cout << "请输入您想要计算结果的算式(以“=”结尾)" << endl;
char temp_char;
s_double_.makeEmpty();
s_char_.makeEmpty();
s_input_.makeEmpty();
do {
cin >> temp_char;
s_input_.Push(temp_char);
} while (temp_char != '=');
return true;
}
流程图
1.运行截图
解释:乘法运算是用*来进行的,而不是X,报非法字符的错误,程序不崩溃
2.代码展示(ps:略去了部分声明和系统提示)
while (!s_input_.isEmpty()) {
s_input_.Pop(cur_char); //从输入字符串当中pop一个字符
if (cur_char >= '0' && cur_char <= '9') {
temp.Push(cur_char); //是数字,进入辅助栈
}
else if (cur_char == '+' || cur_char == '-' ||
cur_char == '*' || cur_char == '/' ||
cur_char == '%' || cur_char == '^') {
temp.Push(cur_char); //是合法字符,进入辅助栈
}
else if (cur_char == ')') {
temp.Push(cur_char); //是合法字符,进入辅助栈
parentheses.Push(')'); //由于是逆序,所以是'('栈,')'出栈
}
else if (cur_char == '(') {
temp.Push(cur_char); //是合法字符,进入辅助栈
parentheses.Pop(cur_char); //与')'匹配,出栈
}
else if (cur_char == '=') { //不用管,删除即可
continue;
}
else if (cur_char == ' ') {
continue; //空格不算非法字符,删除即可
}
else { //如果都不是,说明为非法字符,输出提示
cerr << "输入的表达式【含非法字符"
<<cur_char
<<"】请重新输入:" << endl;
return false;
}
}
while (!temp.isEmpty()) { //把辅助栈里的字符弹出,进入s_input_
temp.Pop(cur_char);
s_input_.Push(cur_char);
}
if (!parentheses.isEmpty()) {
cerr << "输入的表达式【括号不匹配】请重新输入:" << endl;
return false;
}
3.3.1 栈的运行图
ps:此处栈的情况更为清晰易懂,流程图反而复杂
s_input_( 左为栈顶 ): 30+2*(105-95) - 560/56 #
当前项 | 进行比较 | 文字说明 | 操作数栈情况 | 操作符栈情况 | 进行的运算 |
---|---|---|---|---|---|
30 | \ | 数字进栈 | 30 | # | \ |
+ | +.icp > #.isp | 符号进栈 | 30 | #, + | \ |
2 | \ | 数字进栈 | 30,2 | #, + | \ |
* | *****.icp > +.isp | 符号进栈 | 30,2 | #, +, * | \ |
( | ( .icp > *****.icp | 符号进栈 | 30,2 | #, +, * | \ |
105 | \ | 数字进栈 | 30,2,105 | #, +, *, ( | \ |
- | - .icp > (.icp | 符号进栈 | 30,2,105 | #, +, *, ( , - | \ |
95 | \ | 数字进栈 | 30,2,105,95 | #, +, *, ( , - | \ |
) | ) .icp < -.icp | 出栈,运算 | 30,2,10 | #, +, *, ( | 105-95=10 |
) .icp = (.icp | 出栈 | 30,2,10 | #, +, * | ||
- | - .icp < *.icp | 出栈,运算 | 30,20 | #, + | 2*10=20 |
- .icp < +.icp | 出栈,运算 | 50 | # | 30+20=50 | |
-.icp > #.isp | 符号进栈 | 50 | #, - | \ | |
560 | \ | 数字进栈 | 50,560 | #, - | \ |
/ | / .icp > #.icp | 符号进栈 | 50,560 | #, - , / | \ |
56 | \ | 数字进栈 | 50,560,50 | #, -, / | \ |
# | # .icp < /.icp | 出栈,运算 | 50,10 | #, - | 560/56=10 |
# .icp < -.icp | 出栈,运算 | 40 | #, - | 50-10=40 | |
#.icp = #.isp | 循环终止 | 40 | \ | \ |
运行截图
2.代码展示(ps:略去了部分声明和系统提示)
getRes()函数
double Calculator::getRes() {
while (1) {
if (cur_char >= '0' && cur_char <= '9') { //如果是数字,则处理这串数字
int cur_num = charToDouble(cur_char); //将相邻几个数字转为double
s_double_.Push(cur_num); //得到的数字存入s_double_中
s_input_.Pop(cur_char); //继续处理下一个字符
}
else {
s_char_.getTop(top_char);
if (getIsp(top_char) < getIcp(cur_char)) {
//当前操作符的优先级高
Pop_flag = true;
s_char_.Push(cur_char); //储存在字符栈中
s_input_.Pop(cur_char); //继续处理下一个字符
}
else if (getIsp(top_char) > getIcp(cur_char)) {
s_char_.Pop(op); //从字符栈之中退出,进行运算
Calculate(op);
Pop_flag = false;
}
else { //优先级一样
Pop_flag = true;
s_char_.Pop(op);
if (op == '(') {
s_input_.Pop(cur_char); //继续处理下一个字符
}
if (op == '#') {
break; //全部计算完毕,退出循环
}
}
}
}
s_double_.Pop(res);
return res; //返回最终答案
}
Calculate()函数
bool Calculator::Calculate(char op) {
//计算后缀表达式结果
double left, right; //左右两个操作数
double res = 0; //和计算后结果
s_double_.Pop(right); //右操作数的出栈
s_double_.Pop(left); //左操作数的出栈
switch (op) {
case'+':
res = left + right;
break;
case'-':
res = left - right;
break;
case'*':
res = left * right;
break;
case'/':
res = left / right;
break;
case'%':
res = fmod(left, right);
break;
case'^':
res = pow(left, right);
break;
}
s_double_.Push(res);
return true;
}
以下简述一下本项目的一些亮点(仅代表个人观点):
计算器实现了单目运算符 ‘+’ 和 ’ - ’
合法性检测考虑了”非法字符“,”括号不匹配“,”输入算式中含有括号“三种情况,并对应有提示信息
项目文档中附加有 ”流程图“ 和 ”工作栈示意图“ ,使读者易懂,思路清晰
项目的代码风格良好,变量和函数命名统一,有规整的注释帮助理解代码
本项目已经不是第一次使用 ”链式栈“ (勇闯迷宫也用了链式栈),因此在类结构方面没有遇到什么难题,也比较熟练。
项目在对计算的整个思维结构要求较高,我认为难点有三:
一是各个函数之间的调用关系,由于函数要求简洁性和专一性,因此其体量较小,因此在”大函数分解为小函数“的过程中会遇到些难题;
二是单目运算符的处理,本项目采用将+A看作是0+A的运算,将-A看作是0-A的运算,避免了对特殊情况的考虑,极大地减少函数分支数,提高效率,识别单目运算符则采用”判断操作数与操作符数目“的方法进行,一旦操作符个数大于操作数个数,则说明碰到了单目运算符,对其做出特殊标记;
三是”中缀转后缀“的分支处理,由于我们日常生活中采用的运算方式是中缀表达式,所以对于后缀的形式表现得极其生疏,不论是转化过程还是计算后缀表达式都是一个非常”别扭“的事情,但是随着项目的结束,对于后缀表达式的熟练度已经远超过刚开始做项目的时候,这也算是一个大收获。
计算器是一个非常具有实际意义的项目,一方面,这是我们生活中经常接触到的东西,另一方面,它的功能的综合性就要求我们在写代码的时候既要有全局观,又要考虑很多细节,因此十分锻炼代码能力,同时,经过这次项目,再一次巩固了栈的知识,加深了对栈的特点的认识。
二是单目运算符的处理,本项目采用将+A看作是0+A的运算,将-A看作是0-A的运算,避免了对特殊情况的考虑,极大地减少函数分支数,提高效率,识别单目运算符则采用”判断操作数与操作符数目“的方法进行,一旦操作符个数大于操作数个数,则说明碰到了单目运算符,对其做出特殊标记;
三是”中缀转后缀“的分支处理,由于我们日常生活中采用的运算方式是中缀表达式,所以对于后缀的形式表现得极其生疏,不论是转化过程还是计算后缀表达式都是一个非常”别扭“的事情,但是随着项目的结束,对于后缀表达式的熟练度已经远超过刚开始做项目的时候,这也算是一个大收获。
计算器是一个非常具有实际意义的项目,一方面,这是我们生活中经常接触到的东西,另一方面,它的功能的综合性就要求我们在写代码的时候既要有全局观,又要考虑很多细节,因此十分锻炼代码能力,同时,经过这次项目,再一次巩固了栈的知识,加深了对栈的特点的认识。