编译原理极简入门:表达式求值

leetcode: 224. 基本计算器

leetcode: 227. 基本计算器 II

leetcode: 772. 基本计算器 III

本算法基于编译原理实现,可作为以上三个 leetcode 的通解。同时,稍作改动即可实现:产生汇编指令、后缀表达式、AST 语法树等。

问题

一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。

  • 1 <= s.length <= 3 * 105
  • s 由数字、'+''-''*''/''('')'、和 ' ' 组成
  • s 表示一个有效的表达式
  • '+' 不能用作一元运算(例如, "+1" 和 "+(2 + 3)" 无效)
  • '-' 可以用作一元运算(即 "-1" 和 "-(2 + 3)" 是有效的)
  • 输入中不存在两个连续的操作符
  • 每个数字和运行的计算将适合于一个有符号的 32位 整数

测试用例:

input output
2+3*4 14
(2+3)*4 20
-(2 + 3)*4 -20

思路

表达式求值属于编译器的一部分,我们就根据编译原理的方法实现。因为一个编译器的复杂度是单纯表达式求值的成百上千倍,所以这种方式实现的代码,具有更好的可读性和扩展性。

解题方法

编译原理简介

编译原理是计算机科学里面最接近科学的部分,整个编译过程都是一个不断抽象的过程。词法分析是把一个个的字符组织成单词;语法分析是把一个个单词组织成语句;整个编译过程又是对语句的抽象。

这种逐层抽象的方法是编程思维里面最重要的部分,不懂抽象的程序员很容易遇到发展瓶颈。

词法分析器 tokenizer

词法分析就是把一个个字符组织成单词,众所周不知的正则表达式正是为了解决这一问题而发明的。词法分析产生的结果我们叫做 token。

在完整的编译器中词法分析非常复杂,不仅因为token 的类型很多,还有许多状态需要管理。比如在表达式中遇到数字就可以直接提取为 token,但是在一个完整的编译器里面,遇到数字可能是在字符串里面,你必须把整个字符串作为一个 token。或者是代码在注释里面,你必须把整个注释作为一个 token。

在完整的编译器里面会用二元组来表示token,一个是 token的类型,一个是 token 具体的值。比如<数字, "123">、<字符串, "123">、<RETURN, "return">、<PLUS, "+">......

但是在表达式求值中,我们可以遇到的token只有 3 种:数字,运算符,终止符。而运算符和终止符也很简单,都是单字符的,因此不需要复杂的状态管理和二元组表示,只要把数字和其他字符一个个分开即可

语法分析器 parser

语法分析就是实现token的"正则表达式"。有一个更专业的名词叫巴科斯范式(英语:Backus Normal Form,缩写为 BNF)。

BNF 复制代码
expression => term [ + term | - term ] ...
term => factor [ * factor | / factor ] ...
factor => NUM | ( expression ) | -factor

这里定义了三种BNF,expression(表达式),term(项),factor(因子)。我们在代码里面可以直接根据这三个范式来实现。

  • expression 被定义为 term 的连续加减法。
  • term 被定义为 factor 的连续乘除法。
  • factor 有三种:数字,括号表达式,负号一元运算符。

可以看到下面的代码中实现了这三个同名的函数,每个函数返回两个值i和a,i表示编译器当前的扫描位置,a表示当前函数的计算结果。

在本题的官方题解中,总是少不了提到两个栈,以及运算符的优先级处理。而在 BNF 中,已经隐含实现了栈和优先级,比如 expression 只有加减法,乘除法全部在 term 中实现,也就表明在计算加减法之前就要计算好 term,而栈也是用函数间的调用来隐含实现了。这是这种算法更清晰易读的原因。

复杂度

时间复杂度:

<math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n)

Code

Python 复制代码
class Solution:

    def tokenizer(self, expr: str):
        """
        词法分析
        """
        expr = expr.replace(" ", "")
        tokens = []
        for c in expr:
            if c.isdigit():
                if tokens and tokens[-1].isdigit():
                    tokens[-1] += c
                else:
                    tokens.append(c)
            else:
                tokens.append(c)
        return tokens 
    
    def parser(self, tokens):
        """
        语法分析: BNF表达式
        expression => term [ + term | - term ] ...
        term => factor [ * factor | / factor ] ...
        factor => NUM | ( expression )
        """
        # 实现计算单元
        def cpu(a, op , b):
            if op == "+":
                return a+b
            elif op == "-":
                return a-b
            elif op == "*":
                return a*b
            elif op == "/":
                return a//b
        # 以下实现 BNF 表达式的语法分析
        def expression(i):
            i, a = term(i)
            while(tokens[i] in ["+","-"]):
                op = tokens[i]
                i += 1
                i, b = term(i)
                a = cpu(a, op , b)
            return i, a
        def term(i):
            i, a = factor(i)
            while(tokens[i] in ["*","/"]):
                op = tokens[i]
                i += 1
                i, b = factor(i)
                a = cpu(a, op , b)
            return i, a
                
        def factor(i):
            if tokens[i].isdigit():
                i += 1
                return i, int(tokens[i])
            elif tokens[i] == "-":
                i += 1
                i, a = factor(i)
                return i, -1*a
            elif tokens[i] == "(":
                i += 1
                i, a = expression(i)
                if tokens[i] == ")":
                    i += 1
                    return i, a
                else:
                    raise Exception("SyntaxError: near the '%s'"%(tokens[i]))
            else:
                raise Exception("SyntaxError: near the '%s'"%(tokens[i]))
        
        # 表达式解析开始
        i,res = expression(0)
        if tokens[i] != ";":
            raise Exception("SyntaxError: incorrect ending near '%s'"%(tokens[i]))
        return res

    def calculate(self, expr: str) -> int:
        tokens = self.tokenizer(expr+";")
        return self.parser(tokens)

s = Solution()
print(s.calculate("2+3*4"))
print(s.calculate("(2+3)/4"))
print(s.calculate("14/3*2"))
print(s.calculate("0"))
print(s.calculate("1-(     -2)"))
print(s.calculate("1 + 1"))
print(s.calculate(" 2-1 + 2 "))
相关推荐
Beginner x_u2 天前
编译原理 第二章下: 推导,规约,句型句子,语言,文法分类,二义性
编译原理·语言·推导·文法
codists5 天前
《编译原理》阅读笔记:p19-p24
编译原理
Beginner x_u7 天前
编译原理 第一章: 编译概述 重点总结(翻译程序的过程,解释程序,编译程序的结构)
编译原理·解释程序·翻译程序·编译概述
Beginner x_u7 天前
编译原理 第三章上 :词法分析 状态图的画法与检验
编译原理·词法分析·状态图
flysnow0108 天前
多版本GCC安装及切换
编译器·gcc·切换版本
Florian10 天前
仓颉语言HelloWorld内测【仅需三步】
华为·编译器·仓颉·方舟
codists10 天前
《编译原理》阅读笔记:p18
编译原理
岁岁岁平安11 天前
编译原理大题自解(活前缀DFA、LR(0)分析表)
笔记·编译原理
Eloudy11 天前
hipcc 编译 amd gpu kernel 和 打包与解包的流程实验
编译器·llvm·gpu-arch
Eloudy1 个月前
^^ c++ 当然就是整个计算机科学
c++·算法·编译器·llvm·二进制分析