茶艺师学算法打卡17:基于 go 语言,栈在算法题中的应用两例

157 阅读4分钟

茶艺师学算法打卡17:基于 go 语言,栈在算法题中的应用两例

前言

这里借助 LeetCode 的两道算法题,体验一下如何应用栈。
因为 go 语言原生没有栈这数据结构,同时在这里说说基于 go 语言的两个模拟实现方法。

栈的模拟

栈的性质,可以在这里回顾一下:《茶艺师学算法打卡4:栈、队列》。
至于用 go 语言来模拟,我们可以使用数组和切片。
切片模拟栈的行为非常直观,只要记得切片的最后是栈顶,那么切片的 append 就是栈的入栈,然后切片取除最后一位部分的子切片,可以视为栈的出栈,如图所示:

至于使用数组,对于我这初学者(在职茶艺师)来说真的是有带点抽象。
我后来总结出这一句话来帮助理解,“数组栈,入栈出栈不过是栈顶指针的左右移动。”

  • 入栈,先把栈顶指针往右挪一位,再把元素放入这个新位置
  • 出栈,指针往左挪一位,原来位置上的元素不用删,会在以后的入栈中被覆盖掉

如同所示:

栈的应用

基本应用

像叠碟子那样,后进先出且只能从顶往下“拿”,这样的栈,可以用来模拟计算器的加减乘除,甚至包含括号的计算。
面试题 16.26. 计算器
题目说明
给定一个包含正整数、加(+)、减(-)、乘()、除(/)的算数表达式(括号除外),计算其结果。
表达式仅包含非负整数,+, - ,
,/ 四种运算符和空格 。 整数除法仅保留整数部分。
示例 1:

输入: "3+2*2"
输出: 7

示例 2:

输入: " 3/2 "
输出: 1

示例 3:

输入: " 3+5 / 2 "
输出: 5

说明:
你可以假设所给定的表达式都是有效的。
请不要使用内置的库函数 eval。
题目分析
这里如何计算表达式呢?
根据四则运算的规则,可以将表达式分成两部分,数字与运算符号,分别入各自的栈。
关键在于符号的栈,当在栈顶的符号运算优先级高于等于当前表达式的,那就出掉该栈顶运算符,同时去数字栈里取两个数字远端,其结果再压入数字栈里,这过程如图所示:

在扫描完表达式后,如果符号的栈非空,那就按照“取一个符号配上两个数字”,一直算完符号的栈,最后在数字栈里的,就是计算结果。
参考代码

const N = 10000

type EvalStack struct {
    Nums [N]int
    NumsIdx int 
    Opts [N]byte
    OptsIdx int
}

func (e *EvalStack) NumPush(x int) {
    e.NumsIdx++
    e.Nums[e.NumsIdx] = x
}

func (e *EvalStack) OptPush(x byte) {
    e.OptsIdx++
    e.Opts[e.OptsIdx] = x
}

func (e *EvalStack) NumPop() int {
    res := e.Nums[e.NumsIdx]
    e.NumsIdx--
    return res
}

func (e *EvalStack) OptPop() byte {
    res := e.Opts[e.OptsIdx]
    e.OptsIdx--
    return res
}

func (e *EvalStack) NumTop() int {
    return e.Nums[e.NumsIdx]
}

func (e *EvalStack) OptTop() byte {
    return e.Opts[e.OptsIdx]
}

func (e *EvalStack) Eval() {
    b := e.NumPop()
    a := e.NumPop()
    op := e.OptPop()
    x := 0
    switch op {
        case '+':
        x = a + b 
        case '-':
        x = a - b 
        case '*':
        x = a * b 
        case '/':
        x = a / b
    }
    e.NumPush(x)
}

func isNum(x byte) bool {
    if x < '0' || x > '9' {
        return false 
    }
    return true
}

func calculate(s string) int {
    var e EvalStack
    // 标记运算符的等级
    level := map[byte]int{'+': 1, '-': 1, '*': 2, '/': 2}
    for i := 0; i < len(s); i++ {
        c := s[i] 
        if isNum(c) {
            j, x := i, 0 
            for j < len(s) && isNum(s[j]) {
                x = x * 10 + int(s[j] - '0')
                j++
            }
            i = j - 1
            e.NumPush(x)
        } else if c == ' ' {
            continue
        } else {
            for e.OptsIdx > 0 && level[e.OptTop()] >= level[c] {
                e.Eval()
            }
            e.OptPush(c) 
        } 
    }
    for e.OptsIdx > 0 {
        e.Eval()
    }
    return e.NumTop()
}

单调栈

上面的是第一种用法,常规的用法。
现在我们来开个脑洞,假如,我们进入栈的东西,是排好序的,要么是进去的一个比一个大,又要么是进去的一个比一个小。
这就是大家口中的单调栈
这栈用起来的特点是,可以选择性忽略掉一些不符合条件带考察元素。
我们来看 739. 每日温度 这题来感受一下。
题目说明
给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。
示例 1:

输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]

示例 2:

输入: temperatures = [30,40,50,60]
输出: [1,1,1,0]

示例 3:

输入: temperatures = [30,60,90]
输出: [1,1,0]

提示:

1<=temperatures.length<=1051 <= temperatures.length <= 105
30<=temperatures[i]<=10030 <= temperatures[i] <= 100

题目分析
这里就很清楚,只有今后的气温上升了,才考虑,气温还没今日高,直接记成“0”。
那么可以维护一个栈,记录着温度的下标,后来进的都比先进的温度底,当要进入比栈顶的温度高,就要把栈顶的都出掉。
选择从右往左遍历温度数组,栈顶的温度下标减去当前所在的温度下标,就是答案所要的“从今完后出现最高温度的天数”。

参考代码

func dailyTemperatures(temperatures []int) []int {
    n := len(temperatures)
    ans := make([]int, n)
    st := []int{} 
    for i := n - 1; i >= 0; i-- {
        t := temperatures[i]
        for len(st) > 0 && t >= temperatures[st[len(st) - 1]] {
            st = st[:len(st) - 1]
        }
        if len(st) > 0 {
            ans[i] = st[len(st) - 1] - i
        }
        st = append(st, i)
    }
    return ans
}