目录
栈的应用-表达式求值
1.实现功能
2.需要注意的问题
2.1 解决运算符优先级问题
2.2 如何利用栈解决表达式求值
2.3 括号优先级和运算问题
2.4 减号运算符的避坑指南
3.代码实现
3.1代码拆解详解
3.1.1栈的定义和出栈、入栈操作
3.1.2一些工具函数
3.1.3逻辑实现主体
3.2全量代码及执行结果
先吐槽一下:学习的时候以为实现逻辑蛮清晰简单的。代码写起来好几百行...可能是自己的代码能力太弱了。
2.4.1 问题描述:
表达式中出现单个减号,按照2.2中的运算逻辑不会有什么问题。但是如果出现两个连续减号如(7-4/2-3),其逻辑是:
(1)第一个减号与除号比较,减号入栈,除号与下一个运算符比较;
(2)遍历到第二个减号,比较后除号运算,第二个减号入栈,此时括号部分的表达式顺序变成了(7-2-3);
(3)遍历遇到右括号),按照逻辑,循环出栈符号进行运算,此时就会先计算2-3=1,再计算7-1=6。计算结果错误。
2.4.2 解决办法:
可以声明一个标记变量,标记符号是否为减号。如果遇到了减号,将减号改为加号入栈,且下一个数字前添加负号再入栈,然后再将标记还原。
此逻辑处理后,上述例子就会变成(7+(-2)+(-3)),(-2)+(-3)=-5,7+(-5)=2。计算结果正确。
以上将中缀表达式转换为后缀表达式计算的时间复杂度可以达到O(n)
考虑到表达式可能长度无法预估,和确保操作的便利性。我选择栈的链式存储结构-链栈来实现。
1.链栈的定义
type LinkStack struct {
data interface{}
next *LinkStack
}
2.栈的初始化、入栈、出栈
//链栈初始化
func initLinkStack() (linkStack *LinkStack) {
//初始化链栈,新建链栈头节点
linkStack = &LinkStack{
data: "stackHeader",
next: nil,
}
return linkStack
}
//链栈-入栈
func (s *LinkStack) push(v interface{}) {
//链栈可先不考虑栈满,因为目前没有对栈做限制
pushNode := &LinkStack{
data: v,
next: nil,
}
pushNode.next = s.next //入栈节点的next指向头节点的next节点
s.next = pushNode //头节点指向入栈节点
}
//链栈-出栈
func (s *LinkStack) pop() (interface{}, error) {
//判断栈是否为空
if s.next == nil {
return "", errors.New("error: 栈为空")
}
tmpTop := s.next
v := tmpTop.data //出栈,获取节点的元素值
s.next = tmpTop.next //头节点指向原栈顶节点的下一个节点
tmpTop.next = nil //原栈顶节点指向nil
return v, nil
}
1.判断运算符优先级
//判断运算符号优先级
func judgmentType(symbol string) int {
switch symbol {
case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9":
return 0
case "+", "-":
return 1
case "*", "/":
return 2
case "(":
return 3
case ")":
return 4
default:
return -1
}
}
2.判断运算符后对数字进行计算
//判断运算符号并对数值进行计算
func calculatedFormula(num1 int, num2 int, operator string) int {
switch operator {
case "+":
fmt.Println("提示: 计算num1 + num2")
return num1 + num2
case "-":
fmt.Println("提示: 计算num1 - num2")
return num1 - num2
case "*":
fmt.Println("提示: 计算num1 * num2")
return num1 * num2
case "/":
fmt.Println("提示: 计算num1 / num2")
return num1 / num2
default:
fmt.Println("提示: 运算符号没有对应计算方法")
return -9999
}
}
1.循环-遇到右括号),或遍历完表达式后,循环出栈&计算数值
//循环出栈symbolLinkStack中的符号,并进行运算。如遇到左括号(或栈为空时结束循环。
func loopCalculationStack(numLinkStack *LinkStack, symbolLinkStack *LinkStack) {
for {
if symbolLinkStack.next == nil {
fmt.Println("栈中无运算符号")
break
}
tmpSymbol, _ := symbolLinkStack.pop()
preSymbol := tmpSymbol.(string)
fmt.Println("-旧符号出栈-", preSymbol)
if preSymbol == "(" {
fmt.Println("提示: 遇到(, (出栈, 结束遍历符号栈的计算操作。")
break
} else {
//两数根据符号进行运算,并入栈
calculatedNum(numLinkStack, preSymbol)
}
}
}
2.单次-符号出栈&数字化计算
//两数根据符号进行运算,并入栈
func calculatedNum(numLinkStack *LinkStack, symbol string) {
v2, err := numLinkStack.pop()
if err != nil {
fmt.Println(err)
}
v1, err := numLinkStack.pop()
if err != nil {
fmt.Println(err)
}
num1 := v1.(int)
num2 := v2.(int)
fmt.Printf("-旧数字出栈- num2: %v; num1: %v\n", num2, num1)
calculatedValue := calculatedFormula(num1, num2, symbol)
//计算数值入栈
fmt.Println("-计算结果入栈-", calculatedValue)
numLinkStack.push(calculatedValue)
}
3.遍历表达式后的出入栈主体逻辑
实际代码
func calculationFormulaResults(symbol string, numLinkStack *LinkStack, symbolLinkStack *LinkStack) (int, error) {
fmt.Println("---遍历公式---")
symbolTag := -1 //符号标记。判断数字前面符号是否为-,如果是减号,则将减号存为加号+,且数字取本身的负数。
numTag := -1 //数字标记。判断是否为多位数
symbolLoopTimes := 0 //记录公式循环到第几次
for _, s := range symbol {
symbolLoopTimes++
fmt.Printf("----%v----\n", symbolLoopTimes)
sType := judgmentType(string(s))
if sType == 0 {
//公式中的数字直接入栈。如果遍历连续数字,说明是多位数,需要拼接多位数。
var num int
if numTag == 1 {
prenumTmp, _ := numLinkStack.pop()
prenumI := prenumTmp.(int)
prenumA := strconv.Itoa(prenumI)
fmt.Println(prenumA + string(s))
num, _ = strconv.Atoi(prenumA + string(s))
} else {
num, _ = strconv.Atoi(string(s))
}
//如果数字前的符号为减号,则改为加号,且数字变为负数。
if symbolTag == 1 {
symbolLinkStack.pop() //减号出栈
symbolLinkStack.push("+") //加号入栈
fmt.Println("提示:数字前面为负号,执行加号入栈、数字加负号")
num = -num //数字添加负号
symbolTag = -1 //操作结束,还原符号标记为-1
}
fmt.Println("-新数字入栈-", num)
numLinkStack.push(num)
numTag = 1
} else if sType == 1 || sType == 2 {
numTag = -1
//符号如果为符号,修改符号标记为1
if string(s) == "-" {
symbolTag = 1
}
if symbolLinkStack.next == nil {
fmt.Println("-新符号入栈-", string(s))
symbolLinkStack.push(string(s))
} else {
tmpSymbol := symbolLinkStack.next.data //查看栈顶符号
preSymbol, _ := tmpSymbol.(string)
nowSymbol := string(s)
//获取当前符合和前一个符号的优先级,如果符号是(,则(优先级最低
var prePriority int
if preSymbol == "(" {
fmt.Println("-前一个符号是(-")
prePriority = 0
} else {
prePriority = judgmentType(preSymbol)
}
nowPriority := judgmentType(nowSymbol)
if prePriority >= nowPriority { //如果前一个运算符优先级大于后一个运算符优先级
fmt.Printf("提示: 栈顶符号%v优先级大于或等于当前符号%v优先级\n", preSymbol, nowSymbol)
fmt.Println("-旧符号出栈-", preSymbol)
symbolLinkStack.pop() //栈顶旧运算符弹出
fmt.Println("-新符号入栈-", nowSymbol)
symbolLinkStack.push(nowSymbol) //新运算符入栈
//两数根据符号进行运算,并入栈
calculatedNum(numLinkStack, preSymbol)
} else if prePriority < nowPriority { //如果前一个运算符优先级小于后一个运算符优先级
fmt.Printf("提示: 栈顶符号%v优先级小于当前符号%v优先级\n", preSymbol, nowSymbol)
fmt.Println("-新符号入栈-", nowSymbol)
symbolLinkStack.push(nowSymbol)
}
}
} else if sType == 3 {
fmt.Println("-左括号直接入栈-", string(s))
symbolLinkStack.push(string(s))
symbolTag = -1 //左括号前如有减号,还原符号标记为-1
} else if sType == 4 {
fmt.Println("-右括号不入栈-", string(s))
//遇到),循环出栈所有栈中运算符并计算。遇到(时停止
loopCalculationStack(numLinkStack, symbolLinkStack)
} else {
return -1, errors.New("公式格式不合法")
}
}
// 遍历完公式后,计算栈内剩余的元素
fmt.Println("---公式遍历结束,计算栈中剩余元素---")
loopCalculationStack(numLinkStack, symbolLinkStack)
//所有计算完成后,结果被存入numLinkStack,将结果出栈
resultTmp, err := numLinkStack.pop()
result := resultTmp.(int)
if err != nil {
fmt.Println("获取计算公式最终返回值时报错:", err)
}
return result, nil
}
伪代码解释
func calculationFormulaResults(表达式, 存数字的栈, 存符号的栈) (计算结果, 报错信息) {
#初始化一些必要的标记和变量
for 循环遍历表达式 {
if 如果遍历的是数字 {
数字直接入栈
#1.考虑是否为多为数字
#2.考虑数字前是否为减号
}else if 如果遍历的是加、减、乘、除号 {
if 如果符号栈是空的 {
第一个符号就直接入栈
}else 栈不是空 {
1.通过工具函数judgmentType,获取符号的优先级
#如果出栈的前一个符号是左括号(,那么左括号的优先级比所有符号都低。
2.比较前一个符号和后一个符号的优先级
if 前面符号的优先级 >= 当前符号的优先级 {
1.用前一个符号计算两数的数值,然后数值入栈
2.当前符号入栈
} else if 前面符号的优先级 < 当前符号的优先级{
1.当前符号入栈,等待与后面符号比较优先级
}
}
}else if 如果遍历的是左括号( {
1.左括号直接入栈
}else if 如果遍历的是右括号 {
#右括号不入栈
1.遍历出栈并计算栈中的数字和符号,直到遇到左括号(
}else {
如果是其他字符,则报错公式格式不合法。
}
}
#for循环遍历表达式结束
1.遍历数字和字符栈中剩余的所有元素,并进行计算。
2.retrun 计算结果
}
package main
import (
"errors"
"fmt"
"strconv"
)
type LinkStack struct {
data interface{}
next *LinkStack
}
func main() {
//初始化链栈
fmt.Println("---初始化链栈---")
numLinkStack := initLinkStack()
symbolLinkStack := initLinkStack()
//定义一条四则运算
// formula := "1+3*(4/2+1)+3*3+3/(7-6/3-2)*2"
formula := "(43-40)*s(2-(8+3))"
fmt.Println("计算公式: ", formula)
//计算
result, err := calculationFormulaResults(formula, numLinkStack, symbolLinkStack)
if err != nil {
fmt.Println("error!!!: ", err)
} else {
fmt.Printf("\n\n---最终计算结果---\n")
fmt.Println("计算公式: ", formula)
fmt.Println("公式计算结果: ", result)
}
}
func initLinkStack() (linkStack *LinkStack) {
//初始化链栈,新建链栈头节点
linkStack = &LinkStack{
data: "stackHeader",
next: nil,
}
return linkStack
}
func (s *LinkStack) push(v interface{}) {
//链栈可先不考虑栈满,因为目前没有对栈做限制
pushNode := &LinkStack{
data: v,
next: nil,
}
pushNode.next = s.next //入栈节点的next指向头节点的next节点
s.next = pushNode //头节点指向入栈节点
}
func (s *LinkStack) pop() (interface{}, error) {
//判断栈是否为空
if s.next == nil {
return "", errors.New("error: 栈为空")
}
tmpTop := s.next
v := tmpTop.data //出栈,获取节点的元素值
s.next = tmpTop.next //头节点指向原栈顶节点的下一个节点
tmpTop.next = nil //原栈顶节点指向nil
return v, nil
}
//判断运算符号优先级
func judgmentType(symbol string) int {
switch symbol {
case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9":
return 0
case "+", "-":
return 1
case "*", "/":
return 2
case "(":
return 3
case ")":
return 4
default:
return -1
}
}
//判断运算符号并对数值进行计算
func calculatedFormula(num1 int, num2 int, operator string) int {
switch operator {
case "+":
fmt.Println("提示: 计算num1 + num2")
return num1 + num2
case "-":
fmt.Println("提示: 计算num1 - num2")
return num1 - num2
case "*":
fmt.Println("提示: 计算num1 * num2")
return num1 * num2
case "/":
fmt.Println("提示: 计算num1 / num2")
return num1 / num2
default:
fmt.Println("提示: 运算符号没有对应计算方法")
return -9999
}
}
func calculationFormulaResults(symbol string, numLinkStack *LinkStack, symbolLinkStack *LinkStack) (int, error) {
fmt.Println("---遍历公式---")
symbolTag := -1 //符号标记。判断数字前面符号是否为-,如果是减号,则将减号存为加号+,且数字取本身的负数。
numTag := -1 //数字标记。判断是否为多位数
symbolLoopTimes := 0 //记录公式循环到第几次
for _, s := range symbol {
symbolLoopTimes++
fmt.Printf("----%v----\n", symbolLoopTimes)
sType := judgmentType(string(s))
if sType == 0 {
//公式中的数字直接入栈。如果遍历连续数字,说明是多位数,需要拼接多位数。
var num int
if numTag == 1 {
prenumTmp, _ := numLinkStack.pop()
prenumI := prenumTmp.(int)
prenumA := strconv.Itoa(prenumI)
fmt.Println(prenumA + string(s))
num, _ = strconv.Atoi(prenumA + string(s))
} else {
num, _ = strconv.Atoi(string(s))
}
//如果数字前的符号为减号,则改为加号,且数字变为负数。
if symbolTag == 1 {
symbolLinkStack.pop() //减号出栈
symbolLinkStack.push("+") //加号入栈
fmt.Println("提示:数字前面为负号,执行加号入栈、数字加负号")
num = -num //数字添加负号
symbolTag = -1 //操作结束,还原符号标记为-1
}
fmt.Println("-新数字入栈-", num)
numLinkStack.push(num)
numTag = 1
} else if sType == 1 || sType == 2 {
numTag = -1
//符号如果为符号,修改符号标记为1
if string(s) == "-" {
symbolTag = 1
}
if symbolLinkStack.next == nil {
fmt.Println("-新符号入栈-", string(s))
symbolLinkStack.push(string(s))
} else {
tmpSymbol := symbolLinkStack.next.data //查看栈顶符号
preSymbol, _ := tmpSymbol.(string)
nowSymbol := string(s)
//获取当前符合和前一个符号的优先级,如果符号是(,则(优先级最低
var prePriority int
if preSymbol == "(" {
fmt.Println("-前一个符号是(-")
prePriority = 0
} else {
prePriority = judgmentType(preSymbol)
}
nowPriority := judgmentType(nowSymbol)
if prePriority >= nowPriority { //如果前一个运算符优先级大于后一个运算符优先级
fmt.Printf("提示: 栈顶符号%v优先级大于或等于当前符号%v优先级\n", preSymbol, nowSymbol)
fmt.Println("-旧符号出栈-", preSymbol)
symbolLinkStack.pop() //栈顶旧运算符弹出
fmt.Println("-新符号入栈-", nowSymbol)
symbolLinkStack.push(nowSymbol) //新运算符入栈
//两数根据符号进行运算,并入栈
calculatedNum(numLinkStack, preSymbol)
} else if prePriority < nowPriority { //如果前一个运算符优先级小于后一个运算符优先级
fmt.Printf("提示: 栈顶符号%v优先级小于当前符号%v优先级\n", preSymbol, nowSymbol)
fmt.Println("-新符号入栈-", nowSymbol)
symbolLinkStack.push(nowSymbol)
}
}
} else if sType == 3 {
fmt.Println("-左括号直接入栈-", string(s))
symbolLinkStack.push(string(s))
symbolTag = -1 //左括号前如有减号,还原符号标记为-1
} else if sType == 4 {
fmt.Println("-右括号不入栈-", string(s))
//遇到),循环出栈所有栈中运算符并计算。遇到(时停止
loopCalculationStack(numLinkStack, symbolLinkStack)
} else {
return -1, errors.New("公式格式不合法")
}
}
// 遍历完公式后,计算栈内剩余的元素
fmt.Println("---公式遍历结束,计算栈中剩余元素---")
loopCalculationStack(numLinkStack, symbolLinkStack)
//所有计算完成后,结果被存入numLinkStack,将结果出栈
resultTmp, err := numLinkStack.pop()
result := resultTmp.(int)
if err != nil {
fmt.Println("获取计算公式最终返回值时报错:", err)
}
return result, nil
}
//循环出栈symbolLinkStack中的符号,并进行运算。如遇到左括号(或栈为空时结束循环。
func loopCalculationStack(numLinkStack *LinkStack, symbolLinkStack *LinkStack) {
for {
if symbolLinkStack.next == nil {
fmt.Println("栈中无运算符号")
break
}
tmpSymbol, _ := symbolLinkStack.pop()
preSymbol := tmpSymbol.(string)
fmt.Println("-旧符号出栈-", preSymbol)
if preSymbol == "(" {
fmt.Println("提示: 遇到(, (出栈, 结束遍历符号栈的计算操作。")
break
} else {
//两数根据符号进行运算,并入栈
calculatedNum(numLinkStack, preSymbol)
}
}
}
//两数根据符号进行运算,并入栈
func calculatedNum(numLinkStack *LinkStack, symbol string) {
v2, err := numLinkStack.pop()
if err != nil {
fmt.Println(err)
}
v1, err := numLinkStack.pop()
if err != nil {
fmt.Println(err)
}
num1 := v1.(int)
num2 := v2.(int)
fmt.Printf("-旧数字出栈- num2: %v; num1: %v\n", num2, num1)
calculatedValue := calculatedFormula(num1, num2, symbol)
//计算数值入栈
fmt.Println("-计算结果入栈-", calculatedValue)
numLinkStack.push(calculatedValue)
}
由于我的代码中增加了很多输出提示类信息,所以感兴趣代码运行过程的话(数字、运算符出入栈和计算过程) ,可以看一下的详细输出。
---初始化链栈---
计算公式: (43-40)*(2-(8+3))
---遍历公式---
----1----
-左括号直接入栈- (
----2----
-新数字入栈- 4
----3----
43
-新数字入栈- 43
----4----
-前一个符号是(-
提示: 栈顶符号(优先级小于当前符号-优先级
-新符号入栈- -
----5----
提示:数字前面为负号,执行加号入栈、数字加负号
-新数字入栈- -4
----6----
-40
-新数字入栈- -40
----7----
-右括号不入栈- )
-旧符号出栈- +
-旧数字出栈- num2: -40; num1: 43
提示: 计算num1 + num2
-计算结果入栈- 3
-旧符号出栈- (
提示: 遇到(, (出栈, 结束遍历符号栈的计算操作。
----8----
-新符号入栈- *
----9----
-左括号直接入栈- (
----10----
-新数字入栈- 2
----11----
-前一个符号是(-
提示: 栈顶符号(优先级小于当前符号-优先级
-新符号入栈- -
----12----
-左括号直接入栈- (
----13----
-新数字入栈- 8
----14----
-前一个符号是(-
提示: 栈顶符号(优先级小于当前符号+优先级
-新符号入栈- +
----15----
-新数字入栈- 3
----16----
-右括号不入栈- )
-旧符号出栈- +
-旧数字出栈- num2: 3; num1: 8
提示: 计算num1 + num2
-计算结果入栈- 11
-旧符号出栈- (
提示: 遇到(, (出栈, 结束遍历符号栈的计算操作。
----17----
-右括号不入栈- )
-旧符号出栈- -
-旧数字出栈- num2: 11; num1: 2
提示: 计算num1 - num2
-计算结果入栈- -9
-旧符号出栈- (
提示: 遇到(, (出栈, 结束遍历符号栈的计算操作。
---公式遍历结束,计算栈中剩余元素---
-旧符号出栈- *
-旧数字出栈- num2: -9; num1: 3
提示: 计算num1 * num2
-计算结果入栈- -27
栈中无运算符号
---最终计算结果---
计算公式: (43-40)*(2-(8+3))
公式计算结果: -27