当前:LC155 · 最小栈 · 首次出现于 Day 12 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC155 · Min Stack · 栈

最小栈:用「同步最小值」换 O(1) getMin

单个 min 字段弹掉最小值后回不去。每个栈高对应的最小值恰好也是栈,入栈出栈一起走。

主栈存值。辅助栈在每一次 push 时压入「当前栈内最小值」:min(x, 辅助栈顶)。pop 时两栈一起弹。getMin 只看辅助栈顶。也可以只在新值 ≤ 当前最小时才压辅助栈,弹出时值等于辅助栈顶再同步弹——空间更省,语义仍是「栈顶永远是当前最小」。所有操作 O(1)。

时间 O(1) 每次操作空间 O(n)结论先行 · 全文约 6 节
导读

这是 LeetCode 155. Min Stack。设计一个栈,支持 push、pop、top,还要支持 getMin:返回栈里当前最小的元素。题目要求这四件事都在常数时间完成。栈空时不会调用 pop / top / getMin,不必为这些边界另写一套。

主例按顺序 push(3)、push(2)、push(5)。栈里是 3、2、5,当前最小是 2。弹出栈顶 5 之后最小仍是 2;再弹出 2,最小回退成 3。对照只存一个 min 字段:弹出 2 的那一刻,3 已经被覆盖,回不去。

朴素做法每次 getMin 扫一遍主栈,正确但线性。缺的不是「现在谁最小」这一帧,而是弹出之后如何回到上一帧。本文用手走主例的三次入栈和两次出栈,看辅助栈怎样把最小值的历史叠起来。

一个 min 字段存不住回退

push、pop、top 本来就是 O(1)。getMin 若现场扫栈,最小栈这道题就不成立——题目要的是读栈顶一样快的最小值。

只在结构体里留一个 min 行不行?push 时 min = min(min, x) 没问题。主例压入 3 再压入 2,min 从 3 改成 2。接着弹出 2,min 必须回到 3,字段里已经只剩 2。最小值的历史被覆盖了。

缺口是一份和栈高对齐的最小值记录:每多一个元素,就多一次「此刻全局最小」;每少一个元素,就丢掉这一次记录,露出上一层。后进先出,这正是栈。于是不是再发明一种结构,而是再放一个栈,专门存每个时刻的最小值。

为什么必须是栈getMin 问的是「当前栈」的最小,不是「历史上出现过的最小」。弹出最小值之后,要回到弹出前那一帧。帧和栈高一一对应,辅助结构也必须是栈。

入栈时把当前最小一并压进去

约定:主栈存值,最小栈存「压入这个值之后,整个主栈的最小值」。push(x) 时主栈压 x;最小栈压 min(x, 最小栈当前栈顶)。最小栈为空时,当前最小就是 x 自己。

这样最小栈顶永远等于主栈里所有数的最小值。getMin 读这个栈顶,top 读主栈栈顶,都是 O(1)。

用手走主例。push(3):两边都空,压入 3 和 3。push(2):min(2, 3) = 2,主栈 [3, 2],最小栈 [3, 2]。push(5):min(5, 2) = 2,主栈 [3, 2, 5],最小栈 [3, 2, 2]。5 不是新的最小,最小栈仍然记下一层 2——因为 5 还在栈里时,最小确实还是 2。演示三帧就是这三次同步。

每次 push 都同步压入当前最小值
push(3)
主栈
3
最小栈
3
min(3, ∞) = 3

弹出时两栈一起回到上一帧

pop 把主栈栈顶和最小栈栈顶同时拿掉。主例弹出 5:主栈回到 [3, 2],最小栈回到 [3, 2],getMin 仍是 2。再弹出 2:两边都回到 [3],getMin 回到 3。演示先停在弹出 5 之后,再停在弹出 2 之后。

同步弹出安全,是因为每一层最小值都是在对应那次 push 时算好的。5 入栈时记下的是 2,5 出栈就把这层 2 拿走,露出下面那层仍然是 2——那一层属于还在栈里的 2。2 出栈才把真正的最小值拿走,露出 3。

下面的 Go 代码是省空间的写法:新值严格大于当前最小时,最小栈不压。主例 push(5) 不会往 mins 里再放一个 2,mins 停在 [3, 2]。弹出时只有「弹出的值等于最小栈顶」才同步弹,弹出 5 时 5 != 2,mins 不动;弹出 2 时 2 == 2,mins 弹掉 2。getMin 的结果和逐层同步完全一样,只是重复的最小值少存了几份。等于当前最小也要压,否则栈里有两个相同最小值时,弹出一个会把另一个的记录一并毁掉。

pop 弹出 2 后,最小值回退到 3
pop()
主栈
32
最小栈
32
两栈同步弹出 5 / 2

Go:双栈实现

solution.goGo
type MinStack struct {
stack []int
mins []int
}
func Constructor() MinStack { return MinStack{} }
func (s *MinStack) Push(val int) {
s.stack = append(s.stack, val)
if len(s.mins) == 0 || val <= s.mins[len(s.mins)-1] {
s.mins = append(s.mins, val)
}
}
func (s *MinStack) Pop() {
if s.stack[len(s.stack)-1] == s.mins[len(s.mins)-1] {
s.mins = s.mins[:len(s.mins)-1]
}
s.stack = s.stack[:len(s.stack)-1]
}
func (s *MinStack) Top() int { return s.stack[len(s.stack)-1] }
func (s *MinStack) GetMin() int { return s.mins[len(s.mins)-1] }

1主栈存值,mins 只在「新最小值出现」时长一截,栈顶仍是当前最小。

2val <= 栈顶才压。等于也要压:栈里两个 2 时,弹出一个还必须留下一个 2。

3弹出的值等于 mins 栈顶,说明拿走的正是当前最小,mins 才同步缩短。

4Top 读主栈顶,GetMin 读 mins 顶,都不再扫描。

总结

每个栈高对应一份最小值。主例 [3,2,5] 同步出 [3,2,2];弹出 5 最小仍是 2,弹出 2 才回到 3。

  • 单个 min 字段能更新、不能回退。弹出最小值后,上一帧的最小已经被覆盖。
  • 演示用的是逐层同步:每次 push 都记下当前最小。代码只在新值 ≤ 当前最小时压辅助栈,结果相同。
  • getMin 永远读辅助栈顶。四则操作都是 O(1),额外空间最坏 O(n)。
同族题目
LC150逆波兰表达式求值LC20有效的括号LC394字符串解码