(Python)用栈实现计算器的原理及实现
前言
我们日常使用的计算器是怎么实现计算的呢?能自己判断运算符的优先级去计算,能处理括号的匹配,这些都是怎么实现的呢?
一个大家熟知的答案是用栈,好的,那么为什么要用栈?为什么栈能实现呢?
(前|中|后)缀表达式?
我们最熟悉的应该是我们的中缀表达式,也就是形如 1 + 3 * 2 这样的式子,即操作符位于操作数之间。
从类似的概念出发,我们不难得到前缀表达式是 + 1 * 3 2(操作符位于操作数之前),后缀表达式是 1 3 2 * +
前缀表达式又叫做波兰表示法(Polish notation,或波兰记法),后缀表达式叫做逆波兰表示法(Reverse Polish notation,RPN,或逆波兰记法)。
而我们给计算器输入的就是我们的中缀表达式,中缀表达式因为其只能按照顺序一个个计算下去,导致对于运算符的优先级的判断无法实现,因此,一个常见的操作就是,将中缀表达式转换为后缀表达式(可以判断运算的优先级),然后让我们的计算器进行计算。
对于中缀转后缀的过程的实现很简单,即使用运算符优先级栈:
初始化一个空栈,用于存储运算符(
+、-、*、/等),并初始化一个空的输出队列(用于存储后缀表达式的结果)。从左到右扫描中缀表达式,逐个处理每个字符:
如果是操作数(如数字),直接加入输出队列。
如果是左括号(
(),直接压入栈中。如果是右括号(
)),则依次弹出栈顶运算符并加入输出队列,直到遇到左括号(左括号出栈但不加入输出队列)。如果是运算符(
+、-、*、/等),则:如果栈为空,直接压入栈中。
如果栈不为空,比较当前运算符与栈顶运算符的优先级:
- 如果当前运算符的优先级高于栈顶运算符,直接压入栈中。
- 如果当前运算符的优先级低于或等于栈顶运算符,依次弹出栈顶运算符并加入输出队列,直到栈为空或栈顶运算符的优先级低于当前运算符,然后将当前运算符压入栈中。
扫描结束后,如果栈中仍有运算符,依次弹出并加入输出队列。
输出队列中的内容即为后缀表达式。
运算符优先级的判定,在没有括号的情况下是
*/>+-同级遵循从左到右的顺序
拿最简单的 1 + 3 * 2 - 5 举例
| 步骤 | 操作 | 输出(out) | 栈(stack) |
|---|---|---|---|
| 1 | 1 -> out |
[1] |
[] |
| 2 | + -> stack |
[1] |
[+] |
| 3 | 3 -> out |
[1,3] |
[+] |
| 4 | * > +, * -> stack |
[1,3] |
[+,*] |
| 5 | 2 -> out |
[1,3,2] |
[+,*] |
| 6 | - < *, * <- stack, * -> out |
[1,3,2,*] |
[+] |
| 7 | - = +, + <- stack, + -> out |
[1,3,2,*,+] |
[] |
| 8 | - -> stack |
[1,3,2,*,+] |
[-] |
| 9 | 5 -> out |
[1,3,2,*,+,5] |
[-] |
| 10 | stack is not [], - <- stack |
[1,3,2,*,+,5,-] |
[] |
使用后缀表达式的理由是,它只需要用一个从左到右的扫描,每次操作的时间复杂度只需要\(O(1)\),对于长度为\(n\)的表达式,后缀表达式的计算复杂度为\(O(n)\),以及说,这种表达对于计算机是没有歧义的,优先级明确的,易于实现的。
计算器的实现逻辑
总体实现逻辑
那么计算器的实现逻辑就可以写出如下:
A[中缀表达式] --> B[后缀表达式];
B --> C[栈操作计算后缀表达式]
中缀表达式转后缀表达式
假如我们有表达式如下:
expression = "1-2*((60-30+(-40/5)*(9-2*5/3+7/3*99/4*2998+10*568/14))-(-4*3)/(16-3*2))"
中缀表达式转后缀表达式就用我们最爱的正则表达式解决。
-
\d+\.\d+ :匹配小数部分。 -
\d+ :匹配整数部分。 -
[+\-*/()] :匹配运算符和括号。
什么,负数怎么办,怎么将其跟-区分?把它标记出来单独处理就是,比方说用 u- 表示负号。
怎么标记呢?负号出现的位置只有两种情况:
- 表达式的开头
- 前一个操作符的后面
这样,解析的问题就迎刃而解了。
import re
def infix_expression2suffix_expression(infix_expression):
precedence = {'+': 1, '-': 1, '*': 2, '/': 2, '(': 0, 'u-': 3}
op_stack = []
suffix_expression = []
# 匹配小数,整数和操作符
tokens = re.findall(r'\d+\.\d+|\d+|[+\-*/()]', infix_expression)
print(tokens)
for i, token in enumerate(tokens):
if re.match(r'\d+\.\d+|\d+', token): # 如果是数字
suffix_expression.append(token)
elif token == '(': # 如果是左括号
op_stack.append(token)
elif token == ')': # 如果是右括号
while op_stack and op_stack[-1] != '(':
suffix_expression.append(op_stack.pop())
op_stack.pop() # 弹出左括号
else: # 如果是操作符
# 处理负号(负数),表达式开头,前一个操作符的后面
if token == '-' and (i == 0 or tokens[i - 1] in "+-*/("):
token = 'u-' # 标记为负号
while op_stack and precedence.get(token, 0) <= precedence.get(op_stack[-1], 0):
suffix_expression.append(op_stack.pop())
op_stack.append(token)
# 弹出操作符栈剩余的操作符添加到后缀表达式
while op_stack:
suffix_expression.append(op_stack.pop())
print("Suffix expression:", " ".join(suffix_expression))
return " ".join(suffix_expression)
if __name__ == '__main__':
infix_expression = expression
suffix_expression = infix_expression2suffix_expression(infix_expression)
print("Suffix expression:", suffix_expression)
计算后缀表达式的结果
怎么计算后缀表达式的结果呢?从左到右扫描,遇到数字压入栈,遇到操作符就运算,简单无困扰。
负数的话,只需要取出来计算负号,再压回栈就好了。
import re
def evaluate_suffix_expression(suffix_expression):
stack = []
tokens = suffix_expression.split()
print(tokens)
for token in tokens:
if re.match(r'\d+\.\d+|\d+', token):
stack.append(float(token))
elif token == 'u-':
stack.append(-stack.pop())
else:
operand2 = stack.pop()
operand1 = stack.pop()
if token == "+":
stack.append(operand1 + operand2)
elif token == "-":
stack.append(operand1 - operand2)
elif token == "*":
stack.append(operand1 * operand2)
elif token == "/":
stack.append(operand1 / operand2)
else:
raise ValueError("Invalid operator: " + token)
print(f'{operand1} {token} {operand2} = {stack[-1]}')
if len(stack) != 1:
raise ValueError("Invalid expression: " + suffix_expression)
return stack[0]
if __name__ == "__main__":
print(evaluate_suffix_expression(infix_expression2suffix_expression(expression)))
计算的整体实现
接下来把上面的实现过程封装在一起,就可以毫无负担地实现计算器了。
def evaluate(expression):
return evaluate_suffix_expression(infix_expression2suffix_expression(expression))
if __name__ == '__main__':
expression = input("Enter an infix expression: ")
print(evaluate(expression))
进一步简化
前面我们做的是,将中缀表达式转换为后缀表达式,再用后缀表达式去计算得到结果,这个过程中都需要用到数字栈和运算符栈两个栈。
那么,我们可以边读边运算吗?当然可以。后缀表达式直接展示了运算的顺序,那么我们得到后缀表达式的过程,其实就是运算的过程。
import re
def evaluate_expression(expression):
def four_rules_eval(sum_stack, op_stack):
op = op_stack.pop()
if op == 'u-':
operand = sum_stack.pop()
sum_stack.append(-operand)
print(f'u-{operand} = {sum_stack[-1]}')
else:
operand2 = sum_stack.pop()
operand1 = sum_stack.pop()
if op == "+":
sum_stack.append(operand1 + operand2)
elif op == "-":
sum_stack.append(operand1 - operand2)
elif op == "*":
sum_stack.append(operand1 * operand2)
elif op == "/":
sum_stack.append(operand1 / operand2)
else:
raise ValueError("Invalid operator: " + op)
print(f'{operand1} {op} {operand2} = {sum_stack[-1]}')
precedence = {'+': 1, '-': 1, '*': 2, '/': 2, '(': 0, 'u-': 3}
op_stack = []
sum_stack = []
tokens = re.findall(r'\d+\.\d+|\d+|[+\-*/()]', expression)
print(tokens)
for i, token in enumerate(tokens):
if re.match(r'\d+\.\d+|\d+', token): # 如果是数字
sum_stack.append(float(token))
elif token == '(': # 如果是左括号
op_stack.append(token)
elif token == ')': # 如果是右括号
while op_stack and op_stack[-1] != '(':
four_rules_eval(sum_stack, op_stack)
op_stack.pop() # 弹出左括号
else: # 如果是操作符
# 处理负号(负数),表达式开头,前一个操作符的后面
if token == '-' and (i == 0 or tokens[i - 1] in "+-*/("):
op_stack.append('u-')
else:
while op_stack and precedence.get(token, 0) <= precedence.get(op_stack[-1], 0):
four_rules_eval(sum_stack, op_stack)
op_stack.append(token)
while op_stack:
four_rules_eval(sum_stack, op_stack)
return sum_stack[-1]
if __name__ == '__main__':
expression = "1-2*((60-30+(-40/5)*(9-2*5/3+7/3*99/4*2998+10*568/14))-(-4*3)/(16-3*2))"
result = evaluate_expression(expression)
print(result) # out: 2776672.6952380957
参考资料
前缀表达式、中缀表达式和后缀表达式:https://www.cnblogs.com/zzliu/p/10801113.html
(Python)用栈实现计算器的原理及实现的更多相关文章
- python 利用栈实现复杂计算器
#第五周的作业--多功能计算器#1.实现加减乘除及括号的优先级的解析,不能使用eval功能,print(eval(equation))#2.解析复杂的计算,与真实的计算器结果一致#用户输入 1 - 2 ...
- python 全栈开发之路 day1
python 全栈开发之路 day1 本节内容 计算机发展介绍 计算机硬件组成 计算机基本原理 计算机 计算机(computer)俗称电脑,是一种用于高速计算的电子计算机器,可以进行数值计算,又可 ...
- 老男孩Python全栈第2期+课件笔记【高清完整92天整套视频教程】
点击了解更多Python课程>>> 老男孩Python全栈第2期+课件笔记[高清完整92天整套视频教程] 课程目录 ├─day01-python 全栈开发-基础篇 │ 01 pyth ...
- Python全栈开发【基础四】
Python全栈开发[基础四] 本节内容: 匿名函数(lambda) 函数式编程(map,filter,reduce) 文件处理 迭代器 三元表达式 列表解析与生成器表达式 生成器 匿名函数 lamb ...
- Python全栈之路目录结构
基础 1.Python全栈之路-----基础篇 2.Python全栈之路---运算符与基本的数据结构 3.Python全栈之路3--set集合--三元运算--深浅拷贝--初识函数 4.Python全栈 ...
- Python全栈开发
Python全栈开发 一文让你彻底明白Python装饰器原理,从此面试工作再也不怕了. 一.装饰器 装饰器可以使函数执行前和执行后分别执行其他的附加功能,这种在代码运行期间动态增加功能的方式,称之为“ ...
- Python源代码剖析笔记3-Python运行原理初探
Python源代码剖析笔记3-Python执行原理初探 本文简书地址:http://www.jianshu.com/p/03af86845c95 之前写了几篇源代码剖析笔记,然而慢慢觉得没有从一个宏观 ...
- python全栈开发中级班全程笔记(第二模块、第四章(三、re 正则表达式))
python全栈开发笔记第二模块 第四章 :常用模块(第三部分) 一.正则表达式的作用与方法 正则表达式是什么呢?一个问题带来正则表达式的重要性和作用 有一个需求 : 从文件中读取所有联 ...
- Python全栈开发记录_第一篇(循环练习及杂碎的知识点)
Python全栈开发记录只为记录全栈开发学习过程中一些难和重要的知识点,还有问题及课后题目,以供自己和他人共同查看.(该篇代码行数大约:300行) 知识点1:优先级:not>and 短路原则:a ...
- python全栈开发目录
python全栈开发目录 Linux系列 python基础 前端~HTML~CSS~JavaScript~JQuery~Vue web框架们~Django~Flask~Tornado 数据库们~MyS ...
随机推荐
- 谈谈区块链(18):以太坊的UTXO
https://www.8btc.com/article/117510 本来并没有计划写这篇文章,只是这两天微信里有一篇文章传播的比较厉害,叫:<其实并没有什么比特币,只有UTXO>.其中 ...
- 区块链技术已经衰落了吗?(区块链已die)
区块链技术已经好多年没有听到有人提了,不过比特币却一直是不是的又新闻出现,当然国内已经把比特币交易归入到了不合法的地位了.区块链技术是国家战略的技术,但是这个技术说实话确实不是很高深,或者说蛮easy ...
- 一次 Spring 扫描 @Component 注解修饰的类坑
问题现象 之前遇到过一个问题,在一个微服务的目录下有相同功能 jar 包的两个不同的版本,其中一个版本里面的类有 @Component 注解,另外一个版本的类里面没有 @Component 注解,且按 ...
- Qt项目升级到Qt6吐血经验总结
Qt的版本发布越来越频繁,Qt6发布已经有一段时间了,越来越多的人咨询之前的代码是否可以增加对Qt6的支持,包括开源的项目QWidgetDemo(一年时间超过2.6K star),近期百忙之中,对所有 ...
- PHP 安装启用openssl(解决 wordpress可选的模组openssl未被安装或已被禁用)
本教程仅适用Windows Servier IIS网站服务器. 我的博客使用IIS搭建,相比Linux,相关的教程格外少.因此让以后的小伙伴也能马上解决问题,分享此方法. 首先需要下载php对应版本的 ...
- 直播预览层添加滤镜效果(CIFilter使用场景)
直播预览层添加滤镜效果 原理,在显示之前,提前对图片进行滤镜处理,把处理后的图片展示出来就好了. CIFiter(滤镜类):给图片添加特殊效果(模糊,高亮等等). CIFiter滤镜分类(一个滤镜可能 ...
- linux:lamp环境
关于LAMP LAMP搭建 安装php和Apache 先装php,因为安装php有apache的依赖包 yum install php 启动Apache service httpd start 启动成 ...
- thewall靶机
includes.php 内有文件读取漏洞 一开始是想着直接用为协议写入一句话木马但是后来发现不行 因为他的文件读取方式长这样 点击查看代码 <?php include ('/var/www/h ...
- delphi中实现http请求和提交
在对接本地的一个药械监管系统使用实现的代码,编译环境delphi Xe7 ,使用类TIdHTTP实现网络的get请求和post提交 //设置组件的属性 procedure TfrmMain.SetHt ...
- 认识soui4js(第2篇):代码编辑及调试
开始 假定您使用向导在d:\jsdemo目录创建一个工程,您也已经安装好了vscode, 那么您应该可以看到下面的界面效果: 工程生成后,主要包含一个soui资源包及一个main.js 要运行这个程序 ...