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

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 "))
相关推荐
编程重生之路14 天前
今年2024的1024文章
ai·ai编程·编译器·cursor·1024程序员节
Thanks_ks15 天前
【第五章·选择控制结构】第一节:生活中与计算机中的问题求解方法
算法·编译器·机器语言·分治策略·c 语言程序设计·计算机程序·程序设计语言
Eloudy22 天前
函数地址对齐 __attribute__((aligned(64))) 编译器选项 -falign-functions=4
算法·编译器
Trouvaille ~2 个月前
【C++篇】C++类与对象深度解析(六):全面剖析拷贝省略、RVO、NRVO优化策略
c++·c++20·编译原理·编译器·类和对象·rvo·nrvo
无名之逆2 个月前
B树和B+树
javascript·数据结构·b树·算法·编译原理·期末速成
嚯呀怪怪怪2 个月前
从零基础学Go(九)——Go的Goroutine
golang·线程·多线程·并发·编译原理·协程·gorountine
EleganceJiaBao3 个月前
【Story】编译器的基础概念与类型分类
java·c语言·c++·python·gnu·编译器·gcc
怜渠客3 个月前
VS2022快速搭建OLLVM
c++·ide·编译器
张一西3 个月前
ARM学习(31)编译器对overlay方式的支持
arm开发·编译器·overlay·动态加载·bank·armcc·armclang
harykali4 个月前
静态分析学习笔记02:程序中间表示(IR)
编译器