(c5)栈应用:逆波兰表达式

官官方信息技术老师·14 页·深入(追求细节与边界)·0 次浏览·38 天前
栈后缀表达式图解算法手推

栈应用:逆波兰表达式

看完你能手算中缀转后缀、用栈逐图求值并说清每步原因

按 空格/→ 演示下一步

问一句
1 / 14 页

全部页面点击任意一页,跳回舞台从这页播放

栈后缀表达式图解算法手推

栈应用:逆波兰表达式

看完你能手算中缀转后缀、用栈逐图求值并说清每步原因

1第 1 页 · 栈应用:逆波兰表达式

熟悉的中缀式,麻烦在哪

上一页说了栈能记住「还没轮到的事」。现在拿你最熟悉的算式 3+4×2 开刀:明明一眼能算,为什么计算机处理起来反而要费一番周折?问题就出在我们习以为常的写法上。

优先级是隐含规则
3+4×2 必须先算乘法,但「先」字没写在式子里,全靠约定
括号改变顺序
(3+4)×2 与 3+4×2 结果不同,括号强制覆盖默认优先级
运算符夹中间
读到 + 时,右边的 × 还没出现,没法立刻决定先算谁
做菜时口头报菜名对应 →中缀表达式

口头报「先加盐再炒」要靠听者记顺序,写法里同理要靠规则补全

2第 2 页 · 熟悉的中缀式,麻烦在哪

简化:逆波兰式长什么样

上一页我们看到:中缀式里括号和优先级都是为了约定「先算谁」。如果换一种写法,让运算符紧跟在操作数后面,顺序会不会自己显现出来?看看同一道算式换了位置后长什么样。

运算符后置
先写两个操作数,运算符紧跟其后
顺序自明
符号一出现,就作用于前面最近的两个数
无需括号
位置已定顺序,括号没有存在必要
无需优先级
从左到右读即按序计算,规则只剩一条
菜谱步骤卡对应 →逆波兰式

步骤卡的「食材、食材、动作」对应逆波兰式的「数、数、运算符」

(3+4)×2  ⟹  3 4 + 2 ×(3+4)\times 2 \;\Longrightarrow\; 3\ 4\ +\ 2\ \times
3第 3 页 · 简化:逆波兰式长什么样

中缀 vs 后缀

同一个算式,人爱中缀、机爱后缀。容易混:后缀到底好在哪?逐条对照看。

中缀式(人读)
  • 运算符夹在两数之间
  • 靠括号和优先级消歧义
  • 求值需扫描+回看,机器费劲
后缀式(机读)
  • 运算符紧跟两数之后
  • 无括号,写法本身无歧义
  • 一遍扫描,遇算符即弹栈计算
给人看、手算时选中缀;交给程序自动求值时选后缀。
4第 4 页 · 中缀 vs 后缀

体验:用栈求值的直觉

前页我们看清了后缀式不需要括号,但机器怎么算它?先别想代码,把计算机想成一个端盘子的服务员:手里的托盘就是栈,每个数来了先放上去,直到看到运算符才动手。

遇到数:压栈
从左到右扫描,数字依次放上台面等着
遇到符:弹两个
先弹出的是右操作数,后弹出的是左操作数
算完:压回结果
把计算结果放回栈顶,继续扫描
扫完:栈顶即答案
合法表达式的最终结果恰好剩一个数
叠盘子对应 →栈求值

新盘子叠在最上面,取用时也先拿最上面的——后进先出对应弹栈顺序

3 4+2×  ⇒  3,4→push7→×2143\,4+2\times \;\Rightarrow\; 3,4 \xrightarrow{push} 7 \xrightarrow{\times 2} 14
5第 5 页 · 体验:用栈求值的直觉

手工求值:一步步演算

拿「3 4 + 2 *」手工演算,逐token扫描,看清栈的每一步变化。

1
遇数入栈
读到 3,是操作数,压入栈 → 栈:[3]
2
再入一个
读到 4,继续压栈 → 栈:[3, 4]
3
遇符弹出
读到 +,弹出 4 和 3(注意顺序)
4
算完回填
计算 3+4=7,结果压回栈 → [7]
5
重复执行
读到 2 入栈,再遇 *,弹 2 和 7,得 14 → 扫描结束,栈中唯一元素即答案
6第 6 页 · 手工求值:一步步演算

手工转换:中缀变后缀

从左到右扫中缀式,用栈暂存运算符,靠优先级决定何时出栈。

1
遇数直接输出
操作数不进栈,扫描到立即写进后缀结果
2
遇左括号压栈
左括号视作起点入栈,等右括号来配对清除
3
遇运算符先弹出
栈顶优先级≥当前时出栈,再压入当前运算符
4
遇右括号清栈
连续出栈到左括号,括号本身不输出
5
扫尾清空栈
扫描结束后逐个弹出剩余运算符
7第 7 页 · 手工转换:中缀变后缀

转换中栈的状态图解

这张图按『遇数输出、遇符入栈、出栈有条件』推进,一步步看 9-(2+3) 的转换过程。

图解渲染中…
a7:左括号入栈作屏障,栈内运算符不许越过它弹出a11:右括号触发连续弹栈,直到弹出左括号为止a13:表达式读完后弹空栈,得到最终后缀式
8第 8 页 · 转换中栈的状态图解

自测:后缀式求值

点击作答

用栈求值后缀式 6 2 3 + -,两个运算符都该先弹谁?正确结果是?

9第 9 页 · 自测:后缀式求值

求值算法完整流程

从左到右扫一遍,遇数压栈、遇符弹两个算完再压回,扫完栈顶即答案。

1
取下一个token
指针从左到右逐个读取,每轮只处理一个记号
2
判断类型
判断它是操作数还是运算符,决定走哪条分支
3
操作数压栈
是数字就直接入栈,等待将来被消费
4
弹两次栈
遇运算符弹栈两次,注意先弹的是右操作数
5
计算并压回
算出结果重新入栈,栈里元素少一个
6
扫描结束
全部读完后栈中只剩一个元素,即最终结果
10第 10 页 · 求值算法完整流程

算法代码落地

python

用栈实现后缀表达式求值,每行对应前面手工演算的一步。

代码高亮加载中…

数字入栈、遇运算符弹两个数算完再压回——正是手工演算时栈的变化。

11第 11 页 · 算法代码落地

边界与细节辨析

代码写完不等于万事大吉:求值遇上一元负号、转换时栈里堆多少元素、表达式本身不合法,这些边角才是面试与工程里最容易翻车的地方,逐个掰开看。

一元负号
「-3」没有左操作数,直接进栈当作 0-3 或标记处理
栈空间上限
最坏深度约等于操作数个数,可据此预留而非无限扩
非法检测
求值结束栈不剩一个元素、或中途弹栈失败,即表达式非法
次序不可交换
减法除法弹栈顺序不能反:先弹的是右操作数
收银台结账对应 →弹栈次序

先放上台的先结(左操作数),最后放的最先被拿走

12第 12 页 · 边界与细节辨析

收束要点

  • ✓中缀的麻烦在括号与优先级,后缀天然不需要
  • ✓求值与转换共用同一武器:栈暂存待配对的元素
  • ✓操作数入栈,运算符弹出两个数算完再压回
  • ✓转换时栈里存的是运算符,按优先级决定何时弹出
  • ✓细节决定正确性:单目运算、左结合、除零边界
延伸主题:递归表达式与语法树栈在函数调用中的应用广义的中缀求值器实现
13第 13 页 · 收束要点

课后思考

先自己琢磨,再对照参考答案——这些问题的价值在思考过程。

1为什么后缀表达式不需要括号?优先级信息去哪儿了?

参考答案提示:回想手工转换那页。括号和优先级在转换时已被编译进运算次序里——操作数顺序加符号位置就完整表达了结合的先后。

2编译器生成代码时,栈求值和寄存器分配如何衔接?

参考答案思考方向:出栈的两个操作数进入寄存器做运算,结果再压回(或留在寄存器)。连续运算可让结果驻留寄存器,减少访存——这正是现代编译器的优化点。

3如果把操作数换成带副作用的函数调用,栈求值还安全吗?

参考答案边界思考:求值顺序隐含在后缀式中,但副作用与运算顺序的约定因语言而异(如 Java、C 对参数求值顺序规定不同),语义需要额外定义。

14第 14 页 · 课后思考