算式与逆波兰式

致憨憨的从前

当年,老师布置一道作业:编写一个计算器,要求输入算式,给出结果。算式中只包含+-*/^这几个运算符,算式中不含负数。由于是Python课程,我很快给出了解题方式,如下:

while True:
    input_str = input("输入表达式:")
    print(eval(input_str))

不出意料,该程序完美地解决了问题,唯一的缺点是老师看到代码的时候差点给我挂科了。

于是我重新编写代码试图解决这个问题。对于当时啥都不会的我来说,这是一个难题,我的解决思路如下:

  1. 从头开始检索字符串,直到找到第一个)
  2. 从找到的)开始往回检索,直到找到第一个(。显而易见,这两个坐标中间是不含括号,只含数字和运算符。
  3. 提取两个坐标之间的字符串,根据运算优先级循环检索算式内的运算符并进行计算,并将计算结果字符串中的操作数和操作符,直到该算式只剩下一个数字,这个数字就是子式的运算结果。
  4. 将该数字替换回原算式的两个括号左边中间的位置,返回步骤1并重复,直到算式内不含括号。
  5. 执行步骤3,剩下的数字则为最终结果。

这个解决思路从理论上来说是没有问题的,但是存在以下问题:

  • 需要进行很多次循环,对于太长的算式会消耗太多时间
  • 计算过程中对算式字符串进行多次修改,对于太长的算式会消耗太多时间

不过,尽管存在问题,但是对于一个课程作业来说,还是没有太大影响的,所以我也一直没有想是否有更好的解决方法。但是,前两天我刷题的时候,又遇到了一次相同的题目,而且是要求用c语言写。对于没有那么熟练使用c语言的我来说,字符串的处理是比较困难的,所以我在网上寻找题目的解决方法,从而了解到逆波兰式这个神奇的东西。这也警醒我,对于一个问题,即使自己有想法能够解决问题,也一定要交流学习是否有更好的解决方法,更新自己的知识库。

为什么需要逆波兰式

首先介绍下逆波兰式的历史(摘自维基百科-逆波兰表示法):

逆波兰表示法Reverse Polish notationRPN,或逆波兰记法),是一种是由波兰数学家扬·武卡谢维奇1920年引入的数学表达式方式,在逆波兰记法中,所有操作符置于操作数的后面,因此也被称为后缀表示法。逆波兰记法不需要括号来标识操作符的优先级。

逆波兰结构由弗里德里希·鲍尔(Friedrich L. Bauer)和艾兹格·迪科斯彻在1960年代早期提议用于表达式求值,以利用堆栈结构减少计算机内存访问。逆波兰记法和相应的算法澳大利亚哲学家计算机学家查尔斯·汉布尔(Charles Hamblin)在1960年代中期扩充[1][2]

在1960和1970年代,逆波兰记法广泛地被用于台式计算器,因此也在普通公众(如工程商业金融等领域)中使用。

中缀表达式在计算机的计算过程

中缀表达式的意思是,运算符处于两个运算对象之间,例如:1+2,1和2是运算对象,+是运算符,处于中间。

对于给定算式1+(2+3)*(4^5+6/3),我们使用一棵树来直观感受该中缀表达式算式结构。

graph TD A((+)) A --- B((1)) A --- C((*)) C --- D((+)) D --- E((2)) D --- F((3)) C --- G((+)) G --- H((^)) H --- I((4)) H --- J((5)) G --- K((/)) K --- L((6)) K --- M((3))

显而易见,对该树进行中序遍历,可以得到原来的中缀表达式。对于这棵树中任意操作符,我们可以这样计算该子式的结果:

取出左子树的操作数

取出该操作符

取出右子树的操作数

根据操作符对操作数进行计算

由此,一层一层地剪掉子树,将子树的计算结果替换到操作符的节点位置,如此重复,直到最后只留一个根节点,则根节点的数字就是该式的计算结果。实际上,我上文提到的完成课程作业的方法跟这里的解决思想是一摸一样的,只不过这里使用了树的节点作为表达,而我是直接替换了字符串的相应位置。

中缀表达式的缺陷

从上一节可以看出,使用中缀表达式进行计算,需要先构建出树,再递归计算子树的结果,这样就会导致当运算式很长的情况下,这棵树会非常深,与之对应的,递归计算结果的时候就会导致递归堆栈非常深。并且,使用算式字符串得出该树也不是一件简单的事情,需要考虑运算符优先级的问题。

将中缀表达式转为逆波兰式

将中缀表达式的树转为逆波兰式

同样是上述的算式1+(2+3)*(4^5+6/3),其树结构同样如下:

graph TD A((+)) A --- B((1)) A --- C((*)) C --- D((+)) D --- E((2)) D --- F((3)) C --- G((+)) G --- H((^)) H --- I((4)) H --- J((5)) G --- K((/)) K --- L((6)) K --- M((3))

接下来,对其进行后序遍历,即:

取出左子树的操作数

取出右子树的操作数

取出该操作符

根据操作符对操作数进行计算

可以得到,该树的后序遍历结果是:

1 2 3 + 4 5 ^ 6 3 / + * +

该式就是算式1+(2+3)*(4^5+6/3)对应的逆波兰式。

不构建树得到逆波兰式的方法

虽然我们知道了如何将中缀表达式对应的树转换为逆波兰式,由于是后序遍历,计算该式的结果非常容易。但是相较于中序遍历,这种将树进行后序遍历的做法并没有解决任何问题。不信?我将中缀表达式的缺陷粘贴下来,可以看到毫无违和感,因为他们本质上是一样的,不过是换了一种形式表达而已。

中缀表达式的缺陷

从上一节可以看出,使用中缀表达式进行计算,需要先构建出树,再递归计算子树的结果,这样就会导致当运算式很长的情况下,这棵树会非常深,与之对应的,递归计算结果的时候就会导致递归堆栈非常深。并且,使用算式字符串得出该树也不是一件简单的事情,需要考虑运算符优先级的问题。

那应该如何在不构建树的情况下得到逆波兰式呢?我们用一个栈来存储逆波兰式,称为结果栈,再用一个辅助栈来解决运算优先级的问题(原理是用辅助栈存储低优先级的运算符,直到碰到高优先级运算符再按顺序压入到结果栈)。直接将算式转换为逆波兰式的运行方法如下:

从左到右遍历算式,逐个取出里面的元素。

  1. 如果取出的是操作数,直接压入结果栈
  2. 如果取出的是左括号(,压入辅助栈
  3. 如果取出的是运算符,如果此时辅助栈为空,或者辅助栈栈顶为(,或者如果该运算符优先级大于辅助栈顶部的运算符,则将该运算符压入辅助栈。否则,弹出辅助栈栈顶的运算符并压入到结果栈中,直到辅助栈栈顶的运算符低于该运算符,则将该运算符压入辅助栈
  4. 如果取出的是右括号),则将逐个弹出辅助栈中的运算符并压入到结果栈中,直到辅助栈栈顶为(,弹出(并抛弃,将)也抛弃
  5. 重复以上步骤,直到处理完整个算式
  6. 将辅助栈中所有运算符逐个弹出并压入到结果栈中
  7. 将结果栈进行逆序处理(因为栈的输出本来就是逆序的,逆逆得正)

使用逆波兰式计算最终结果

创建一个操作数栈,计算方法如下(注意此时结果栈已进行逆序处理):

  1. 不断弹出结果栈并压入到操作数栈,直到结果栈栈顶为运算符
  2. 弹出操作数栈顶为v2,再弹出操作数栈顶为v1,将[v1操作符v2,如v1+v2]的结果压入操作数栈,弹出结果栈栈顶的运算符并抛弃
  3. 重复以上步骤,直到结果栈为空,则此时操作数栈只剩一个数,为最终结果

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]);
}
原文地址:https://www.cnblogs.com/focksor/p/13256191.html