致憨憨的从前
当年,老师布置一道作业:编写一个计算器,要求输入算式,给出结果。算式中只包含+-*/^
这几个运算符,算式中不含负数。由于是Python课程,我很快给出了解题方式,如下:
while True:
input_str = input("输入表达式:")
print(eval(input_str))
不出意料,该程序完美地解决了问题,唯一的缺点是老师看到代码的时候差点给我挂科了。
于是我重新编写代码试图解决这个问题。对于当时啥都不会的我来说,这是一个难题,我的解决思路如下:
- 从头开始检索字符串,直到找到第一个
)
。- 从找到的
)
开始往回检索,直到找到第一个(
。显而易见,这两个坐标中间是不含括号,只含数字和运算符。- 提取两个坐标之间的字符串,根据运算优先级循环检索算式内的运算符并进行计算,并将计算结果字符串中的操作数和操作符,直到该算式只剩下一个数字,这个数字就是子式的运算结果。
- 将该数字替换回原算式的两个括号左边中间的位置,返回
步骤1
并重复,直到算式内不含括号。- 执行
步骤3
,剩下的数字则为最终结果。
这个解决思路从理论上来说是没有问题的,但是存在以下问题:
- 需要进行很多次循环,对于太长的算式会消耗太多时间
- 计算过程中对算式字符串进行多次修改,对于太长的算式会消耗太多时间
不过,尽管存在问题,但是对于一个课程作业来说,还是没有太大影响的,所以我也一直没有想是否有更好的解决方法。但是,前两天我刷题的时候,又遇到了一次相同的题目,而且是要求用c语言写。对于没有那么熟练使用c语言的我来说,字符串的处理是比较困难的,所以我在网上寻找题目的解决方法,从而了解到逆波兰式这个神奇的东西。这也警醒我,对于一个问题,即使自己有想法能够解决问题,也一定要交流学习是否有更好的解决方法,更新自己的知识库。
为什么需要逆波兰式
首先介绍下逆波兰式的历史(摘自维基百科-逆波兰表示法):
逆波兰表示法(Reverse Polish notation,RPN,或逆波兰记法),是一种是由波兰数学家扬·武卡谢维奇1920年引入的数学表达式方式,在逆波兰记法中,所有操作符置于操作数的后面,因此也被称为后缀表示法。逆波兰记法不需要括号来标识操作符的优先级。
逆波兰结构由弗里德里希·鲍尔(Friedrich L. Bauer)和艾兹格·迪科斯彻在1960年代早期提议用于表达式求值,以利用堆栈结构减少计算机内存访问。逆波兰记法和相应的算法由澳大利亚哲学家、计算机学家查尔斯·汉布尔(Charles Hamblin)在1960年代中期扩充[1][2]
中缀表达式在计算机的计算过程
中缀表达式的意思是,运算符处于两个运算对象之间,例如:1+2
,1和2是运算对象,+是运算符,处于中间。
对于给定算式1+(2+3)*(4^5+6/3)
,我们使用一棵树来直观感受该中缀表达式算式结构。
显而易见,对该树进行中序遍历,可以得到原来的中缀表达式。对于这棵树中任意操作符,我们可以这样计算该子式的结果:
取出左子树的操作数
取出该操作符
取出右子树的操作数
根据操作符对操作数进行计算
由此,一层一层地剪掉子树,将子树的计算结果替换到操作符的节点位置,如此重复,直到最后只留一个根节点,则根节点的数字就是该式的计算结果。实际上,我上文提到的完成课程作业的方法跟这里的解决思想是一摸一样的,只不过这里使用了树的节点作为表达,而我是直接替换了字符串的相应位置。
中缀表达式的缺陷
从上一节可以看出,使用中缀表达式进行计算,需要先构建出树,再递归计算子树的结果,这样就会导致当运算式很长的情况下,这棵树会非常深,与之对应的,递归计算结果的时候就会导致递归堆栈非常深。并且,使用算式字符串得出该树也不是一件简单的事情,需要考虑运算符优先级的问题。
将中缀表达式转为逆波兰式
将中缀表达式的树转为逆波兰式
同样是上述的算式1+(2+3)*(4^5+6/3)
,其树结构同样如下:
接下来,对其进行后序遍历,即:
取出左子树的操作数
取出右子树的操作数
取出该操作符
根据操作符对操作数进行计算
可以得到,该树的后序遍历结果是:
1 2 3 + 4 5 ^ 6 3 / + * +
该式就是算式1+(2+3)*(4^5+6/3)
对应的逆波兰式。
不构建树得到逆波兰式的方法
虽然我们知道了如何将中缀表达式对应的树转换为逆波兰式,由于是后序遍历,计算该式的结果非常容易。但是相较于中序遍历,这种将树进行后序遍历的做法并没有解决任何问题。不信?我将中缀表达式的缺陷粘贴下来,可以看到毫无违和感,因为他们本质上是一样的,不过是换了一种形式表达而已。
中缀表达式的缺陷
从上一节可以看出,使用中缀表达式进行计算,需要先构建出树,再递归计算子树的结果,这样就会导致当运算式很长的情况下,这棵树会非常深,与之对应的,递归计算结果的时候就会导致递归堆栈非常深。并且,使用算式字符串得出该树也不是一件简单的事情,需要考虑运算符优先级的问题。
那应该如何在不构建树的情况下得到逆波兰式呢?我们用一个栈来存储逆波兰式,称为结果栈,再用一个辅助栈来解决运算优先级的问题(原理是用辅助栈存储低优先级的运算符,直到碰到高优先级运算符再按顺序压入到结果栈)。直接将算式转换为逆波兰式的运行方法如下:
从左到右遍历算式,逐个取出里面的元素。
- 如果取出的是操作数,直接压入结果栈
- 如果取出的是左括号
(
,压入辅助栈- 如果取出的是运算符,如果此时辅助栈为空,或者辅助栈栈顶为
(
,或者如果该运算符优先级大于辅助栈顶部的运算符,则将该运算符压入辅助栈。否则,弹出辅助栈栈顶的运算符并压入到结果栈中,直到辅助栈栈顶的运算符低于该运算符,则将该运算符压入辅助栈- 如果取出的是右括号
)
,则将逐个弹出辅助栈中的运算符并压入到结果栈中,直到辅助栈栈顶为(
,弹出(
并抛弃,将)
也抛弃- 重复以上步骤,直到处理完整个算式
- 将辅助栈中所有运算符逐个弹出并压入到结果栈中
- 将结果栈进行逆序处理(因为栈的输出本来就是逆序的,逆逆得正)
使用逆波兰式计算最终结果
创建一个操作数栈,计算方法如下(注意此时结果栈已进行逆序处理):
- 不断弹出结果栈并压入到操作数栈,直到结果栈栈顶为运算符
- 弹出操作数栈顶为v2,再弹出操作数栈顶为v1,将[v1操作符v2,如v1+v2]的结果压入操作数栈,弹出结果栈栈顶的运算符并抛弃
- 重复以上步骤,直到结果栈为空,则此时操作数栈只剩一个数,为最终结果
C语言编写对应程序
# include<stdio.h>
# include<string.h>
// 算式的最长长度
# define MAX_LEN 50
int calc(int v1, char opera, int v2) {
int res;
switch (opera) {
case '+':
return v1+v2;
case '-':
return v1-v2;
case '*':
return v1*v2;
case '/':
return v1/v2;
case '^':
res = 1;
for (int i=0; i<v2; i++) {
res *= v1;
}
return res;
}
}
int char_type(char c) {
if (c == '^') return 3;
if (c == '*' || c == '/') return 2;
if (c == '+' || c == '-') return 1;
if (c>=48 && c<=57) return 0;
return -1;
}
int main(void) {
char str[MAX_LEN];
scanf("%s", str);
int len = strlen(str);
char stack1[MAX_LEN] = ""; // 辅助栈
char stack2[MAX_LEN] = ""; // 结果栈
int stack1_top = 0;
int stack2_top = 0;
for (int i=0; i<strlen(str); i++) {
if (char_type(str[i]) == 0) {
while (char_type(str[i])==0 && i<len) {
stack2[stack2_top++] = str[i++];
}
stack2[stack2_top++] = ' ';
}
if (str[i] == '(') {
stack1[stack1_top++] = str[i];
}
if (char_type(str[i]) > 0) {
if (stack1_top==0 || stack1[stack1_top-1]=='(' || char_type(str[i])>char_type(stack1[stack1_top-1])) {
stack1[stack1_top++] = str[i];
} else {
while (char_type(stack1[stack1_top-1]) >= char_type(str[i])) {
stack2[stack2_top++] = stack1[--stack1_top];
}
stack1[stack1_top++] = str[i];
}
}
if (str[i] == ')') {
while (stack1[--stack1_top] != '(') {
stack2[stack2_top++] = stack1[stack1_top];
}
stack1[stack1_top] = ' ';
}
}
while (stack1_top) {
stack2[stack2_top++] = stack1[--stack1_top];
}
printf("逆波兰式:%s
", stack2);
int calc_stack[MAX_LEN]; // 操作数栈
int calc_stack_top=0;
char temp[MAX_LEN]; // 用于char*到int的转换
int temp_index = 0;
memset(temp, 0, MAX_LEN);
for (int i=0; i<stack2_top; i++) {
if (char_type(stack2[i]) == 0) {
temp[temp_index++] = stack2[i];
} else if (stack2[i] == ' ') {
calc_stack[calc_stack_top++] = atoi(temp);
temp_index = 0;
memset(temp, 0, MAX_LEN);
} else {
int v2 = calc_stack[--calc_stack_top];
int v1 = calc_stack[--calc_stack_top];
calc_stack[calc_stack_top++] = calc(v1, stack2[i], v2);
}
}
printf("结果:%d
", calc_stack[0]);
}