前言
计算器功能实际简单,但是写代码来做其实一点也不简单.这里总结下文章 中使用的方法.
按照实现难度排序:
-
初级
支持+ -
操作, 输入表达式可以有空格, 整数相除结果取整. -
中级
初级的基础上,额外支持* /
操作,这时开始有了操作符优先级的概念 -
高级
中级的基础上额外再支持()
操作.这时开始解决括号内符号展开的问题. -
逆波兰表达式解法
对于计算器的问题有些方法使用的是双栈的方法,一个栈保存操作符,一个栈保存操作数, 这里使用的是单栈的方法, 不过为了方便记忆, 我更想称其为三指针法, 下面开始循序渐进的实现该功能.
初级(三指针方法)
方法是将表达式中每个数字分解出来放在一个栈中, 接着对栈里的数字进行累加,最后就能得到结果.
接下来不管是初级,中级还是高级方法,都是围绕这个目的进行的,想尽办法将数字放在栈里.
def basic_calculate(expression):
i = 0 # 当前字符编号
num_sign = '+' # num前面的符号
num = 0# 解析出来的数字
stk = []# 保存所有解析出来的num
while i < len(expression):
cc = expression[i]
# 如果当前字符是数字,则落入下面的if block中.
if cc.isdigit():
num = num * 10 + int(cc)
# 如果当前字符是操作符或者当前字符是最后一个字符,则触发填充栈操作,更新sign并初始化num.
if cc in ['-', '+'] or i == len(expression) - 1:
if num_sign == '-':
stk.append(-num)
elif num_sign == '+':
stk.append(num)
# 更新 num_sign和num
num_sign = cc
num = 0
i += 1
# 如果是空格,则不做任何处理,进入下一个循环.
print(f'stack:{stk}')
return sum(stk)
txt = '1-12+ 3'
result = basic_calculate(txt)
# stack:[1, -12, 3]
print(f'result:{result}')
assert result == -8
下面这张图可以更好的帮助理解, 可以简单认为它有三个指针,sign, num和i.
i为当前字符的编号,它从0开始不断增加到最后一个字符.
sign保存符号, 默认为+
如下图所示,当i指向’+'时, 此时stk顶部保存的是+1, sign为-, num为12, 因为i指向的是操作符,所以强行让stk[-1], sign和num进行相互作用(融合sign和num添加到stk中去, 这里的sign很重要,决定了num的去向)
中级(三指针)
中级的版本要解决*/
符号的问题,下列代码
def median_calculate(expression):
i = 0 # 当前字符编号
num = 0# num前面的符号
num_sign = '+'
stk = []
while i < len(expression):
cc = expression[i]
if cc.isdigit():
num = num * 10 + int(cc)
# 如果当前字符是操作符或者当前字符是最后一个字符,则触发填充栈操作,需要对val进行操作(push到栈里?还是直接替换栈的最后一个数字?)
if cc in ['-', '+', '*', '/'] or i == len(expression) - 1:
if num_sign == '-':
stk.append(-num)
elif num_sign == '+':
stk.append(num)
# 针对*/添加的判断
elif num_sign == '*':
stk[-1] *= num
elif num_sign == '/':
stk[-1] = int(stk[-1] / num)
num_sign = cc
num = 0
i += 1
print(f'stack:{stk}')
return sum(stk)
与初级版本相比,制作了下面两部分修改:
cc in ['-', '+']
改为cc in ['-', '+', '*', '/']
- 增加了对这两种符号的处理
# 针对*/添加的判断
elif sign == '*':
stk[-1] *= num
elif sign == '/':
stk[-1] /= num
如果num前面的sign为正负号,将正负号放在num前直接push到stk中即可,但是如果是*/, 则可以直接和stack顶端的数字进行操作.
为了方便接下来高级部分的介绍, 这里对中级计算器的代码做下简化:
def median_calculate_with_stack(expression):
num = 0# num前面的符号
num_sign = '+'
stk = []
while len(expression) > 0:
cc = expression.pop(0)
if cc.isdigit():
num = num * 10 + int(cc)
# 如果当前字符是操作符或者当前字符是最后一个字符,则触发填充栈操作,需要对val进行操作(push到栈里?还是直接替换栈的最后一个数字?)
if cc in ['-', '+', '*', '/'] or len(expression) == 0:
if num_sign == '-':
stk.append(-num)
elif num_sign == '+':
stk.append(num)
# 针对*/添加的判断
elif num_sign == '*':
stk[-1] *= num
elif num_sign == '/':
stk[-1] = int(stk[-1] / num)
num_sign = cc
num = 0
print(f'stack:{stk}')
return sum(stk)
txt = '3 * 4 - 5/2 - 6' # 12-2-6 = 4
result = median_calculate(txt)
result_with_stack = median_calculate_with_stack(list(txt))
print(f'result:{result}, {result_with_stack}')
assert result == 4 and result_with_stack ==4
这个版本去掉了i这个变量,将表达式从str转换为list.
高级
高级版本开始考虑括号了.
def expert_calculate(expression):
num_sign = '+'# num前面的符号
num = 0
stk = []
while len(expression) > 0:
cc = expression.pop(0)
if cc.isdigit():
num = num * 10 + int(cc)
if cc == '(':
num = expert_calculate(expression)
# 如果当前字符是操作符或者当前字符是最后一个字符,则触发填充栈操作,需要对val进行操作(push到栈里?还是直接替换栈的最后一个数字?)
if cc in ['-', '+', '*', '/', '(', ')'] or len(expression) == 0:
if num_sign == '-':
stk.append(-num)
elif num_sign == '+':
stk.append(num)
# 针对*/添加的判断
elif num_sign == '*':
stk[-1] *= num
elif num_sign == '/':
stk[-1] = int(stk[-1] / num)
num_sign = cc
num = 0
if cc == ')':
break
print(f'stack:{stk}')
return sum(stk)
txt = '3* (4-5/2) -6' # 3*2 - 6 = 0
result = expert_calculate(list(txt))
print(f'result:{result}')
assert result == 0
与median_calculate_with_stack
这个版本相比, 增加了
if cc == '(':
num = expert_calculate(expression)
和
if cc == ')':
break
每次遇到(
时则用递归方法解析剩余表达式内容, 遇到)
时,退出递归, 其本质是将一个大的父问题分割为子问题,子问题解决了,这个父问题也就解决了.
这里可以看出,将expression转换为list作为输入的好处是,在递归内部就可以直接删除已经解析过得字符, 递归返回时, expression剩下的就是未解析的字符,如果我们还用i的话, 这个calculate函数就要额外返回更多数据才行,不像这个stack版本那么简洁.
偷个图来说明下
逆波兰表达式解法
波兰表达式也就是前缀表达式,比如对于a+b*(c-d)+e/f
这种中缀表达式式子,波兰表达式为:+a+*b-cd/ef
, 逆波兰表达式为后缀表达式,对应的后缀表达式为:a b c d - * + e f / +
使用逆波兰表达式求解计算器问题有两个步骤:
- 将中缀表达式转换为后缀表达式
- 计算后缀表达式
细节参考文章:https://zhuanlan.zhihu.com/p/65110137:
代码实现
"""
计算器python实现, 支持四则运算,支持括号。
步骤:
1. 将字符串转换为逆波兰表达式(后缀表达式),该步骤会消除左右括号。
2. 计算逆波兰表达式,这也是整个过程中比较难的一步。
e.g. 对于输入(1+2*(4-3)+6/2), 它的后缀表达式为:[1, 2, 4, 3, '-', '*', '+', 6, 2, '/', '+'],
后缀表达式的好处是可以不用考虑括号的影响,按顺序从后缀列表中取出数字或者操作符就行。
语法树结构如下,从叶子节点开始计算,结果填充到父节点,继续递归计算。
+
+ /
1 * 6 2
2 -
4 3
"""
def make_post_exp(cmd):
#https://leetcode.cn/problems/basic-calculator-ii/discussion/
#// 中序转后序
#// 线性列表L,操作数栈S
#// 1.遇到数字:直接把数字按顺序加入列表L
#// 2.遇到左括号:直接压入操作数栈
#// 3.遇到操作数:如果当前操作数优先级 > 栈顶元素的优先级,或者栈为空或者栈顶元素是左括号,那么直接压入栈
#// 如果当前操作数优先级 <= 栈顶元素的优先级,则循环pop()栈顶操作数,加入列表L,直到可以加入当前操作数
#// 4.遇到右括号:一直pop栈的操作数,加入列表,直接遇到左括号,把左括号也pop了
#// 5.一遍遍历完后,把stack中剩余操作符,依次pop到L中
idx = 0
post_exp = [] # 保存后缀表达式
ops = [] # 保存符号: +-*/()
while idx < len(cmd):
ch = cmd[idx]
if ch == ' ':
idx += 1
continue
elif str.isdigit(ch):
next_idx = idx + 1
while str.isdigit(cmd[next_idx]):
next_idx += 1
num = int(cmd[idx:next_idx])
post_exp.append(num)
idx = next_idx
continue
elif ch == '(':
ops.append(ch)
idx += 1
continue
elif ch == ')':
# 弹出括号间的所有操作符
while ops[-1] != '(':
post_exp.append(ops.pop())
# pop '('
ops.pop()
idx += 1
else:
#case operand: + - * /
def degree(char):
# 判断运算符的优先级,乘除优先级大于+-
if char == '+' or char == '-':
return 1
else:
return 2
while len(ops) > 0 and \
ops[-1] != '(' and \
degree(ops[-1]) >= degree(ch):
post_exp.append(ops.pop(-1))
ops.append(ch)
idx += 1
while len(ops)> 0:
post_exp.append(ops.pop())
return post_exp
def eval_post_exp(post_exp):
num_stack = []
while len(post_exp) > 0:
if isinstance(post_exp[0], int):
num_stack.append(post_exp.pop(0))
else:
vala, valb = num_stack.pop(), num_stack.pop()
op = post_exp.pop(0)
if op == '+':
num_stack.append(vala + valb)
elif op == '-':
num_stack.append(valb - vala)
elif op == '*':
num_stack.append(vala * valb)
elif op == '/':
num_stack.append(valb / vala)
else:
assert False
assert len(num_stack) == 1
return num_stack.pop()
def run2():
cmd = '(1+2*(4-3)+6/2)'
post_exp = make_post_exp(cmd)
print(post_exp)
result = eval_post_exp(post_exp)
print(result)
run2()
总结
- i指向的字符触发前面的stk, sign和num更新状态.
- sign决定num和stk的融合方式,sign为*/时直接替换stk栈顶数字, sign为±时, num直接入栈即可.
- leetcode有可能超时,可以是用collection.deque, 双向队列对于push和pop的效率更高.
上面例子的源代码