茶艺师学算法打卡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]
提示:
题目分析
这里就很清楚,只有今后的气温上升了,才考虑,气温还没今日高,直接记成“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
}