强大的计算器

前言

计算器功能实际简单,但是写代码来做其实一点也不简单.这里总结下文章 中使用的方法.
按照实现难度排序:

  • 初级
    支持+ - 操作, 输入表达式可以有空格, 整数相除结果取整.

  • 中级
    初级的基础上,额外支持* /操作,这时开始有了操作符优先级的概念

  • 高级
    中级的基础上额外再支持()操作.这时开始解决括号内符号展开的问题.

  • 逆波兰表达式解法

对于计算器的问题有些方法使用的是双栈的方法,一个栈保存操作符,一个栈保存操作数, 这里使用的是单栈的方法, 不过为了方便记忆, 我更想称其为三指针法, 下面开始循序渐进的实现该功能.

初级(三指针方法)

方法是将表达式中每个数字分解出来放在一个栈中, 接着对栈里的数字进行累加,最后就能得到结果.
接下来不管是初级,中级还是高级方法,都是围绕这个目的进行的,想尽办法将数字放在栈里.

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)

与初级版本相比,制作了下面两部分修改:

  1. cc in ['-', '+'] 改为cc in ['-', '+', '*', '/']
  2. 增加了对这两种符号的处理
 # 针对*/添加的判断
 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 / +
使用逆波兰表达式求解计算器问题有两个步骤:

  1. 将中缀表达式转换为后缀表达式
  2. 计算后缀表达式
    细节参考文章: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()

总结

  1. i指向的字符触发前面的stk, sign和num更新状态.
  2. sign决定num和stk的融合方式,sign为*/时直接替换stk栈顶数字, sign为±时, num直接入栈即可.
  3. leetcode有可能超时,可以是用collection.deque, 双向队列对于push和pop的效率更高.

上面例子的源代码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值