mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
1288 字
3 分钟
数据结构:最小栈
2026-05-13

最小栈是在普通栈上多加一个能力:getMin() 要在 O(1) 时间返回当前栈里的最小值。

如果每次 getMin() 都从栈底扫到栈顶,当然能找到答案,但时间复杂度是 O(n)。题目要求所有操作都是常数时间,所以不能等到查询时再计算,必须在入栈时就把答案准备好。

核心思路:

栈的每一层,都应该知道”如果栈只剩到我这一层,当前最小值是谁”。

题目概览#

来源内容
class015GetMinStack

题型: 带最小值查询的栈设计。

核心做法: 准备两个栈,一个存真实数据,一个同步存”当前层往下的最小值快照”。

易错点: datamin 必须同步进出;有重复最小值时不能丢掉上一层的最小状态;数组实现里 size 的读写顺序要统一。

155. 最小栈#

题目链接

实现一个 MinStack,支持:

  • push(int val):压入元素
  • pop():弹出栈顶
  • top():返回栈顶元素
  • getMin():返回当前栈中的最小元素

题目保证 poptopgetMin 都会在非空栈上调用。

LeetCode 调用过程

如下:

MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); // 返回 -3
minStack.pop();
minStack.top(); // 返回 0
minStack.getMin(); // 返回 -2

给每一层存一份最小值快照#

我们准备两个栈:

  • data:正常存数据
  • min:和 data 一样高,每一层存”从栈底到当前层的最小值”

下面的栈按”左边是栈底,右边是栈顶”来看:

push(-2)
data: [-2]
min: [-2] 当前最小值是 -2
push(0)
data: [-2, 0]
min: [-2, -2] 0 不是最小值,但 min 仍然压入一层,记录"此时最小值仍是 -2"
push(-3)
data: [-2, 0, -3]
min: [-2, -2, -3] 当前最小值更新成 -3
getMin() → -3
pop()
data: [-2, 0]
min: [-2, -2] -3 被同步弹出,最小值自然回到 -2
top() → 0
getMin() → -2

重点在第二步:0 本身不是最小值,但 min 仍然压入一层 -2。因为这一层表达的是——当栈顶是 0 时,整个栈的最小值是 -2

这就是”快照”的含义:每次 push 都把当时的最小值记录下来。以后 pop 时,真实数据弹一层,快照也弹一层,状态自然回到上一层。

整个设计只靠一个不变量撑住:

不变量

datamin 永远等高。data 的栈顶是当前元素,min 的栈顶是当前最小值。

解法一:辅助栈#

核心思路#

  • data[i]:第 i 层真实压入的值
  • min[i]:从栈底到第 i 层的最小值

每次 push(val) 时,真实值压入 data;同时把 val 和上一层最小值中更小的那个压入 min。每次 pop() 时,两个栈一起弹。因为栈顶元素离开后,它对应的最小值快照也应该一起失效。

解法示意图

代码实现#

import java.util.Stack;
class MinStack {
private Stack<Integer> data;
private Stack<Integer> min;
public MinStack() {
data = new Stack<Integer>();
min = new Stack<Integer>();
}
public void push(int val) {
data.push(val);
// min 和 data 同步压入一层,保存当前栈的最小值快照
if (min.isEmpty()) {
min.push(val);
} else {
min.push(Math.min(val, min.peek()));
}
}
public void pop() {
data.pop();
min.pop();
}
public int top() {
return data.peek();
}
public int getMin() {
return min.peek();
}
}

这份写法最舒服的地方在 pop():不用判断弹出的元素是不是最小值。因为 min 不是”最小值出现记录表”,而是”每一层的最小值快照”,直接同步弹出即可。

变量角色#

变量作用
data真实栈,保存所有压入的元素
min辅助栈,min.peek() 永远是当前最小值

为什么不只在变小时压入 min#

另一种常见写法是:只有当 val <= min.peek() 时,才把 val 压入 min。这当然也可以,但它有两个额外负担:

  1. pop() 时需要判断弹出的值是否等于 min.peek()
  2. 遇到重复最小值时,必须用 <=,不能只用 <

例如:

push(2)
push(2)
pop()
getMin()

如果第二个 2 没有压进 min,第一次 pop() 就把唯一的 2min 里弹掉,栈里明明还剩一个 2getMin() 却已经没有正确答案了。

同步快照写法虽然多存了一些重复最小值,但胜在简单稳定:入栈一起入,出栈一起出。

解法二:数组模拟#

如果想避开 Stack<Integer> 的对象开销,也可以用数组模拟两个栈。题目里总操作次数最多是 3 × 10^4,数组容量开到 30001 就足够。

class MinStack {
private static final int MAXN = 30001;
private int[] data;
private int[] min;
private int size;
public MinStack() {
data = new int[MAXN];
min = new int[MAXN];
size = 0;
}
public void push(int val) {
data[size] = val;
min[size] = size == 0 ? val : Math.min(val, min[size - 1]);
size++;
}
public void pop() {
size--;
}
public int top() {
return data[size - 1];
}
public int getMin() {
return min[size - 1];
}
}

size 表示”下一个可写位置”,所以代码顺序要保持一致:

push: 先写 data[size] 和 min[size],再 size++
pop: size--
top: data[size - 1]
min: min[size - 1]

易错点#

min 忘记同步弹出#

错误写法:

public void pop() {
data.pop();
// 漏掉 min.pop()
}

真实栈已经少了一层,但 min 还停在旧状态。之后 getMin() 可能返回一个已经被弹掉的值。

正确写法:

public void pop() {
data.pop();
min.pop();
}

min 理解成”只存最小值”#

同步快照写法里,min 不是只存新纪录:

data: [3, 5, 2, 4]
min: [3, 2] ← 错误:只记录变小的值

而应该是:

data: [3, 5, 2, 4]
min: [3, 3, 2, 2] ← 正确:每层都有对应的最小值快照

每个 data 位置都有对应的 min 位置,这样 pop 才能无脑同步。

数组版搞乱 size 的含义#

数组写法里最怕一会儿把 size 当”当前最后一个元素位置”,一会儿又当”下一个可写位置”。选一个含义后就不要变。

复杂度#

操作时间复杂度说明
push()O(1)真实栈和辅助栈各压入一次
pop()O(1)两个栈同步弹出一次
top()O(1)直接看真实栈栈顶
getMin()O(1)直接看辅助栈栈顶

空间复杂度:O(n)。除了真实数据外,还要为每一层额外保存一个最小值快照。

记法#

最小栈不是在查询时找最小值,而是在入栈时就把答案存好。

真实栈存元素,辅助栈存每一层的最小值快照;进一起进,出一起出,辅助栈顶就是答案。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

数据结构:最小栈
https://github.com/algorithmzuo/algorithm-journey/tree/main/src/class015
作者
黎明
发布于
2026-05-13
许可协议
MIT

部分信息可能已经过时

目录