CHAPTER_7 提高篇(1)——数据结构(1)
7.2栈和队列的应用情形—表达式计算
一般来说,我们将表达式分为三种:前缀表达式、中缀表达式、后缀表达式。我们日常中使用最多的是中缀表达式。这里不在赘述这三种表达式的定义,有需要的读者可以参考博客前缀、中缀、后缀表达式(逆波兰表达式) - chensongxian - 博客园 。
栈的一个非常典型的应用,就是制作简单计算器,在计算器读入我们输入的中缀表达式时,并不会直接进行计算,通常会将它转变为后缀表达式,然后在计算后缀表达式的值。
接下来我们通过一个简单的例题,来练习栈和队列的应用。
题目:
读入一个包含 + - * / 四种运算符和括号的非负整数计算表达式,计算该表达式的值。
输入格式:
测试用例包含若干测试用例,每个测试用例占一行,每行不超过200个字符,整数和运算符之间用空格分隔,括号和数字之间不用空格分隔。括号皆是英文输入法下的括号,没有非法表达式。当一行中只有0时输入结束,相应的结果不要输出。
输出格式:
对每个测试用例输出一行,即该表达式的值,精确到小数点后2位。
输入样例:
30 / 90 - 26 + 97 - 5 - 6 - (13 / 88 * 6 + 51 / 29) + 79 * 87 + 57 - 92
985211 * 985 / 211
2 * (3 + 5 / 2)
0
输出样例:
12178.21
4599207.75
11.00
思路:
我们用string来接收输入的表达式。首先我们要对输入字符串进行去空格操作,以便于后续的处理。而处理的方法也很简单,我们遍历串,对出现空格的位置使用erase()方法即可。
接下来的算法包含两个步骤:
(1)中缀表达式转后缀表达式;
(2)计算后缀表达式。
步骤一:中缀转后缀-void change()函数
设立一个栈用于临时存放操作符;设立一个队列,用以存放后缀表达式。接着我们要按照如下定义运算符的优先级:乘号=除号>加号=减号,具体的实现可以用哈希表。然后我们从左至右开始遍历输入字符串,对于每一次遍历,按照如下情况处理:
(1)如果当前字符为数字,我们要考虑到数字可能不止一位,因此需要继续向后面遍历,将碰到的数字一位位合并,直到碰到操作符停止,将最后所得的操作数入队。例如123+4中,我们要将遍历得到的'1' '2' '3'合并成123。最后遍历继续至字符串下一位。
(2)如果当前字符为操作符op,比较其栈顶运算符的优先级。
a. op为左括号,则将op入栈,遍历继续至字符串下一位;
b. 若栈为空或者op的优先级大于栈顶元素的优先级,则将op入栈,遍历继续至字符串下一 位;
c. 若op的优先级小于等于栈顶元素,则将栈顶操作符出栈,并将其入队存放于后缀表达式; 之后我们的遍历停止一位,也就是继续保持当前操作符号op,待下一轮继续比较op与栈顶 元素的优先级
d. 若op为右括号,依次将栈顶元素出栈,并且将它们入队,直达栈顶元素为左括号停止。最 后将栈顶左括号出栈丢弃,遍历继续至字符串下一位。
待字符串遍历完成后,将栈内所有剩余操作符依次出栈,然后入队。
最后按顺序遍历队列,即可得到转换后的后缀表达式。
步骤二:后缀表达式的计算-double cal()函数
从左至右遍历后缀表达式(实际上就是队列出队的过程),对每一次遍历,按照如下情况处理:
a. 若当前为数字,将其压入栈。
b. 若当前为运算符op,弹出栈顶的两个数a和b,计算b op a,并将结果入栈;
待后缀表达式遍历完成后,栈内剩余唯一元素(即为栈顶元素),该元素为最终结果。
PS:上面描述了算法过程,而并未阐述其原理。读者不妨跟着参考博客中的举例来手动模拟这个算法,详细体会其过程。进而在这个过程中,慢慢理解其原理。前缀、中缀、后缀表达式(逆波兰表达式) - chensongxian - 博客园 (cnblogs.com) 。
代码如下:
#include <iostream>
#include <string>
#include <map>
#include <stack>
#include <queue>
using namespace std;
typedef struct Node {
double num; //操作数
char op; //操作符
bool flag; //true表示操作数,false表示操作符
}node; //node是一个既能存放数字又能存放运算符的结构体,用flag来控制其存储哪种类型
stack<node> s;
queue<node> q;
map<char,int> op;
string str;
void change() {
node tmp;
for(int i=0;i<str.size();) {
if(str[i]>='0'&&str[i]<='9') { //当前为数字
tmp.flag=1; //标记为数字
tmp.num=0;
while(str[i]>='0'&&str[i]<='9'&&i<str.size()) { //将数字逐一合并,直至碰到操作符或者遍历完串
tmp.num=tmp.num*10+(str[i]-'0');
i++;
}
q.push(tmp); //该数字入队
}
else if(str[i]=='(') { //左括号则入栈
tmp.flag=0;
tmp.op='(';
s.push(tmp);
i++;
}
else if(str[i]==')') { //当前为右括号,持续将栈顶出栈入队,直到碰到左括号
while(s.top().op!='(') {
q.push(s.top()); //栈顶元素入队
s.pop(); //出栈
}
s.pop(); //丢弃栈顶左括号
i++;
}
else if(s.empty()||op[str[i]]>op[s.top().op]) { //栈顶优先级低或者栈空
tmp.flag=0;
tmp.op=str[i]; //运算符入栈
s.push(tmp);
i++;
}
else { //栈顶优先级大于等于操作符
q.push(s.top());
s.pop(); //栈顶入队,然后出栈,
}
}
while(!s.empty()) { //遍历完成后,将栈内剩余操作符入队
q.push(s.top());
s.pop();
}
}
double cal() {
double a,b;
char tmp_op;
node tmp;
while(!q.empty()) {
if(q.front().flag==true) { //当前为数字
s.push(q.front());
q.pop(); //将队首数字压入栈,然后出队
}
else { //当前为操作符
tmp_op=q.front().op;
q.pop(); //队首操作符赋予临时变量
a=s.top().num;
s.pop();
b=s.top().num;
s.pop(); //弹出栈顶两个数字,并赋予a和b
switch(tmp_op) { //根据运算符做不同运算
case '+':
tmp.num=b+a;
break;
case '-':
tmp.num=b-a;
break;
case '*':
tmp.num=b*a;
break;
case '/':
tmp.num=b/a;
break;
}
tmp.flag=1;
s.push(tmp); //将结果入栈
}
}
return s.top().num;
}
int main() {
op['*']=2;
op['/']=2;
op['+']=1;
op['-']=1;
op['(']=0; //定义运算符优先级
while(getline(cin,str),str!="0") {
for(string::iterator it=str.begin();it!=str.end();it++) {
if(*it==' ')
str.erase(it);
} //去空格
while(!s.empty()) {
s.pop();
} //将栈初始化为空
change();
printf("%.2f\n",cal());
return 0;
}
}