摘要
对于数据结构初学者来说,带有优先级的计算器的实现是一个很头疼的问题,即使目前教材上上已经讲解得很详细,但依然难以形象的理解并通过编程实现计算器的功能。作者通过实践,运用栈,对带有’*’、’/‘运的表达式和带有’)’、’)'的表达式进行化归,实现了此功能。
问题重述
Description
通过程序来完成任意算术表达式的求值。表达式中包含的运算符包括+,-,*,/,()等5类。运算对象均为整数。
Input
包含多组测试数据。每组测试数据一个算术表达式,占一行。每个表达式不超过100个字符。
Output
输出每个算术表达式的计算结果(按照C语言运算规则),一个结果占一行。
Case 1:
输入:1+2-3+4
输出:4
Case 2:
输入:1+2*1-3+2*2
输出:4
Case 3:
输入:1+2*1-(1+2)*1+((1+2)*2-2)
输出:4
问题分析
对于Case 1这种仅含有’+’、’-'的表达式,我们只需要从左到右运算就行,可以让用户输入表达式之后遍历字符串,或者运用如下代码边输入边计算,进而求解。
//仅含'+'和'-'的表达式的求解
int Cal_EXP()
{
int n;
char ch;
std::cin >> n;
int ans = n;
while ((ch=getchar())!='\n')
{
if (ch == '+')
{
cin >> n;
ans = ans + n;
}
if (ch == '-')
{
cin >> n;
ans = ans - n;
}
}
return ans;
}
可如果是Case 2呢?
1+2*1-3+2*2
按照这种写法,就显然不正确了。
但我们可以试图讲这个表达式化成Case 1的表达式
将优先级高的运算符看成一个整体
1 | 2*1 | 3 | 2*2 | |||
---|---|---|---|---|---|---|
+ | - | + |
化归后
1 | + | 2 | - | 3 | + | 4 |
---|
=4
对于有括号的表达式同理
1+2*1-(1+2)*1+((1+2)*2-2)
1 | 2*1 | (1+2)*1 | ((1+2)*2-2) | |||
---|---|---|---|---|---|---|
+ | - | + |
而最后一个表达式又可以同理化简。
我们可以理解为,计算一个算术表达式时,从左到右运算,如果遇到’+’、’-’、正常运算就行,但如果遇到优先级高的’*’、’\’,将该运算符两边的数运算后,当成一个数后和先前的结果进行’+’、’-‘操作,遇到’(‘时,将即将读进来的表达式“包裹”起来,直到’)’,算出这部分结果后,再将这部分结果与前面的结果做’+’、’-'运算,直到读取字符串结束。
因此我们可以设计两个栈实现这个功能,所有的’*’,’/'操作都在栈顶实现,随后将结果推入栈中。
1+2*1-(1+2)*1
第1步
num | 1 |
---|---|
ope |
第2步
1+
num | 1 | |
---|---|---|
ope | + |
第3步
1+2
num | 1 | 2 | |
---|---|---|---|
ope | + |
第4步
1+2*
num | 1 | 2 | ||
---|---|---|---|---|
ope | + | * |
第5步
1+2*3
num | 1 | 2 | 3 | ||
---|---|---|---|---|---|
ope | + | * |
第6步
1+6
num | 1 | 6 | |
---|---|---|---|
ope | + |
第7步
1+6-
num | 1 | 6 | ||
---|---|---|---|---|
ope | + | - |
第8步
1+6-(
num | 1 | 6 | |||
---|---|---|---|---|---|
ope | + | - | ( |
第9步
1+6-(1
num | 1 | 6 | 1 | |||
---|---|---|---|---|---|---|
ope | + | - | ( |
第10步
1+6-(1+
num | 1 | 6 | 1 | ||||
---|---|---|---|---|---|---|---|
ope | + | - | ( | + |
第11步
1+6-(1+2
num | 1 | 6 | 1 | 2 | ||||
---|---|---|---|---|---|---|---|---|
ope | + | - | ( | + |
第12步
1+6-(3
num | 1 | 6 | 3 | |||
---|---|---|---|---|---|---|
ope | + | - | ( |
第13步
1+6-(3)
num | 1 | 6 | 3 | ||||
---|---|---|---|---|---|---|---|
ope | + | - | ( | ) |
第14步
1+6-3
num | 1 | 6 | 3 | ||
---|---|---|---|---|---|
ope | + | - |
第15步
1+6-3*
num | 1 | 6 | 3 | |||
---|---|---|---|---|---|---|
ope | + | - | * |
第16步
1+6-3*1
num | 1 | 6 | 3 | 1 | |||
---|---|---|---|---|---|---|---|
ope | + | - | * |
第17步
1+6-3
num | 1 | 6 | 3 | ||
---|---|---|---|---|---|
ope | + | - |
第18步
1+3
num | 1 | 3 | |
---|---|---|---|
ope | + |
第19步
4
num | 4 |
---|---|
ope |
每一个运算符入栈后,我们都需要判断他很前面运算符的优先级关系,如果他优先级高,需要先运算。
因此我们需要知道每个运算符之间的优先级关系。
/*
前面优先级大于后面为1 前面优先级小于后面为-1 相等为0
不可能出现的情况为9
'='代表'\n'
+ - * / ( ) =
+ 1 1 -1 -1 -1 1 1
- 1 1 -1 -1 -1 1 1
* 1 1 1 1 -1 1 1
/ 1 1 1 1 -1 1 1
( -1 -1 -1 -1 -1 0 9
) 1 1 1 1 9 1 1
= -1 -1 -1 -1 -1 9 0
*/
//定义一个二维数组存放优先级表
const int m[10][10] =
{
{1, 1, -1,-1, -1, 1, 1},
{1, 1, -1,-1, -1, 1, 1},
{1, 1, 1, 1,-1, 1, 1},
{1, 1, 1, 1,-1 ,1, 1},
{-1,-1, -1, -1, -1, 0, 9},
{1, 1, 1, 1, 9, 1, 1},
{ -1, -1, -1, -1 ,-1, 9, 0}
};
根据题意,我们需要实现
int main()
{
string t;
while (cin >> t)
{
cout << CalExp(t) << endl;
}
return 0;
}
在输入一个字符串后,需要遍历字符串,分离出数字和运算符。
int CalExp(string &t)
{
t = t + "\n";//cin输入的string末尾没有换行
//需要换行表示结束,方便后续判断
stack<int> num;
stack<char> ope;
ope.push('\n');//第一次比较运算符优先级时,
//避免出现栈空时top导致RE,在ope栈内存放'\n'
int st = -1; //由于是遍历字符串,
//需要一个指针表示现在读取到哪了
int n; //整数
char ch; //表示从字符串中读取的一个字符,
//ch可能是一个运算符+、-、*、/、(、)、'\n'
//也可以是一个整数的第一位 如100的'1'
//因此需要后面的Input函数去判断ch是一个运算符
//还是一个整数的开始?
ch = t[++st]; //先读取字符串的第一个字符
Input(num, ope, t, ch, st);
while (!(ch == '\n'&&ope.top() == '\n'))
{ //输入结束的条件是回车
int temp = cmp(ope.top(), ch);
//比较刚刚输入的运算符和栈顶运算符的优先级
int a, b; //运算符两侧的整数
switch (temp)
{
case 1://如果栈顶运算符优先级大如 2 3
// + -
a = num.top();
num.pop();
b = num.top();
num.pop(); //取两个整数出栈
num.push(Cal(a, ope.top(), b));
//运算 a ope b 并将结果当做一个整数压入栈
ope.pop(); //运算完之后运算符丢弃
break;
/*
为什么即使每如一个运算符就计算还是会出现读取的运算符比栈顶小的可能?
如 1+2*3+1
读取第一个+时,由于栈中的'\n'优先级比'+'低,switch运行到default,不会进行运算,之后运算的2*3得到的6压入栈中,造成'+'的积压,等到读取第二个'+'的时候,就出现了栈顶的第一个'+'和第二个'+'的比较了。这个时候第二个'+'是ch,等待下一轮循环判断。
*/
case 0:
ope.pop();
ch = t[++st];
break;
/*
只有当栈顶'('且读取')'才会出现这种情况,比如前文的第13步
去括号就行
*/
default:
ope.push(ch);//优先级大的入栈
//case 1中的运算就是拿栈顶运算符
ch = t[++st];
Input(num, ope, t, ch, st);
//接受到一个运算符之后我们需要继续读取
//可以读取到一个整数,也可以读取到一个'('
//因为C语言中((1))这种写法是正确的
//但括号是成对入栈出栈,不受影响
}
}
return num.top();
//循环结束返回的栈顶元素就是运算结果
}
输入函数Input的实现
void Input(stack<int> &num, stack<char> &ope, string &t, char &ch, int &st)
{
//该函数的核心是判断ch是一个运算符还是一个整数的第一位
if (ch == '-' || ch == '+')
{
/*
但这个整数也可以为负数
如果是-number,ch读取到的是'-'
由于'*'、'/'优先级最高,他们必定会在表达式后面的'-'之前运算完,因此此时'-'前面只可能有'+'与'-',而在一个仅含有'+'、'-'的表达式中的任意位置插入0结果不变,因此我们只需将它看成0-number。*/
num.push(0);
ope.push(ch);
ch = t[++st];
}
if (!Isope(ch))
{
//如果ch是一个整数的第一位,就可以开始字符串转int的常规操作了
char stringnum[200];
memset(stringnum, 0, sizeof(stringnum));
stringnum[0] = ch;
int i = 1;
while ((ch = t[++st]) && !Isope(ch))
stringnum[i++] = ch;
int sum = 0;
int p = 1;
while (--i >= 0)
{
sum += (stringnum[i] - '0')*p;
p = p * 10;
}
//转化结束后st指针指向下一个运算符
num.push(sum);
}
}
比较优先级大小的cmp函数
int cmp(const char a, const char b)
{
int i = 0, j = 0;
switch (a)
{
case'+': i = 0; break;
case'-': i = 1; break;
case'*': i = 2; break;
case'/': i = 3; break;
case'(': i = 4; break;
case')': i = 5; break;
case'\n': i = 6; break;
}
switch (b)
{
case'+': j = 0; break;
case'-': j = 1; break;
case'*': j = 2; break;
case'/': j = 3; break;
case'(': j = 4; break;
case')': j = 5; break;
case'\n': j = 6; break;
}
return m[i][j];
}
计算运算符字符两边的Cal函数
int Cal(int a, char t, int b)
{
int ans = 1;
//由于栈进出会将元素顺序倒置,因此为b ope a
switch (t)
{
case'+':ans = b + a; break;
case'-':ans = b - a; break;
case'*':ans = b * a; break;
case'/':ans = b / a; break;
}
return ans;
}
------------------------分---------割---------线-------------------
完整AC代码如下:
(编译环境 Microsoft Visual Studio 2017 x86)
/*
calculator
by wust-cws 2018.10.13
*/
#include <cstdio>
#include <stack>
#include <cstring>
#include <string>
#include <iostream>
using namespace std;
/*
+ - * / ( ) = 前面优先级大于后面为1 前面优先级小于后面为-1 相等为0
+ 1 1 -1 -1 -1 1 1
- 1 1 -1 -1 -1 1 1
* 1 1 1 1 -1 1 1
/ 1 1 1 1 -1 1 1
( -1 -1 -1 -1 -1 0 9
) 1 1 1 1 9 1 1
= -1 -1 -1 -1 -1 9 0
*/
const int m[10][10] =
{
{1, 1, -1,-1, -1, 1, 1},
{1, 1, -1,-1, -1, 1, 1},
{1, 1, 1, 1,-1, 1, 1},
{1, 1, 1, 1,-1 ,1, 1},
{-1,-1, -1, -1, -1, 0, 9},
{1, 1, 1, 1, 9, 1, 1},
{ -1, -1, -1, -1 ,-1, 9, 0}
};
int cmp(const char a, const char b);
int Cal(int a, char t, int b);
void Input(stack<int> &num, stack<char> &ope,string &t, char &ch, int &st);
bool Isope(char a);
int CalExp(string &t);
int main()
{
string t;
while (cin >> t)
{
cout << CalExp(t) << endl;
}
return 0;
}
int cmp(const char a, const char b)
{
int i = 0, j = 0;
switch (a)
{
case'+': i = 0; break;
case'-': i = 1; break;
case'*': i = 2; break;
case'/': i = 3; break;
case'(': i = 4; break;
case')': i = 5; break;
case'\n': i = 6; break;
}
switch (b)
{
case'+': j = 0; break;
case'-': j = 1; break;
case'*': j = 2; break;
case'/': j = 3; break;
case'(': j = 4; break;
case')': j = 5; break;
case'\n': j = 6; break;
}
return m[i][j];
}
int Cal(int a, char t, int b)
{
int ans = 1;
switch (t)
{
case'+':ans = b + a; break;
case'-':ans = b - a; break;
case'*':ans = b * a; break;
case'/':ans = b / a; break;
}
return ans;
}
void Input(stack<int> &num, stack<char> &ope, string &t, char &ch, int &st)
{
if (ch == '-' || ch == '+')
{
num.push(0);
ope.push(ch);
ch = t[++st];
}
if (!Isope(ch))
{
char stringnum[200];
memset(stringnum, 0, sizeof(stringnum));
stringnum[0] = ch;
int i = 1;
while ((ch = t[++st]) && !Isope(ch))
stringnum[i++] = ch;
int sum = 0;
int p = 1;
while (--i >= 0)
{
sum += (stringnum[i] - '0')*p;
p = p * 10;
}
num.push(sum);
}
}
bool Isope(char a)
{
return a == '+' || a == '-' || a == '*' || a == '/' || a == '(' || a == ')' || a == '\n';
}
int CalExp(string &t)
{
t = t + "\n";
stack<int> num;
stack<char> ope;
ope.push('\n');
int st = -1;
char ch;
ch = t[++st];
Input(num, ope, t, ch, st);
while (!(ch == '\n'&&ope.top() == '\n'))
{
int temp = cmp(ope.top(), ch);
int a, b;
switch (temp)
{
case 1:
a = num.top();
num.pop();
b = num.top();
num.pop();
num.push(Cal(a, ope.top(), b));
ope.pop();
break;
case 0:
ope.pop();
ch = t[++st];
break;
default:
ope.push(ch);
ch = t[++st];
Input(num, ope, t, ch, st);
}
}
return num.top();
}
理论上还可以用getchar()一个个读取,但很不幸,提交本题的时候收到了RE,确定不是空栈pop分母为0,到现在也不知道原因,代码如下:
https://paste.ubuntu.com/p/rBbQ5ZH6BY/
当同学用DFS秒接这一题后,留下了没有技术的泪水,不过,计算器可以算是栈的经典入门题了。