青训营X豆包 MarsCode AI|豆包MarsCode AI刷题

114 阅读8分钟

简单四则运算器|python解法

题目分析

本题要求实现一个基本的计算器,计算包含数字、加法(+)、减法(-)、乘法(*)、除法(/)和括号(())的算式的结果。需要注意的是:

  • 括号的优先级:括号中的内容应该首先计算。
  • 运算符的优先级:乘法和除法的优先级高于加法和减法。
  • 除法操作:除法结果必须取整(即整数除法)。
  • 字符串输入无空格:字符串中的表达式中不包含任何空格。

解题思路

为了实现这个计算器,我们需要设计一个合适的数据结构来处理数字和运算符,并且能够正确处理运算符优先级和括号。

  1. 栈的应用:我们使用两个栈,一个用于存储数字,一个用于存储操作符。通过栈的方式,可以帮助我们实现中缀表达式到后缀表达式的转换,并且能够正确处理运算符的优先级和括号。
  2. 优先级控制:利用优先级字典来确保运算顺序,乘法和除法的优先级比加法和减法高。
  3. 操作符处理:每次遇到操作符时,我们需要判断栈顶的操作符的优先级,如果当前操作符的优先级较高或相等,则继续从栈中弹出数字进行计算。
  4. 括号处理:遇到左括号时,我们将其压入操作符栈,遇到右括号时,进行括号内的运算,直到遇到左括号。
  5. 数字处理:当遇到数字时,我们将其处理并压入数字栈。

关键知识点

  • :用来存储数字和运算符,确保运算顺序符合中缀表达式的规则。
  • 运算符优先级:需要确保在正确的顺序中执行加法、减法、乘法和除法。
  • 括号的处理:括号优先于其他运算符,因此需要特殊处理。

基本思路

  1. 使用两个栈,num_stack 存储数字,operator_stack 存储运算符。
  2. 遍历输入字符串,遇到数字时,将其解析为整数并压入 num_stack
  3. 遇到操作符时,判断栈顶的操作符是否具有更高的优先级,如果有,则执行运算,直到栈顶操作符的优先级更低。
  4. 遇到左括号时,直接压入 operator_stack,遇到右括号时,执行直到遇到左括号的操作。
  5. 在遍历完成后,执行所有剩余的操作符,直到栈为空。

代码实现

def evaluate(num1, num2, operator):
    if operator == '+':
        return num1 + num2
    elif operator == '-':
        return num1 - num2
    elif operator == '*':
        return num1 * num2
    elif operator == '/':
        return num1 // num2  # 整数除法

def solution(expression):
    s = expression
    num_stack = []
    operator_stack = []
    precedence = {'+': 1, '-': 1, '*': 2, '/': 2, '(': 0}
    n = len(s)
    i = 0

    while i < n:
        char = s[i]

        # 处理数字
        if char.isdigit():
            num = 0
            while i < n and s[i].isdigit():
                num = num * 10 + int(s[i])
                i += 1
            num_stack.append(num)
            i -= 1

        # 处理左括号
        elif char == '(':
            operator_stack.append(char)

        # 处理右括号
        elif char == ')':
            while operator_stack and operator_stack[-1] != '(':
                if len(num_stack) < 2:
                    raise ValueError("Not enough operands in stack")
                operator = operator_stack.pop()
                num2 = num_stack.pop()
                num1 = num_stack.pop()
                num_stack.append(evaluate(num1, num2, operator))
            operator_stack.pop()  # 弹出 '('

        # 处理操作符
        elif char in precedence:
            while (operator_stack and operator_stack[-1] != '(' and
                   precedence[char] <= precedence[operator_stack[-1]]):
                if len(num_stack) < 2:
                    raise ValueError("Not enough operands in stack")
                operator = operator_stack.pop()
                num2 = num_stack.pop()
                num1 = num_stack.pop()
                num_stack.append(evaluate(num1, num2, operator))
            operator_stack.append(char)

        i += 1

    # 处理剩余操作符
    while operator_stack:
        if len(num_stack) < 2:
            raise ValueError("Not enough operands in stack")
        operator = operator_stack.pop()
        num2 = num_stack.pop()
        num1 = num_stack.pop()
        num_stack.append(evaluate(num1, num2, operator))

    return num_stack.pop()

# 测试用例
print(solution("1+1"))               # 输出 2
print(solution("3+4*5/(3+2)"))       # 输出 7
print(solution("4+2*5-2/1"))         # 输出 12
print(solution("(1+(4+5+2)-3)+(6+8)"))  # 输出 23
print(solution("2*(5+5*2)/3+(6+8*3)"))  # 输出 40

复杂度分析

  • 时间复杂度O(n),其中 n 是表达式的长度。我们只遍历一次字符串,并对每个字符执行常数时间的操作。
  • 空间复杂度O(n),我们需要存储数字栈和运算符栈,最多存储 n 个元素。

扩展适用场景

这种方法可以扩展到更复杂的数学表达式解析与计算中。它不依赖于任何内置的 eval 函数,通过栈的方式手动计算表达式,可以灵活地扩展到更复杂的表达式(如支持更多运算符或功能)。

相关知识点

有限状态机

在解析和求值字符串表达式时,使用栈是一种常见的方法。然而,如果我们从状态机的角度来看待这个问题,会发现这道题也可以抽象成一个有限状态自动机(Finite State Machine, FSM)的过程。状态机是解决这类表达式解析问题的另一种常用思路,尤其在实现编译器或解释器时,状态机和正则表达式解析器被广泛使用。

状态机与这道题的关联

我们可以将表达式求值的问题看作是在一系列不同的状态之间转换。每个状态对应于解析字符串时可能遇到的情况,如读取数字、读取操作符、处理左括号或右括号等。通过状态的转换,我们能够按照输入表达式的字符逐步解析,并根据不同的状态执行相应的操作。

主要状态设计

根据题目要求,我们可以设计如下几个核心状态:

  1. 开始状态(START) :初始状态,用于识别第一个字符,可以是数字、左括号或操作符。
  2. 读取数字状态(READ_NUMBER) :读取并累积完整的数字,直到遇到操作符或括号为止。
  3. 读取操作符状态(READ_OPERATOR) :识别操作符 +-*/,并根据优先级决定是继续解析还是执行计算。
  4. 处理左括号状态(LEFT_PARENTHESIS) :遇到左括号时,进入括号内的子表达式解析。
  5. 处理右括号状态(RIGHT_PARENTHESIS) :遇到右括号时,结束当前子表达式的解析,并返回上一层表达式继续处理。

状态转换图

START --> READ_NUMBER --> READ_OPERATOR --> READ_NUMBER
    |           |              ^              |
    |           v              |              v
    |--> LEFT_PARENTHESIS --> READ_OPERATOR --> RIGHT_PARENTHESIS

状态转换规则

  • START

    • 遇到数字:进入 READ_NUMBER 状态。
    • 遇到左括号:进入 LEFT_PARENTHESIS 状态,压入操作符栈。
  • READ_NUMBER

    • 遇到数字:继续累积数字。
    • 遇到操作符:进入 READ_OPERATOR 状态,将数字压入数字栈。
    • 遇到右括号:进入 RIGHT_PARENTHESIS 状态。
  • READ_OPERATOR

    • 遇到数字:进入 READ_NUMBER 状态。
    • 遇到左括号:进入 LEFT_PARENTHESIS 状态。
  • LEFT_PARENTHESIS

    • 遇到数字:进入 READ_NUMBER 状态。
    • 遇到左括号:继续压入左括号。
  • RIGHT_PARENTHESIS

    • 弹出操作符栈,直到遇到左括号。
    • 返回上一层表达式解析。

用状态机实现的优势

  1. 逻辑清晰:通过状态转换图,我们可以明确地看到各个状态之间的转换关系,这使得程序逻辑更加清晰。
  2. 易于扩展:状态机的设计允许我们轻松地增加新功能或支持更多的操作符。例如,如果需要支持指数运算符 ^ 或函数调用(如 sincos 等),我们只需增加新的状态并定义相应的转换规则。
  3. 错误处理:状态机可以帮助我们轻松检测语法错误。例如,如果在 READ_OPERATOR 状态时遇到连续的两个操作符,或在 RIGHT_PARENTHESIS 状态时找不到匹配的左括号,这些都可以作为非法输入进行处理。

与当前代码的对比

当前代码使用了栈来处理运算符和数字,并通过优先级控制来决定何时执行计算,这是一种手动管理状态的做法。实际上,栈的使用隐含了状态的转换:

  • 数字压入栈,相当于从 READ_NUMBER 状态转换到 READ_OPERATOR 状态。
  • 操作符压入栈,或者弹出计算,相当于从 READ_OPERATOR 状态转换回 READ_NUMBER 状态。
  • 左括号和右括号的处理,实际上是在状态之间的跳转和切换。

我们可以将栈视为一种状态管理工具,而当前的逻辑可以看作是对状态机的隐式实现。

有限状态机扩展

通过状态机的设计思路,我们可以看到这道题不仅仅是简单的栈操作,还涉及到状态转换的过程。在更复杂的表达式求值或解释器实现中,状态机常常用于:

  1. 编译器的词法分析和语法分析:识别和解析编程语言中的各种语法结构。
  2. 正则表达式引擎:通过状态机匹配模式和输入字符串。
  3. 表达式求值:不仅限于基本的加减乘除,还可以支持更多的数学运算和函数调用。

本题中的状态机设计思路,为未来扩展到更复杂的解析器或计算器提供了理论基础。

总结

本题通过使用栈来管理数字和运算符,并且合理处理括号和运算符的优先级,成功实现了一个基本的计算器。这种方法可以扩展和适应更复杂的表达式求值问题。