前置技能
栈 (stack)
栈是一种限制访问端口的线性表,栈的所有操作都先定在线性表的一端进行。表首被称为“ 栈底 ”,表尾被称为“ 栈顶 ”(这里书上第36页大概说反了)。每次取出的总是最后压进的元素,即“ 后进先出 ”。与之相对的是队列 (queue),在表的一端插入,另一端取出,即“ 先进先出 ”。-
中缀表达式 (InfixExp) 与后缀表达式 (PostfixExp)
中缀表达式即我们常用的23 + (34*45) / (5+6+7)
这样的表达式(没错就是65页的例子),对于人类的我们来说,经过了小学的各种计算训练,基本到了五年级统考的时候我们都能扫一眼以后就轻松计算出上面的例子,但是对于计算机来说,括号的处理是一个大难题。反正我没有想到什么比书上讲到的后缀表达式更好的解决方法。
此时我们就需要把中缀表达式转变为后缀表达式,后缀表达式又称逆波兰表达式,不含括号,运算符放在两个参与运算的语法成分后面,求值严格从左向右(对计算机很友好)。顺便吐个槽:此处的infix、postfix都是算术里中缀和后缀的意思。书上概要设计里面的suffix是英文单词的后缀的意思……
中缀:
23 + (34*45) / (5+6+7)
后缀:23 34 45 * 5 6 + 7 + / +
需求描述
- 可以使别加减乘除以及括号的中缀表达式并计算
- 如果表达式有误,应给出相应的提示
概要设计
书上的类设计图可以债见了。
如果有人按着后面的表达式流程图码代码,我可以保证你能码到怀疑人生。(然而前提是能码出来(?))
下面是头文件calculator.h
#ifndef CALCULATOR_H
#define CALCULATOR_H
#include
#include
using namespace std;
class C {
public:
//C(); 默认构造
//~C(); 默认析构
double calculate(string InfixExp) ; //计算函数(炒鸡安全2333
private:
string infix_to_postfix(string InfixExp); //中缀转后缀
double cal_postfix(string PostfixExp); //计算后缀
int priority(char op); //返回优先级
double stringToDouble(string num); //字符串转浮点数
double cal(double num1, double num2, char op); //计算两个数
};
#endif // CALCULATOR_H
在这里我们可以清晰地看明白这个计算器类的运行方法:在我们读入了一段中缀表达式以后,直接调用calculate(string InfixExp)
函数,在calculate函数内先调用infix_to_postfix(string InfixExp)
函数,将中缀表达式转为后缀表达式,然后调用cal_postfix(string PostfixExp)
函数,计算后缀表达式。
函数详细设计
这里直接简单摘取书上66、67页的内容对infix_to_postfix(string InfixExp)
函数和cal_postfix(string PostfixExp)
函数进行说明(然而书上对压栈等操作的说明写的十分笼统,且对栈的类型定义等一律没有……)
中缀转后缀
infix_to_postfix(string InfixExp)
0 ) 定义后缀表达式string PostfixExp = " ";
,定义栈存储操作符stack
calculate;
1 ) 当读入的是数字时,直接输出到后缀表达式PostfixExp += InfixExp[i];
当读入的不是数字时,需要插入空格将数字隔开PostfixExp += " ";
2 ) 当读入的是左括号时,直接将其压栈calculate.push(InfixExp[i]);
3 ) 当读入的是右括号时,在栈非空的情况下弹出左括号前所有操作符PostfixExp += calculate.top(); calculate.pop();
。此时如果括号不匹配(缺少左括号),则栈会被清空,检测栈是否为空if(calculate.empty())
,为空则报错 “ 括号不匹配 ” ,返回 “ error ” 。括号匹配则在之后弹出左括号。
4 ) 当读入的是运算符+ - * /
时,当栈非空且栈顶非左括号且当前运算符优先级不高于栈顶运算符优先级时,反复将栈顶元素弹出至后缀表达式。之后将输入的运算符压栈 <书上原话 >while (!calculate.empty() && calculate.top() != '(' && priority(InfixExp[i]) <= priority(calculate.top()))
通俗地讲就是把* /
先存进后缀表达式,再存入+ -
。
5 ) 读到了非法字符,报错返回 “ error ” 。
6 )while (!calculate.empty())
清栈,如果碰到左括号,说明括号不匹配(缺少右括号),报错返回 “ error ” 。计算后缀
cal_postfix(string PostfixExp)
0 ) 处理中缀转后缀的报错if (PostfixExp == "error") return 0;
。定义字符串用来转成浮点型string str;
定义栈存储数字stack
calculate;
~ ) 我在这个函数中读取数字并转成浮点型存储,助教小哥哥的意思好像是读取的时候就分开存好(?)不过没差啦……我这个方法除了读取的部分十分诡异以外,更大限度地保留了书上的原汁原味(大雾)至于哪里诡异呢……大概就是为了防止把123读出1,12,123三个数字,我在循环嵌套的内循环里顺便把外循环的计数君也自增了,那么外循环会多1,所以后面又要减掉1……嗯,常规操作常规操作……
1 ) 读取数字并压栈calculate.push(stringToDouble(str));
2 ) 遇到运算符则弹出两个数字并计算,将结果压栈calculate.push(cal(num1, num2, PostfixExp[i]));
计算两个数
cal(double num1, double num2, char op)
这个函数没什么好说的,num1 和 num2 的顺序一开始有点懵但实际上跑一下看看就好了。唯一要注意的是除法要判断被除数是否为 0 ,为 0 则报错(返回什么随意) 。优先级
priority(char op)
这个优先级的大小就自己看着办吧,反正可以测试一下3 + 6 / 2
这样的东西。正确与否取决于中缀转后缀的第四步的 while 语句写法和这里的大小赋值。原理上只要赋值+ - * /
但我连括号都赋上了 2333 完全不理解当时在想什么啊!字符串转浮点型
stringToDouble(string num)
这个代码我在网上copy的,很显然我也忘了是copy哪里的所以就不放出处了。这个代码还能转小数点……我就顺手加了两个判断,把几个int型改成double型,就可以计算浮点数了哦好神奇。
具体实现
我觉得直接copy不能提高个人coding能力,所以我觉得如果你真的想学好coding,还是应该对照我上面十分详尽的讲解看完下面的代码,然后自己写一个,或者至少尝试自己写,不会了再跟大神们讨论讨论,刷一下自己的魅力值,对吧。
下面是calculator.cpp
#include"calculator.h"
#include
#include
#include
//计算函数
double C::calculate(string InfixExp) {
string PostfixExp = infix_to_postfix(InfixExp); //调用中缀转后缀
return cal_postfix(PostfixExp); //返回后缀计算值
}
//中缀转后缀
string C::infix_to_postfix(string InfixExp) {
string PostfixExp = "";
stack calculate;
for (int i = 0; i < InfixExp.length(); i++) {
if ((InfixExp[i] >= '0' && InfixExp[i] <= '9')|| InfixExp[i] == '.') {
PostfixExp += InfixExp[i]; //数字直接压栈
}
else {
PostfixExp += " "; //操作符
if (InfixExp[i] == '(') { //左括号压栈
calculate.push(InfixExp[i]);
}
else if (InfixExp[i] == ')') { //右括号
while (!calculate.empty() && calculate.top() != '(') {
PostfixExp += calculate.top(); //将所有操作符弹出
calculate.pop();
}
if (calculate.empty()) {
std::cout << "括号不匹配" << std::endl;
return "error"; //确认括号匹配
}
calculate.pop(); //删除栈内左括号
}
else if (InfixExp[i] == '+' || InfixExp[i] == '-' ||
InfixExp[i] == '*' || InfixExp[i] == '/') {
while (!calculate.empty() && calculate.top() != '('
&& priority(InfixExp[i]) <= priority(calculate.top())) {
PostfixExp += calculate.top(); //比较优先级
calculate.pop();
}
calculate.push(InfixExp[i]);
}
else {
std::cout << "非法字符" << std::endl;
return "error";
}
}
}
while (!calculate.empty()){ //清栈
if(calculate.top() == '('){
std::cout << "括号不匹配" << std::endl;
return "error";
}
PostfixExp += calculate.top();
calculate.pop();
}
return PostfixExp;
}
//计算后缀
double C::cal_postfix(string PostfixExp) {
if (PostfixExp == "error")
return 0;
double num1,num2;
string str;
stack calculate;
for (int i = 0; i < PostfixExp.length(); i++) { //遇到数字,读取并入栈
if (PostfixExp[i] == ' ') continue; //无视空格
else if ((PostfixExp[i] >= '0'&&PostfixExp[i] <= '9')
||PostfixExp[i]=='.') {
str = "";
for (int j = i;(PostfixExp[j] >= '0'&&PostfixExp[j] <= '9')
|| PostfixExp[i] == '.'; i++, j++) {
str += PostfixExp[j];
}
i--;
calculate.push(stringToDouble(str));
}
else { //遇到操作符,提取并计算
num1 = calculate.top();
calculate.pop();
num2 = calculate.top();
calculate.pop();
calculate.push(cal(num1, num2, PostfixExp[i]));
}
}
return calculate.top();
}
//计算两个数
double C::cal(double num1, double num2, char op) {
if (op == '+')
return (num2 + num1);
else if (op == '-')
return (num2 - num1);
else if (op == '*')
return (num2 * num1);
else if (op == '/') {
if(num1 == 0){
std::cout<<"除数不能为0"<
下面是main.cpp
#include"calculator.h"
#include
#include
int main() {
string temp;
cout << "请输入表达式:" << endl;
cin >> temp;
C cal;
cout << "计算结果为:" << cal.calculate(temp) << endl;
return 0;
}
封装好的类通常可以让调用很简单……但码这个代码让我明白,像我这样的凡人想封装好这么一个类还是很难的。
谨以此文祭奠我逝去的头发。