简单四则运算器|python解法
题目分析
本题要求实现一个基本的计算器,计算包含数字、加法(+)、减法(-)、乘法(*)、除法(/)和括号(())的算式的结果。需要注意的是:
- 括号的优先级:括号中的内容应该首先计算。
- 运算符的优先级:乘法和除法的优先级高于加法和减法。
- 除法操作:除法结果必须取整(即整数除法)。
- 字符串输入无空格:字符串中的表达式中不包含任何空格。
解题思路
为了实现这个计算器,我们需要设计一个合适的数据结构来处理数字和运算符,并且能够正确处理运算符优先级和括号。
- 栈的应用:我们使用两个栈,一个用于存储数字,一个用于存储操作符。通过栈的方式,可以帮助我们实现中缀表达式到后缀表达式的转换,并且能够正确处理运算符的优先级和括号。
- 优先级控制:利用优先级字典来确保运算顺序,乘法和除法的优先级比加法和减法高。
- 操作符处理:每次遇到操作符时,我们需要判断栈顶的操作符的优先级,如果当前操作符的优先级较高或相等,则继续从栈中弹出数字进行计算。
- 括号处理:遇到左括号时,我们将其压入操作符栈,遇到右括号时,进行括号内的运算,直到遇到左括号。
- 数字处理:当遇到数字时,我们将其处理并压入数字栈。
关键知识点
- 栈:用来存储数字和运算符,确保运算顺序符合中缀表达式的规则。
- 运算符优先级:需要确保在正确的顺序中执行加法、减法、乘法和除法。
- 括号的处理:括号优先于其他运算符,因此需要特殊处理。
基本思路
- 使用两个栈,
num_stack存储数字,operator_stack存储运算符。 - 遍历输入字符串,遇到数字时,将其解析为整数并压入
num_stack。 - 遇到操作符时,判断栈顶的操作符是否具有更高的优先级,如果有,则执行运算,直到栈顶操作符的优先级更低。
- 遇到左括号时,直接压入
operator_stack,遇到右括号时,执行直到遇到左括号的操作。 - 在遍历完成后,执行所有剩余的操作符,直到栈为空。
代码实现
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)的过程。状态机是解决这类表达式解析问题的另一种常用思路,尤其在实现编译器或解释器时,状态机和正则表达式解析器被广泛使用。
状态机与这道题的关联
我们可以将表达式求值的问题看作是在一系列不同的状态之间转换。每个状态对应于解析字符串时可能遇到的情况,如读取数字、读取操作符、处理左括号或右括号等。通过状态的转换,我们能够按照输入表达式的字符逐步解析,并根据不同的状态执行相应的操作。
主要状态设计
根据题目要求,我们可以设计如下几个核心状态:
- 开始状态(START) :初始状态,用于识别第一个字符,可以是数字、左括号或操作符。
- 读取数字状态(READ_NUMBER) :读取并累积完整的数字,直到遇到操作符或括号为止。
- 读取操作符状态(READ_OPERATOR) :识别操作符
+、-、*、/,并根据优先级决定是继续解析还是执行计算。 - 处理左括号状态(LEFT_PARENTHESIS) :遇到左括号时,进入括号内的子表达式解析。
- 处理右括号状态(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:
- 弹出操作符栈,直到遇到左括号。
- 返回上一层表达式解析。
用状态机实现的优势
- 逻辑清晰:通过状态转换图,我们可以明确地看到各个状态之间的转换关系,这使得程序逻辑更加清晰。
- 易于扩展:状态机的设计允许我们轻松地增加新功能或支持更多的操作符。例如,如果需要支持指数运算符
^或函数调用(如sin、cos等),我们只需增加新的状态并定义相应的转换规则。 - 错误处理:状态机可以帮助我们轻松检测语法错误。例如,如果在
READ_OPERATOR状态时遇到连续的两个操作符,或在RIGHT_PARENTHESIS状态时找不到匹配的左括号,这些都可以作为非法输入进行处理。
与当前代码的对比
当前代码使用了栈来处理运算符和数字,并通过优先级控制来决定何时执行计算,这是一种手动管理状态的做法。实际上,栈的使用隐含了状态的转换:
- 数字压入栈,相当于从
READ_NUMBER状态转换到READ_OPERATOR状态。 - 操作符压入栈,或者弹出计算,相当于从
READ_OPERATOR状态转换回READ_NUMBER状态。 - 左括号和右括号的处理,实际上是在状态之间的跳转和切换。
我们可以将栈视为一种状态管理工具,而当前的逻辑可以看作是对状态机的隐式实现。
有限状态机扩展
通过状态机的设计思路,我们可以看到这道题不仅仅是简单的栈操作,还涉及到状态转换的过程。在更复杂的表达式求值或解释器实现中,状态机常常用于:
- 编译器的词法分析和语法分析:识别和解析编程语言中的各种语法结构。
- 正则表达式引擎:通过状态机匹配模式和输入字符串。
- 表达式求值:不仅限于基本的加减乘除,还可以支持更多的数学运算和函数调用。
本题中的状态机设计思路,为未来扩展到更复杂的解析器或计算器提供了理论基础。
总结
本题通过使用栈来管理数字和运算符,并且合理处理括号和运算符的优先级,成功实现了一个基本的计算器。这种方法可以扩展和适应更复杂的表达式求值问题。