GO语言-栈的应用-表达式求值

目录

栈的应用-表达式求值

1.实现功能

2.需要注意的问题 

2.1 解决运算符优先级问题

2.2 如何利用栈解决表达式求值

2.3 括号优先级和运算问题

2.4 减号运算符的避坑指南

3.代码实现

3.1代码拆解详解

        3.1.1栈的定义和出栈、入栈操作

        3.1.2一些工具函数

        3.1.3逻辑实现主体

3.2全量代码及执行结果


先吐槽一下:学习的时候以为实现逻辑蛮清晰简单的。代码写起来好几百行...可能是自己的代码能力太弱了。

栈的应用-表达式求值

1.实现功能

  • 表达式可包含运算符号:+、-、*、/、(、)
  • 公式中的数字支持多位数
  • 如果公式有奇奇怪怪的字符则报错

2.需要注意的问题 

2.1 解决运算符优先级问题

  • 我们平时见到的表达式都是中缀表达式,就是运算符号在两个数之间的。如:1+2*3
  • 在利用栈进行表达式求值时,我们可以利用后缀表达式的思路来解决运算符的优先级问题。后缀表达式就是运算符号在两个数的后面。如:123*+(先做23*,再做16+)
  • 遇到优先级一样的两个运算符时,遵循从左至右计算。

2.2 如何利用栈解决表达式求值

  • 准备两个栈,一个用于存储数字,另一个用于存储运算符。
  • 从左至右读取表达式。识别出数字和运算符,分别存入不同的栈。遍历数字时需要考虑多位数的情况
  • 遇到运算数时,直接入栈,等待被计算。
  • 遇到运算符号时入栈,等待与下一个运算符号比较优先级。如果前一个符号优先级高,后一个符号优先级低,优先级低的入栈,优先级高的计算;如果前一个符号优先级低,后一个符号优先级高,优先级低的入栈,优先级高的继续与下一个符号比较(这里高优先级的乘除法还要与下一个符号比较的是考虑代码的扩展性,以后扩展可能会有更高优先级的运算符,如幂运算)。
  • 遇到右括号)时,需要循环出栈进行算术运算,直至遇到左括号(。
  • 表达式从左至右遍历完后,虽然过程中可以做算术运算,但栈中可能还会存在数字和运算符。最终需要将所有数字和运算符遍历出栈后计算。

2.3 括号优先级和运算问题

  • 遇到左括号(时。左括号没有入栈时其优先级比所有运算符都要高,需要入栈比较下一个运算符。
  • 遇到左括号(时。左括号已入栈,拿出来与下一个运算符比较时,其优先级比所有运算符都要低
  • 遇到右括号)时。开始抛出栈中的运算符号,直至遇到栈中的左括(号。

2.4 减号运算符的避坑指南

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)

3.代码实现

3.1代码拆解详解

3.1.1栈的定义和出栈、入栈操作

考虑到表达式可能长度无法预估,和确保操作的便利性。我选择栈的链式存储结构-链栈来实现。

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
}

3.1.2一些工具函数

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
	}
}

3.1.3逻辑实现主体

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 计算结果
}

3.2全量代码及执行结果

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

你可能感兴趣的:(Golang,数据结构与算法,go,数据结构)