问题描述
表达式求值要解决的问题一般是输入一个字符串表示的表达式,要求输出它的值。当然也有变种比如表达式中是否包含括号,指数运算,含多少变量,判断多个表达式是否等价,等等。
表达式一般需要先进行语法分析(grammer parsing)再求值,也可以边分析边求值,语法分析的作用是检查输入的字符串是否是一个合法的表达式,一般使用语法分析器(parser)解决。
表达式包含两类字符:运算数和运算符。对于长度为n的表达式,借助合适的分析方法,可以在 $O(n)$的时间复杂度内完成分析与求值。
表达式树与逆波兰表达式
一种递归分析表达式的方法是,将表达式当成普通的语法规则进行分析,分析后拆分成如图所示的
表达式树,然后在树结构上自底向上进行运算。
表达式树上进行 树的遍历 可以得到不同类型的表达式。算术表达式分为三种,分别是前缀表达式、中缀表达式、后缀表达式。中缀表达式是日常生活中最常用的表达式;后缀表达式是计算机容易理解的表达式。
前序遍历对应前缀表达式(波兰式) 中序遍历对应中缀表达式 后序遍历对应后缀表达式(逆波兰式) 逆波兰表达式(后缀表达式)是书写数学表达式的一种形式,其中运算符位于其操作数之后。例如,以下表达式:
$$a+b_c_d+(e-f)_(g_h+i)$$
可以用逆波兰表达式书写:
$$abc_d_+ef-gh_i+_+$$
因此,逆波兰表达式与表达式树一一对应。逆波兰表达式不需要括号表示,它的运算顺序是唯一确定的。
逆波兰表达式的方便之处在于很容易在线性时间内计算。举个例子:在逆波兰表达式32*1- 中,首先计算 3*2=6(使用最后一个运算符,即栈顶运算符),然后计算6-1=5 。可以看到:对于一个逆波兰表达式,只需要 维护一个数字栈,每次遇到一个运算符,就取出两个栈顶元素,将运算结果重新压入栈中。最后,栈中唯一一个元素就是该逆波兰表达式的运算结果。该算法拥有O(n) 的时间复杂度。
只含左结合的二元运算符的含括号表达式
考虑简化的问题。假设所有运算符都是二元的:所有运算符都有两个参数。并且所有运算符都是左结合的:如果运算符的优先级相等,则从左到右执行。允许使用括号。
对于这种类型的中缀表达式的计算,可以将其转化为后缀表达式再进行计算。定义两个 栈 来分别存储运算符和运算数,每当遇到一个数直接放进运算数栈。每个运算符块对应于一对括号,运算符栈只对于运算符块的内部单调。每当遇到一个操作符时,要查找运算符栈中最顶部运算符块中的元素,在运算符块的内部保持运算符按照优先级降序进行适当的弹出操作,弹出的同时求出对应的子表达式的值。
以下部分用「输出」表示输出到后缀表达式,即将该数字放在运算数栈上,或者弹出运算符和两个操作数,运算后再将结果压回运算数栈上。从左到右扫描该中缀表达式:
- 如果遇到数字,直接输出该数字。
- 如果遇到左括号,那么将其放在运算符栈上。
- 如果遇到右括号,不断输出栈顶元素,直至遇到左括号,左括号出栈。换句话说,执行一对括号内的所有运算符。
- 如果遇到其他运算符,不断输出所有运算优先级大于等于当前运算符的运算符。最后,新的运算符入运算符栈。
- 在处理完整个字符串之后,一些运算符可能仍然在堆栈中,因此把栈中剩下的符号依次输出,表达式转换结束。
Code
1 | std::string convert(const std::string &s) { |
本文作者:jujimeizuo
本文地址: https://blog.jujimeizuo.cn/2022/12/06/expression/
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0 协议。转载请注明出处!