表达式求值用python_用栈进行表达式求值-程序员宅基地

技术标签: 表达式求值用python  

上一节课,我们介绍了栈,并且在习题里使用栈计算了后缀表达式(最后一道题,这道题一定要先完成。如果那道题做不出来,这节课的内容就更加难以理解)。

我们今天继续看一下,如何使用栈完成标准的四则混合运算表达式求值。

不同于后缀表达式,遇到一个运算符就可以直接从栈里取两个数进行运算。在标准的四则混合运算表达式中(或者我们称之为中缀表达式),遇到一个操作符是不能直接计算的,因为计算的顺序要取决于后面的运算符。多举几个例子,大家就能明白了。由于加和减是相同的运算优先级,乘和除是相同的运算优先级,我们就只用加和乘来举例就可以了。

我们像计算后缀表达式一样,引入一个操作数栈。由于后缀表达式不用缓存 +-*/ 这些操作符,所以一个操作数栈就够了。但是,中缀表达式的计算是要用到操作符的缓存的,所以,我们还要再引入一个操作符栈,专门存储各个操作符。

第一个例子:a + b * c,我们从前向后扫描,遇到a,压到操作数栈里,遇到 + ,压到操作符栈里,下一个是 b,此时,我们是不能像后缀表达式求值那样,直接计算 a + b,然后压栈的。因为我们不确定,b 是否会先参与后面的运算,所以我们只能把 b 先压栈,继续向后扫描。看到 * 以后,再把 * 压到操作符栈里;看到 c 以后,也要把 c 先压栈,理由与 b 压栈相同。接下来,再往下扫描,就发现已经到了末尾了。那我们就可以放心地从操作符栈里取出顶上的操作符,在这个例子中,就是 *,然后再从操作数栈里取出两个操作数,就是 b 和 c,然后把b和c的积,不妨记为d,放到操作数栈里。接下来,再去看操作符栈里,还有一个 +,那就把 + 取出来,然后去操作数栈里取出 a 和 d,计算它们的和,这就是最终的结果了。

第二个例子:a + b + c,我们从前向后扫描,遇到a,压到操作数栈里,遇到 + ,压到操作符栈里,下一个是 b,压到操作数栈里。再下一个,又是 + ,由于加法的结合律,我们知道,先把a + b 的结果计算出来,或者后计算,都不会影响最终的结果。所以我们就把能做的化简先做掉。就是说,在扫描到第二个操作符是 + 以后,我们就把第一个操作符取出来,再把两个操作数取出来,求和并且把和送到操作数栈里。接下来的过程与第一个例子是相同的,不再赘述了。

通过这两个例子,我们看到,一个操作符究竟什么时候进行运算,并不取决于它前面的那个操作符是什么,而是取决于它后面的那个操作符是什么。更具体一点讲:如果后面的操作符的运算优化级比前面的操作符高,那么前面的操作符就必须延迟计算;如果后面的操作符优化级比前面的低或者相等,那么前面的操作符就可以进行计算了。上面这句话,非常重要,是我们这节课的核心。请多读几遍,结合上面的两个例子,务必想明白它。

好了,理解了这个,就可以上代码了。

先看 Stack的完整定义:

class Stack {

private ArrayList list;

public Stack(int size) {

this.list = new ArrayList(size);

}

public T getTop() {

if (isEmpty())

return null;

return list.get(list.size() - 1);

}

public void push(T t) {

this.list.add(t);

}

public T pop() {

if (isEmpty())

return null;

return list.remove(list.size() - 1);

}

public boolean isEmpty() {

return list.isEmpty();

}

}

然后我们创建两个栈,一个是操作数栈,一个是操作符栈:

public class StackExpression {

public static void main(String args[]) throws IOException {

TokenStream ts = new ExpressionTokenStream(System.in);

Stack numbers = new Stack<>(100);

Stack operators = new Stack<>(100);

}

}

其中,TokenStream 是这节课里的课后作业:适配器模式

按照上面分析的算法,如果遇到数字,就压栈到numbers里,如果遇到操作符,就要看前面一个的操作符的优先级是否比当前操作符高,如果前一个操作符高,那么执行前一个操作符的操作,如果是后面的高,那就只要把后面的操作符压栈即可:

public class StackExpression {

public static void main(String args[]) throws IOException {

TokenStream ts = new ExpressionTokenStream(System.in);

Stack numbers = new Stack<>(100);

Stack operators = new Stack<>(100);

while (ts.getToken().tokenType != Token.TokenType.NONE) {

if (ts.getToken().tokenType == Token.TokenType.INT) {

numbers.push((Integer)ts.getToken().value);

ts.consumeToken();

}

else {

if (operators.getTop() == null || preOrder( operators.getTop().tokenType, ts.getToken().tokenType) < 0) {

operators.push(ts.getToken());

ts.consumeToken();

}

else {

binaryCalc(numbers, operators);

operators.push(ts.getToken());

ts.consumeToken();

}

}

}

while (!operators.isEmpty()) {

binaryCalc(numbers, operators);

}

System.out.println("result is " + numbers.getTop());

}

private static void binaryCalc(Stack numbers, Stack operators) {

int a = numbers.pop();

int b = numbers.pop();

Token oprt = operators.pop();

int d = 0;

if (oprt.tokenType == Token.TokenType.PLUS)

d = b + a;

else if (oprt.tokenType == Token.TokenType.MULT)

d = a * b;

else if (oprt.tokenType == Token.TokenType.MINUS)

d = b - a;

else if (oprt.tokenType == Token.TokenType.DIV)

d = b / a;

numbers.push(d);

}

private static int preOrder(Token.TokenType left, Token.TokenType right) {

if (left == Token.TokenType.PLUS || left == Token.TokenType.MINUS) {

if (right == Token.TokenType.MULT || right == Token.TokenType.DIV)

return -1;

else

return 1;

}

else

return 1;

}

}

好了。我们的这个程序已经可以处理类似 a + b - c * d + e / f 这样的算式了。

为了让大家看得更清楚,我用图把 a + b * c / d 的过程画一遍。

首先,遇到 a ,把 a 送到操作数栈,遇到 + ,送到操作符栈:

遇到 b,压栈

遇到乘,由于乘的优先级高于加,所以,现在就什么也不做,只把乘号进栈:

同样,遇到 c 把 c 进栈(此图略,请自己补上),再遇到 / ,由于除的优先级与 * 的优先级相同,所以,乘就可以先做了。这个动作是把乘号出栈,把c 和 b出栈,求 c * b的值,并且把这个值入栈。即:

然后把 / 入栈,把 d 入栈:

现在到了运算的结尾了。我们只需要把现在的栈里的内容从顶向下计算起来即可,先算除法:

再算加法:

但是,括号怎么办?

可以这样想,在遇到形如 a * (b + c) 这样的形式的时候,左边的乘法是一定不能做的,我们只需要将左括号进栈即可。所以,我们可以把TokenStream里取得的左括号看做是一个优先级无穷大的运算符,它使得左方的运算符都不能提前进行计算。

遇到右括号时,b + c 这个加法是可以进行运算的了,所以可以把右括号看作是一个优先级无穷小的运算符,它会使得操作符栈上的所有运算符都出栈并执行计算,直到遇到左括号。遇到左括号以后,只需要把左括号从栈里弹出来,然后让它和右括号一起狗带即可(这就是括号匹配啊,同学们~)。由于括号内的计算都已经完成了,结果是一个整数,我们已经在计算的过程中把这个整数放到操作数栈里了。所以整个括号内的求值就完成了。

好了。今天的课程很短,但是比较难理解,尤其是,今天的程序都依赖于本周前边的课程。请务必先完成本周前边的课程再来看这节课。

演示一下,我的完整版的效果:

好了。今天的课程就到这里了。

今天的作业:

把括号的逻辑补充好。使用栈完成完整的表达式求值。

我的完整的代码上传到小密圈《进击的Java新人》里了,圈里的同学请先不要直接看代码,请一定自己动动脑筋,争取把这个过程想明白。尤其是要对照着后缀表达式求值的程序去看,体会一下这两个题目有什么相关性。

好了。到这里为止,第二周的课程就全部结束了。

总结一下:

在Java中的设计模式:适配与装饰 这一课中,我们学会了如何把标准输入上的字符处理成各种Token

数据结构(一):栈 这一课中,我们学会了栈这种数据结构,并且使用栈处理了括号匹配,以及后缀表达式求值。有了这两节课的基础,在本节课中,我们写出了完整的表达式求值的程序 。

本周课程到这里就结束了。下一周,我会介绍另外一种方法进行表达式求值。那就是使用自顶向下的文法分析来处理表达式。这将为我们稍稍揭开编译器工作方式的一点奥秘。谢谢你们关注我的课程,各位读者,圣诞快乐~

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_39870413/article/details/111813292

智能推荐

【信贷风控30分钟精通39】风控策略体系搭建2-程序员宅基地

文章浏览阅读938次,点赞23次,收藏21次。反欺诈策略是为防范恶意客户采取欺诈行为谋取利益而制订的策略,目的是通过对欺诈行为的识别,遏制欺诈风险,为金融机构止损。根据欺诈的不同维度,欺诈的分类目前,应对欺诈风险的有效措施包括反欺诈规则和反欺诈模型。

yum安装及配置_安装yum-程序员宅基地

文章浏览阅读10w+次,点赞40次,收藏332次。yum是用来管理rpm的,就跟maven管理jar包相似。yum源(库)分为本地库、网络库。首先要配置yum源,可支持多个源。先查看一下挂载情况:df -h这里我们要更换光盘,并挂载:mount /dev/cdrom /mnt(如果不能成功挂载,点击一下连接即可)之后再次使用 df -h命令,就能查看到光盘的内容。下面我们cd到 /mnt下查看一下:首先关注一下Pa..._安装yum

关于STM32 CAN的过滤器/滤波器_stm32can mailbox filter-程序员宅基地

文章浏览阅读3.8k次,点赞5次,收藏12次。1.在设置CanTxMsg.StdId时注意需要将其右移一位,比如如下滤波器配置:CAN_FilterInitStructure.CAN_FilterNumber=0;CAN_FilterInitStructure.CAN_FilterMode=CAN_FilterMode_IdMask;CAN_FilterInitStructure.CAN_FilterScale=CAN_Filter..._stm32can mailbox filter

HDU 5119 Happy Matt Friends(动态规划)【状压基础类模板】_matt has n friends. they are playing a game togeth-程序员宅基地

文章浏览阅读373次。att has N friends. They are playing a game together. Each of Matt’s friends has a magic number. In the game, Matt selects some (could be zero) of his friends. If the xor (exclusive-or) sum of the selected friends’magic numbers is no less than M , Matt wi_matt has n friends. they are playing a game together.

vue3+vite+ts项目配置开发环境和生产环境 打包命令配置_vite打包配置-程序员宅基地

文章浏览阅读8.4k次,点赞6次,收藏29次。开发环境和生产环境的配置和打包方式有所不同,下面是基于vue3+vite+ts项目的开发环境和生产环境配置及打包方式的详细说明。打包完成后会在项目根目录下生成dist目录,里面包含了打包后的静态文件和index.html文件,可以直接部署到服务器上。这里配置了三个命令,分别是开发环境启动命令、开发环境打包命令和生产环境打包命令。1.3 配置.env.development。2.2 配置.env.production。1.2 配置vite.config.ts。2.1 配置vite.config.ts。_vite打包配置

(最新最详细)安装ubuntu18.04-程序员宅基地

文章浏览阅读2w次,点赞4次,收藏91次。目录1. window10中下载ubuntu镜像2. 制作U盘启动盘3. Ubuntu 分配硬盘空间1. window10中下载ubuntu镜像下载地址2. 制作U盘启动盘安装制作工具:UltraISO(点我下载),下载完成后安装插入用来做启动盘的U盘(最好是usb3.0接口,16GB或以上),并清空里面的文件打开安装好的UltraISO,点击继续试用按钮工作界面进入工作界面后,点击菜单栏文件(F),在弹出的选项卡里点击打开在弹出的文件选择对话框中找到下载好的 Ubuntu18.04._ubuntu18.04

随便推点

Linux 文件系统 EXT4 的前世今生-程序员宅基地

文章浏览阅读161次。在先前关于Linux文件系统的文章中,我写了一份说明书去介绍Linux文件系统,里面有一些高级的概念,比如说,一切都是文件。我很想去深入地讨论更多EXT文件系统的特性的信息。所以,首先让我们来回答这个问题:什么是文件系统?一个文件系统应该遵循以下特点: 1.数据存储:文件系统主要的功能是结构化存储和取回数据。 2.命名空间:提供一套命名和组织的方法,就是命名和结构化数据的规则。 3.安全模型:一种访问控制的策略。 4.API:系统操控文件系统对象的函数,就像操作文件夹...

别人家的公司:微软为员工发1500美元疫情奖金-程序员宅基地

文章浏览阅读308次。西雅图IT圈:seattleit【今日作者】Dexter读书巨慢理事会会长别人家的公司什么样?坐拥巨额现金流的微软,一言不合就发钱。01昨天微软首席人事官凯瑟琳霍根宣布——将向微软全球员工..._微软 西雅图 年底奖金

Ik分词器配置远程扩展字典_ik analyzer 扩展词典配置远程词典 可实时编辑-程序员宅基地

文章浏览阅读2k次。通过配置远程扩展词典,可以读取远程词典,当改变远程词典时,不必重启服务器,elasticsearch会自动加载并进行分词。步骤:配置文件服务器,把远程扩展词典放到服务器下。修改elasticsearch目录下plugins\ik\config\IKAnalyzer.cfg.xml文件并保存,如下: <properties> <comment>IK A..._ik analyzer 扩展词典配置远程词典 可实时编辑

分布式系列教程(11) -分布式协调工具Zookeeper(分布式锁实现)_分布式锁 的具体实现工具-程序员宅基地

文章浏览阅读553次,点赞2次,收藏2次。代码已提交至Github,有兴趣的同学可以下载来看看(git版本号:bea4d6f7ec9f7309033bcfa43316a660171ae5b6):https://github.com/ylw-github/Zookeeper-Demo本文目录结构:l____1. 知识点回顾l________1.1 多线程l________1.2 Java共享内存模型l____2. 分布式锁的解决方..._分布式锁 的具体实现工具

Nginx网站服务详解(Nginx服务的主配置文件 ——nginx.conf)-程序员宅基地

文章浏览阅读9.3k次,点赞9次,收藏51次。Nginx网站服务详解,Nginx服务的主配置文件,修改,监听,配置,密码认证,以及IP和端口虚拟主机配置方法,含图文步骤拆解讲解_nginx.conf

推荐文章

热门文章

相关标签