一、题目描述
请用 python3 编写一个计算器的控制台程序,支持加减乘除、乘方、括号、小数点,运算符优先级为括号>乘方>乘除>加减,同级别运算按照从左向右的顺序计算。
二、输入描述
- 数字包括"0123456789",小数点为".",运算符包括:加("+")、减("-")、乘("*")、除("/")、乘方("^",注:不是**!)、括号("()")
- 需要从命令行参数读入输入,例如提交文件为 main.py,可以用 python3 main.py "1+2-3+4" 的方式进行调用
- 输入需要支持空格,即 python3 main.py "1 + 2 - 3 + 4" 也需要程序能够正确给出结果
- 所有测试用例中参与运算的非零运算数的绝对值范围保证在 10^9-10^(-10) 之内, 应该输出运算结果时非零运算结果绝对值也保证在该范围内
三、输出描述
- 数字需要支持小数点,输出结果取10位有效数字,有效数字位数不足时不能补0
- 对于不在输入描述内的输入,输出INPUT ERROR
- 对于格式不合法(例如括号不匹配等)的输入,输出 FORMAT ERROR
- 对于不符合运算符接收的参数范围(例如除0等)的输入,输出VALUE ERROR
- 对于2、3、4的情况,输出即可,不能抛出异常
- 同时满足2、3、4中多个条件时,以序号小的为准
四、样例
输入: 1 + 2 - 3 + 4
输出: 4
输入: 1 + 2 - 3 + 1 / 3
输出: 0.3333333333
输入: 1 + + 2
输出: FORMAT ERROR
输入: 1 / 0
输出: VALUE ERROR
输入: a + 1
输出: INPUT ERROR
【注:此题为TsinghuaX:34100325X 《软件工程》 MOOC 课程 Spring, 2016 Chapter 1 Problem,此文发布时,这门课刚刚在 “学堂在线” 上开课两天】
用 Python3 实现,初看一下,首先想到的其实是一种“讨巧”(作弊 >_<)的方法(由于曾经网站被挂码的悲壮历史……),即通过 eval() 函数(这应该是黑客用得最多的一个函数了吧+_+)直接求解表达式,谁叫题目指定用 Python 这种方便的脚本语言呢~
大致代码区区几行:
1 from sys import argv 2 3 if __name__ == "__main__": 4 exp = argv[1] 5 print(eval(exp))
即便是考虑到题干中的输出要求做异常处理(try...except)输出相应的错误信息,也就十行左右。
但稍深入就会发现,有些情形还是无法按要求处理的,比如样例 “1 + + 2”,用 eval() 会输出结果 “3” (+2 前的 “+” 被当作正号),而题目要求输出 “FORMAT ERROR”。
没办法,只能老老实实做苦力活儿了。
表达式求值其实是《数据结构》课程里一个基本且重要的问题之一,一般作为 “栈” 的应用来提出。
问题的关键就是需要按照人们通常理解的运算符的优先级来进行计算,而在计算过程中的临时结果则用 栈 来存储。
为此,我们可以首先构造一个 “表” 来存储当不同的运算符 “相遇” 时,它们谁更 “屌” 一些(优先级更高一些)。这样就可以告诉计算机,面对不同的情形,它接下来应该如何来处理。
其次,我们需要构造两个栈,一个运算符栈,一个运算数栈。
运算符栈是为了搞定当某个运算符优先级较低时,暂时先让它呆在栈的底部位置,待它可以 “重见天日” 的那一天(优先级相对较高时),再把它拿出来使用。正确计算完成后,此栈应为空。
运算数栈则是为了按合理的计算顺序存储运算中间结果。正确计算完成后,此栈应只剩下一个数,即为最后的结果。
完整的代码如下:
1 # -*- coding: utf-8 -*- 2 3 ################################# 4 # @Author: Maples7 5 # @LaunchTime: 2016/2/24 12:32:38 6 # @FileName: main 7 # @Email: maples7@163.com 8 # @Function: 9 # 10 # A Python Calculator for Operator +-*/()^ 11 # 12 ################################# 13 14 from sys import argv 15 from decimal import * 16 17 def delBlank(str): 18 """ 19 Delete all blanks in the str 20 """ 21 ans = "" 22 for e in str: 23 if e != " ": 24 ans += e 25 return ans 26 27 def precede(a, b): 28 """ 29 Compare the prior of operator a and b 30 """ 31 # the prior of operator 32 prior = ( 33 # '+' '-' '*' '/' '(' ')' '^' '#' 34 ('>', '>', '<', '<', '<', '>', '<', '>'), # '+' 35 ('>', '>', '<', '<', '<', '>', '<', '>'), # '-' 36 ('>', '>', '>', '>', '<', '>', '<', '>'), # '*' 37 ('>', '>', '>', '>', '<', '>', '<', '>'), # '/' 38 ('<', '<', '<', '<', '<', '=', '<', ' '), # '(' 39 ('>', '>', '>', '>', ' ', '>', '>', '>'), # ')' 40 ('>', '>', '>', '>', '<', '>', '>', '>'), # '^' 41 ('<', '<', '<', '<', '<', ' ', '<', '=') # '#' 42 ) 43 44 # operator to index of prior[8][8] 45 char2num = { 46 '+': 0, 47 '-': 1, 48 '*': 2, 49 '/': 3, 50 '(': 4, 51 ')': 5, 52 '^': 6, 53 '#': 7 54 } 55 56 return prior[char2num[a]][char2num[b]] 57 58 def operate(a, b, operator): 59 """ 60 Operate [a operator b] 61 """ 62 if operator == '+': 63 ans = a + b 64 elif operator == '-': 65 ans = a - b 66 elif operator == '*': 67 ans = a * b 68 elif operator == '/': 69 if b == 0: 70 ans = "VALUE ERROR" 71 else: 72 ans = a / b 73 elif operator == '^': 74 if a == 0 and b == 0: 75 ans = "VALUE ERROR" 76 else: 77 ans = a ** b 78 79 return ans 80 81 def calc(exp): 82 """ 83 Calculate the ans of exp 84 """ 85 exp += '#' 86 operSet = "+-*/^()#" 87 stackOfOperator, stackOfNum = ['#'], [] 88 pos, ans, index, length = 0, 0, 0, len(exp) 89 while index < length: 90 e = exp[index] 91 if e in operSet: 92 # calc according to the prior 93 topOperator = stackOfOperator.pop() 94 compare = precede(topOperator, e) 95 if compare == '>': 96 try: 97 b = stackOfNum.pop() 98 a = stackOfNum.pop() 99 except: 100 return "FORMAT ERROR" 101 ans = operate(a, b, topOperator) 102 if ans == "VALUE ERROR": 103 return ans 104 else: 105 stackOfNum.append(ans) 106 elif compare == '<': 107 stackOfOperator.append(topOperator) 108 stackOfOperator.append(e) 109 index += 1 110 elif compare == '=': 111 index += 1 112 elif compare == ' ': 113 return "FORMAT ERROR" 114 else: 115 # get the next num 116 pos = index 117 while not exp[index] in operSet: 118 index += 1 119 temp = exp[pos:index] 120 121 # delete all 0 of float in the end 122 last = index - 1 123 if '.' in temp: 124 while exp[last] == '0': 125 last -= 1 126 temp = exp[pos:last + 1] 127 128 try: 129 temp = Decimal(temp) 130 except: 131 return "INPUT ERROR" 132 stackOfNum.append(temp) 133 134 if len(stackOfNum) == 1 and stackOfOperator == []: 135 return stackOfNum.pop() 136 else: 137 return "INPUT ERROR" 138 139 if __name__ == "__main__": 140 # get the exp 141 exp = argv[1] 142 143 # set the precision 144 getcontext().prec = 10 145 146 # delete blanks 147 exp = delBlank(exp) 148 149 # calc and print the ans 150 ans = calc(exp) 151 print(ans)
其中需要稍微注意的细节有:
1. 表达式处理前,前后都插入一个 '#' 作为一个特殊的运算符,这样做是为了方便统一处理,即不用再去特别判断表达式是否已经结束(从而引发一系列边界问题导致代码冗长复杂,这种处理也可称之为 “哨兵” 技巧)。如果最后两个运算符相遇则说明表达式处理完毕,这个运算符的优先级也是最低的(在 prior 表中也有体现)。
2. 输出要求比较复杂,抛去错误信息输出不说(只能具体情况具体分析),不能输出多余的0,折腾了一会儿最后发现用高精度的 Decimal 可以完美满足题目要求。
3. 由于不能输出多余的0,所以在带有小数部分的数字 “录入” 时(代码115-132行),就要把一些多余的0提前去掉(代码121-126行),比如 2.0 这样的情况。
[ by Maples7 ]
[ Copyright @Maples7,转载请注明出处。 ]
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步