前言:
有关中缀表达式计算是数据结构中非常经典的题目,以至于很多文章或课本喜欢直接给出计算方法一步到位,但关于其中的原理却并未深究,本文试图通过分析运算符的栈内优先级,栈外优先级的排序方法探求中缀表达式计算中的原理。 为了简便起见,在本文的讨论中只考虑双目运算符(仅+、-、*、/ 四种)以及括号。并默认输入的表达式正确。
引用:
请看完这篇文章以对中缀表达式的计算有一个大体的了解 :
正文:
中缀表达式的两种经典的计算方法,即转为后缀表达式法(间接法)与直接计算法。我们分别讨论这两种方法的优先级排序。
1.中缀转后缀表达式算法(间接法)
1.1需要什么样的数据结构?
- 一个运算符栈用来处理所有运算符,并将他们按正确的排序输出到后缀表达式中。
- 一个输出序列用来输出后缀表达式(可以用stack ,也可以用vector、string等 ,因为需要的操作只是在序列尾添加数据),可以记为操作数栈,因为遍历时遇到操作数直接压栈,但其中也会有运算符。
1.2运算规则是什么?
结合中缀表达式的计算规则及后缀表达式的计算规则:
中缀(人的计算):
(1) 先计算括号内,后计算括号外;
(2) 在无括号或同层括号内,先乘除运算,后加减运算,即乘除运算的优先级高于加减运算的优先级;
(3) 同一优先级运算,从左向右依次进行。
后缀(计算机的计算):
从左到右扫描后缀表达式,如果遇到操作数,将其压入栈中,如果遇到操作符,则从栈中弹出两个操作数,计算结果,然后把结果入栈,直到遍历完后缀表达式,则计算完成,此时的栈顶元素即为计算结果。
1.3优先级排序及操作步骤?
这种情况的优先级排序比较简单也易于理解,只将四则运算的优先级编码,记 +、-为 0, 记 * 、 / 为1。
- 当遇到操作数的时候放入输出序列中。
- 当遇到运算符时情况变得复杂,由于运算符有不同的优先级,我们不能确定取到的运算符是否可以直接计算,需要和上一个操作符进行比较:如果优先级相等,说明是同一类型的运算符,应该从左到右进行运算,即先算栈顶运算符(出栈到输出队列),在把这个运算符压栈;如果该运算符优先级低,说明应该先进行栈内运算符的计算,直到该运算符比栈顶的高再压栈;如果该运算符优先级高,则直接压栈。可以看出,我们的运算符栈实际上是在维护一个严格递增的单调队列。
- 当遇到括号,如果左括号直接进栈,如果遇到右括号,需要依次取出栈中的运算符并输出,直到遇到左括号并弹出左括号(因为后缀表达式不存在括号)
- 遍历完之后将运算符栈依次弹出到输出序列
1.4代码示例
#include<iostream>
#include<string>
#include<stack>
#include<vector>
#include<locale>
using namespace std;
//判断是否为运算符
bool isops(string s) {
if (s == "+" || s == "-" || s == "*" || s == "/")
return true;
else
return false;
}
//定义运算符优先级
int icp(char c)
{
if (c == '*' || c == '/')
return 1;
if (c == '+' || c == '-')
return 0;
}
//中缀转后缀
vector <string> MidtoRPN(string s)
{
vector<string> out;
stack<string> sta;
string str = "";
int i = 0;
while (i < s.size())
{
//1.读入操作数
if (isdigit(s[i]))
{
str += s[i];
i++;
while (i < s.size() && isdigit(s[i]))
{
str += s[i];
i++;
}
sta.push(str);
str = "";
}
//2.读入开括号
else if (s[i] == '(')
{
str += s[i];
sta.push(str);
str = "";
i++;
}
//3.读入闭括号
else if (s[i] == ')')
{
while (!sta.empty() && sta.top()[0] != '(')
{
out.push_back(sta.top());
sta.pop();
}
sta.pop();//弹出左括号
i++;
}
//4.读入运算符
else
{
while (!sta.empty() && sta.top()[0] != '(' && icp(s[i]) <= icp(sta.top()[0]))
{
out.push_back(sta.top());
sta.pop();
}
str += s[i];
sta.push(str);
str = "";
i++;
}
}
while (!sta.empty())
{
out.push_back(sta.top());
sta.pop();
}
return out;
}
//后缀表达式求值
double evalRPN(vector<string>& tokens)
{
double a, b;
stack<double> sta;
for (auto i : tokens)
{
if (!isops(i))
{
sta.push((double)stoi(i));
}
else
{
a = sta.top();
sta.pop();
b = sta.top();
sta.pop();
if (i == "+")
sta.push(a + b);
else if (i == "-")
sta.push(b - a);
else if (i == "*")
sta.push(a * b);
else if (i == "/")
{
if (a == 0)
{
cout << "除数不能为0" << endl;
exit(0);
}
sta.push(b / a);
}
}
}
return sta.top();
}
//中缀表达式间接求值
double calculate(string s)
{
vector<string> res = MidtoRPN(s);
return evalRPN(res);
}
int main()
{
double result = calculate("3*(7-2)");
cout << result << endl;
return 0;
}
2.中缀表达式直接求值
2.1与第一种方法思路有什么区别
大体思路相同,只是在第一种方法中,我们将从运算符栈中弹出的运算符输出到后置表达式中;而弹出的运算符不输出,而是从操作数栈取两个数直接进行运算,并把运算结果压入操作数栈。
2.2需要什么样的数据结构
- 一个操作数栈(nums)
- 一个运算符栈(ops)
2.3 运算符的优先级排序
该种方法下的优先级排序极为复杂,你可能看过下面这两种图
其实下面这张图就是按上面优先级的编码进行比较得到的图,可是为什么,为什么会是这样呢?
2.3.1什么是栈内优先级,什么是栈外优先级?
当遍历到一个运算符时,我们先不入栈,而将它的优先级与栈顶元素的优先级比较;当前遍历到的运算符在栈外,取栈外优先级,栈顶元素取栈内优先级,将这两种优先级进行比较。
2.3.2为什么要有栈内和栈外两种优先级,一种不行吗?
根据2.1的分析,这两种方法大体思路一样,直接求中缀表达式用一种优先级的排序是完全可以的,间接法也可以用栈内和栈外两种优先级算法。这两种方法使用时是等价的,而大家约定俗称的作法就是:转后缀表达式法用一种优先级排序,直接法用两种优先级排序,也许这就是经典8,咱也不太懂呢~
2.3.3两种优先级是怎么排的,意义是什么?
我们发现所有的二元运算符不管是在栈内还是栈外的优先级排序都是一样的:即乘方>乘除>加减。这与我们在前文的优先级排序相符,而这种双优先级排序的灵魂就在于将左括号 ( 、右括号 )及 # 进行了编码。下面为运算符进栈的几种情况的编码分析:
- 对于 # :我们用这个符号标志着表达式的开始和结束,若计算 "3*(7-2)" ,转为计算 "#3*(7-2)#" ,一开始先在运算符栈中放入一个 #, 再遇到一个#时,说明表达式已经结束,我们需要依次取出运算符栈中剩下的运算符进行运算,直到两个#相遇,程序结束,返回最终结果,因此,我们需要在遇到#时将栈中全部元素出栈,分析可知#的栈外优先级应该是最低的,编码为0;
- 对于 ( : 遇到左括号需要直接压栈,因此其栈外优先级应该比所有二元运算符的栈内优先级都高,编码为8;
- 对于 ):遇到右括号,我们需要将左右括号之间的元素出栈,因此其栈外优先级应该比所有二元运算符的栈内优先级都低,编码为1;
- 当遇到左右括号相遇时,我们需要消除这一对括号,定义当栈外优先级和栈内优先级相等时消除,则左括号( 的栈内优先级应该等于右括号 ) 的栈外优先级,为1。
- 对于所有的二元运算符: 宏观顺序是乘方>乘除>加减,而每一种类型的栈内外优先级都不相等,而是差1,这是因为当连续遇到两个同类型的运算符时,我们应该从左到右运算,即先算栈内的,再算栈外的,因此内比外要高。
可以看出,该运算符栈实际上也是在维护一个严格递增的单调队列。而通过将( 、)、# 的编码,简化了上一种算法中括号匹配和表达式遍历结束剩余运算符出栈的步骤,使其和其它二元运算符一样通过编码参与运算,消除其特殊性。
1.4操作步骤
依次读入表达式中的每个字符,若是
•操作数,则进nums栈;
•运算符s1,则和ops栈中的栈顶元素s2做比较再操作。
1)若icp(s1)>isp(s2),则s1入栈,接收下一字符;
2)若icp(s1)==isp(s2),则ops中的栈顶元素出栈,接收下一字符。
3)若icp(s1)<isp(s2),则从nums栈顶弹出两个操作数,与ops中的栈顶元素做运算,并将运算结果入nums栈,并不接收下一字符,因为栈内可能有多个比该字符优先级高的运算符,要一直进行二元运算的操作直至转为1或2的情况;
直至表达式扫描完毕。
1.5代码示例
#include <iostream>
#include <stack>
#include <string>
#include <cctype>
using namespace std;
//栈内优先级
int isp(char ch)
{
switch (ch)
{
case '#': return 0;
case '(': return 1;
case '^':return 7;
case '*':case '/':case '%':return 5;
case '+':case '-':return 3;
case ')':return 8;
}
}
//栈外优先级
int icp(char ch)
{
switch (ch)
{
case '#': return 0;
case '(': return 8;
case '^':return 6;
case '*':case '/':case '%':return 4;
case '+':case '-':return 2;
case ')':return 1;
}
}
//比较栈内栈外优先级大小
char precede(int isp, int icp)
{
if (isp < icp) return '<';
else if (isp > icp) return '>';
else return '=';
}
//计算最简单的双目运算符表达式
int cal(int first, char op, int second)
{
switch (op)
{
case'+':
return(first + second);
case'-':
return(first - second);
case'*':
return(first * second);
case'/':
return(first / second);
case'%':
return(first % second);
case'^':
return(pow(first, second));
}
}
//计算中缀表达式
int middexpression(string s)
{
s += '#';//表达式尾加#
stack<char>ops;
ops.push('#');//表达式头加#
stack<int>nums;
int num = 0, i = 0,first,second;
while (s[i] != '#' || ops.top() != '#')// 字符扫描完毕且运算符栈仅有‘#’时返回结束
{
//1.是数字
if (isdigit(s[i]))
{
num = num*10 + (s[i] - '0');
if (!isdigit(s[i + 1]))
{
nums.push(num);
num = 0;
}
i++;
}
//2.是字符有三种情况
else
{
switch (precede(isp(ops.top()), icp(s[i])))
{
case '<':// 栈顶元素优先权低
ops.push(s[i]);
i++;
break;
case '=':// 脱括号并接收下一字符
ops.pop();
i++;
break;
case '>':// 退栈并将运算结果入栈,但不取下一表达式字符
second = nums.top();
nums.pop();
first = nums.top();
nums.pop();
nums.push(cal(first,ops.top(), second));
ops.pop();
break;
}
}
}
return nums.top();
}
int main()
{
cout << middexpression("30*2^3*(7-2)") << endl;
return 0;
}