zoukankan      html  css  js  c++  java
  • 算式与逆波兰式

    致憨憨的从前

    当年,老师布置一道作业:编写一个计算器,要求输入算式,给出结果。算式中只包含+-*/^这几个运算符,算式中不含负数。由于是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]);
    }
    
  • 相关阅读:
    2017年软件工程基础-个人项目作业
    [2017BUAA软工]第1次个人作业
    [2017BUAA软工]第0次个人作业
    个人作业——软件工程实践总结&个人技术博客
    个人作业——软件评测
    结对第二次作业
    软件工程结对作业
    寒假作业(2/2)
    软件工程寒假快乐作业
    技术总结——Vue页面刷新的方法
  • 原文地址:https://www.cnblogs.com/focksor/p/13256191.html
Copyright © 2011-2022 走看看