一. 中缀表达式与后缀表达式:
1. 简介
中缀表达式的特征是运算符的位置在两个参数之间,例如“13+14×10”、“18÷5+12”。一看便知,中缀表达式就是自然运算式,只不过现在为了有别于后缀表达式,另起了一个名字。
2. 转换
现在来讲解一下如何将中缀表达式转换成后缀表达式。在 Python 代码中可以创建两个空列表,一个用来临时存储运算符,另一个用来存储后缀表达式的列表形式。比如对于中缀表达式“13+14×10”,遍历它,首先将 13 存到后缀表达式列表中索引 0 的位置;第二次循环将 ‘+’ 存到运算符列表中;第三次把 14 存到后缀表达式中,索引为1;接下来遇到‘×’,将它与已经存在于运算符列表的 ‘+’ 进行比较,因为数学运算规则中乘法优先级高于加法,所以将 ‘×’ 存到运算符列表 ‘+’ 之后的位置(反之,如果是先遇到 ‘×’ 后遇到 ‘+’,则将 ‘×’ 弹出并追加到后缀表达式列表的最后位置);第五次遍历,将 10 存到后缀表达式列表索引 2 的位置。中缀表达式已经遍历完成,下面将运算符列表中的元素倒序追加到后缀表达式列表末尾,最终生成的后缀表达式列表就是 [13,14,10,'×','+']。
中缀表达式遍历元素 | 运算符列表 | 后缀表达式列表 |
13 | [] | [13] |
'+' | ['+'] | [13] |
14 | ['+'] | [13,14] |
'×' | ['+','×'] | [13,14] |
10 | ['+','×'] | [13,14,10] |
已遍历完 | ['+','×'] | [13,14,10,'×','+'] |
3. 运算
接下来讲一下后缀表达式的运算步骤。需要用到一个临时的栈。在这个项目里,只需实现一个简单的栈即可。还是用上边这个例子,遍历这个列表,如果遇到数字,就压入栈;如果遇到运算符,就把栈顶的两个数字弹出,执行相应的运算,将运算结果再压入栈;判断当后缀表达式列表长度为0时结束循环,如果这时栈中只有一个元素,弹出这个元素并打印,这个元素就是运算结果。
后缀表达式列表 | 栈 |
[13,14,10,'×','+'] | [] |
[14,10,'×','+'] | [13] |
[10,'×','+'] | [14,13] |
['×','+'] | [10,14,13] |
['+'] | [140,13] |
[] | [153] |
4. 有括号的情况
括号的运算优先级是最高的,所以要在转换后缀表达式的代码中加一点逻辑。在遍历中缀表达式时如果遇到右括号,就在运算符列表里向前找到左括号的索引值,将从这个索引值加一对应的元素到列表末尾的元素从列表中弹出,追加到后缀表达式列表中,左括号则只弹出,不追加到后缀表达式列表中。
二. 计算器的具体实现:
1. 创建一个计算器类:
初始化时只需要定义计算器实例的表达式这一个变量。
def __init__(self, exp):
self.exp = exp
2. 格式检查:
这个步骤指的是在用户输入了表达式之后,程序会对表达式的逻辑是否正确进行判断,比如连续输入了两个运算符。
def preCheck(self):
exp = self.exp
for index in range(len(exp)):
if exp[index] in "+-×÷" and exp[index - 1] in "+-×÷":
return False
if exp[index] == "." and exp[index - 1] == ".":
return False
return self.exp
3. 格式改造:
这一步的作用是将用户输入的字符串改造成列表,这样可以使数字和运算符独立存在,方便后续操作。
def formatChange(self):
self.midfix = []
tmp = ''
for index in range(len(self.exp)):
// 如果第一个字符是负号“-”
if index == 0 and self.exp[index] == "-":
tmp += "-"
// 如果字符是负号“-”且前一个字符是左括号
elif index > 0 and self.exp[index - 1] == "(" and self.exp[index] == "-":
tmp += str(self.exp[index])
// 如果字符是数字或小数点或“π”
elif self.exp[index].isdigit() or self.exp[index] == '.' or self.exp[index] == 'π':
tmp += str(self.exp[index])
else:
if tmp != '':
self.midfix.append(tmp)
tmp = ''
self.midfix.append(self.exp[index])
if tmp != '':
self.midfix.append(tmp)
return self.midfix
4. 转换后缀表达式:
def makeSuffixExp(self):
opList = []
opDict = self.opDict
self.suffixExpList = []
for item in self.midfix:
// 如果是数
if self.isNumber(item):
self.suffixExpList.append(item)
// 如果是π
if item == 'π':
self.suffixExpList.append(str(math.pi))
// 如果是这几种运算符号(+-×÷^!)
if item in "+-×÷^!":
if opList == []:
opList.append(item)
else:
if opDict[item] <= opDict[opList[len(opList)-1]]:
for index in range(len(opList)):
self.suffixExpList.append(opList[index])
del opList[index]
opList.append(item)
// 如果是左括号
if item == "(":
opList.append(item)
// 如果是右括号
if item == ")":
pt = len(opList) - 1
while opList[pt] != "(":
pt -= 1
pt += 1
for index in range(pt, len(opList)):
self.suffixExpList.append(opList[index])
opList.pop(index)
opList.pop()
self.suffixExpList.extend(opList[::-1])
return self.suffixExpList
其中,opDict 是我定义的一个字典,定义了运算符的优先级:
opDict = {"+":1, "-":1, "×":2, "÷":2, "(":0, "^":3, "!":3}
这里也引用了我自定义的一个函数 isNumber,用来判断一个变量是否是数字:
def isNumber(self, num):
try:
float(num)
return True
except ValueError:
pass
try:
import unicodedata
unicodedata.numeric(num)
return True
except (TypeError, ValueError):
pass
return False
5. 运算:
def calculate(self):
stack = Stack()
while expLength := len(self.suffixExpList):
poppedItem = self.suffixExpList.pop(0)
if poppedItem in "+-×÷^":
num2 = stack.pop()
num1 = stack.pop()
if poppedItem == "+":
newNum = self.add(num1, num2)
elif poppedItem == "-":
newNum = self.sub(num1, num2)
elif poppedItem == "×":
newNum = self.mul(num1, num2)
elif poppedItem == "÷":
newNum = self.dev(num1, num2)
else:
newNum = self.power(num1, num2)
stack.push(newNum)
elif poppedItem == "!":
num = stack.pop()
newNum = self.factorial(num)
stack.push(newNum)
else:
stack.push(float(poppedItem))
expLength -= 1
if len(stack) == 1:
return stack.pop()
这个函数中引用了几个自定义的运算函数:
// 加
def add(self, num1, num2):
return num1 + num2
// 减
def sub(self, num1, num2):
return num1 - num2
// 乘
def mul(self, num1, num2):
return num1 * num2
// 除
def dev(self, num1, num2):
return num1 / num2
// 指数
def power(self, num1, num2):
return math.pow(num1, num2)
// 阶乘
def factorial(self, num):
result = 1
for i in range(2, int(num)+1):
result *= i
return result
6. 执行函数:
def execute(self):
if self.preCheck():
self.formatChange()
self.makeSuffixExp()
return self.cancelZero(self.calculate())
else:
return "ERROR"
这个函数里引用的cancelZero函数的作用是如果结果的小数位为0,则在显示时去掉小数部分:
def cancelZero(self, num):
if str(num).endswith(".0"):
return int(num)
else:
return num
7. 栈的实现:
在第 5 步运算这个函数中,使用了 Stack 类的实例,这个类的实现放在这里,就是一个简单的用链表实现的栈:
class Node:
def __init__(self, data, next=None):
self.data = data
self.next = next
class Stack:
def __init__(self):
self.head = None
def __iter__(self):
def visitNodes(node):
if node != None:
visitNodes(node.next)
tempList.append(node.data)
tempList = list()
visitNodes(self.head)
return iter(tempList)
def peak(self):
if self.isEmpty():
raise KeyError("The stack is empty.")
return self.head.data
def isEmpty(self):
return self.head is None
def __len__(self):
if cur := self.head:
self.size = 1
else:
return 0
while cur.next is not None:
self.size += 1
cur = cur.next
return self.size
def push(self, data):
self.head = Node(data, self.head)
def pop(self):
poppedData = self.head.data
self.head = self.head.next
return poppedData
8. 使用 tkinter 搞个可视化界面:
tkinter 在这个项目中的作用就比较简单了,我主要的想法就是在窗口最上部放一个显示屏,下边都是计算器的各种按钮,布局比较简单。为了不逼死强迫症(我就是强迫症),我设计了5X5共25个按钮,在以上的基础上又加入了开根号、清除一个数字的C按钮、清除所有数字的CE按钮。以下是我的代码,抛砖引玉吧:
import tkinter as tk
from turtle import bgcolor
from calculator import Expression
window = tk.Tk()
window.title("计算器")
window.geometry('450x490')
window.configure(background="#4169e1", cursor="mouse")
exp = tk.StringVar()
exp.set('')
entry_panel = tk.Entry(window, textvariable=exp, width=25, font=("Arial", 20), foreground="#00bfff", background="#191970", relief="sunken")
entry_panel.grid(row=0, column=4, columnspan=5)
//输入字符的功能
def enterNum(num):
if exp.get() == "ERROR":
exp.set("")
exp.set(exp.get() + num)
//清空显示屏的功能
def clearEntry():
exp.set('')
//退格功能
def backspace():
exp.set(exp.get()[:-1])
//开二次根号功能
def sqrtExec():
a = Expression(exp.get())
exp.set(a.sqrtExec())
//计算
def calculate():
a = Expression(exp.get())
exp.set(str(a.execute()))
w = 10
h = 5
buttonOne = tk.Button(text='1', width=w, height=h, command=lambda: enterNum('1'), font=(30), background="#00bfff")
buttonOne.grid(row=2, column=4)
buttonTwo = tk.Button(text='2', width=w, height=h, command=lambda: enterNum('2'), font=(30), background="#00bfff")
buttonTwo.grid(row=2, column=5)
buttonThree = tk.Button(text='3', width=w, height=h, command=lambda: enterNum('3'), font=(30), background="#00bfff")
buttonThree.grid(row=2, column=6)
buttonFour = tk.Button(text='4', width=w, height=h, command=lambda: enterNum('4'), font=(30), background="#00bfff")
buttonFour.grid(row=3, column=4)
buttonFive = tk.Button(text='5', width=w, height=h, command=lambda: enterNum('5'), font=(30), background="#00bfff")
buttonFive.grid(row=3, column=5)
buttonSix = tk.Button(text='6', width=w, height=h, command=lambda: enterNum('6'), font=(30), background="#00bfff")
buttonSix.grid(row=3, column=6)
buttonSeven = tk.Button(text='7', width=w, height=h, command=lambda: enterNum('7'), font=(30), background="#00bfff")
buttonSeven.grid(row=4, column=4)
buttonEight = tk.Button(text='8', width=w, height=h, command=lambda: enterNum('8'), font=(30), background="#00bfff")
buttonEight.grid(row=4, column=5)
buttonNine = tk.Button(text='9', width=w, height=h, command=lambda: enterNum('9'), font=(30), background="#00bfff")
buttonNine.grid(row=4, column=6)
buttonClear = tk.Button(text='CE', width=w, height=h, command=clearEntry, font=(30), background="#1e90ff")
buttonClear.grid(row=2, column=8)
buttonZero = tk.Button(text='0', width=w, height=h, command=lambda: enterNum('0'), font=(30), background="#00bfff")
buttonZero.grid(row=5, column=5)
buttonBackspace = tk.Button(text='C', width=w, height=h, command=backspace, font=(30), background="#1e90ff")
buttonBackspace.grid(row=2, column=7)
buttonAdd = tk.Button(text='+', width=w, height=h, command=lambda: enterNum('+'), font=(30), background="#87cefa")
buttonAdd.grid(row=3, column=7)
buttonMinus = tk.Button(text='-', width=w, height=h, command=lambda: enterNum('-'), font=(30), background="#87cefa")
buttonMinus.grid(row=4, column=7)
buttonMulti = tk.Button(text='×', width=w, height=h, command=lambda: enterNum('×'), font=(30), background="#87cefa")
buttonMulti.grid(row=5, column=7)
buttonDevide = tk.Button(text='÷', width=w, height=h, command=lambda: enterNum('÷'), font=(30), background="#87cefa")
buttonDevide.grid(row=6, column=7)
buttonCalc = tk.Button(text='=', width=w, height=h, command=calculate, font=(30), background="#468284")
buttonCalc.grid(row=6, column=8)
buttonLeftPar = tk.Button(text='(', width=w, height=h, command=lambda: enterNum('('), font=(30), background="#87cefa")
buttonLeftPar.grid(row=6, column=5)
buttonRightPar = tk.Button(text=')', width=w, height=h, command=lambda: enterNum(')'), font=(30), background="#87cefa")
buttonRightPar.grid(row=6, column=6)
buttonDot = tk.Button(text='.', width=w, height=h, command=lambda: enterNum('.'), font=(30), background="#00bfff")
buttonDot.grid(row=6, column=4)
buttonPower = tk.Button(text='^', width=w, height=h, command=lambda: enterNum('^'), font=(30), background="#87cefa")
buttonPower.grid(row=3, column=8)
buttonSqrt = tk.Button(text='√', width=w, height=h, command=sqrtExec, font=(30), background="#87cefa")
buttonSqrt.grid(row=4, column=8)
buttonFact = tk.Button(text='!', width=w, height=h, command=lambda: enterNum('!'), font=(30), background="#87cefa")
buttonFact.grid(row=5, column=8)
buttonDouZero = tk.Button(text='00', width=w, height=h, command=lambda: enterNum('00'), font=(30), background="#00bfff")
buttonDouZero.grid(row=5, column=4)
buttonPi = tk.Button(text='π', width=w, height=h, command=lambda: enterNum('π'), font=(30), background="#00bfff")
buttonPi.grid(row=5, column=6)
window.mainloop()