力扣刷题#22-0155-最小栈
力扣刷题#22-0155-最小栈
题目
设计一个支持 push、pop、top 操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack 类:
| 方法 | 说明 |
|---|---|
MinStack() |
初始化栈对象 |
void push(int value) |
将元素 value 推入栈 |
void pop() |
删除栈顶元素 |
int top() |
获取栈顶元素 |
int getMin() |
获取栈中最小元素 |
输入:
["MinStack","push","push","push","getMin","pop","top","getMin"]
[[],[-2],[0],[-3],[],[],[],[]]
输出:
[null,null,null,null,-3,null,0,-2]
我的思路之旅
第一反应:排序数组?太蠢了
看到 getMin 要 O(1),我第一反应是——在 push 里塞个数组,每次 push 就排序,getMin 直接返回第一个元素。
但马上意识到这太慢了:每 push 一次就是 O(n log n)。栈的优势全被浪费了。
关键洞察:辅助栈存"历史最小值"
既然 getMin 要在最值变化时立即感知,那最简单的方式就是——用一个独立的栈,专门记录"到当前层为止出现过的最小值"。
push(x):
minStack.push( min(x, minStack.top() 或 x) ) ← 同步记录
mainStack.push(x)
这样 getMin() 直接看 minStack.top(),O(1)。
跑一遍例子
操作序列: push 5, push 3, push 7, push 2, getMin, pop, getMin
| 操作 | mainStack | minStack | minStack.top() 逻辑 |
|---|---|---|---|
| push 5 | [5] | [5] | 栈空,5 就是最小值 |
| push 3 | [5,3] | [5,3] | min(3, 5)=3,压入 3 |
| push 7 | [5,3,7] | [5,3,3] | min(7, 3)=3,压入 3 |
| push 2 | [5,3,7,2] | [5,3,3,2] | min(2, 3)=2,压入 2 |
| getMin | — | — | 返回 2 ✓ |
| pop | [5,3,7] | [5,3,3] | 两栈同步弹出 |
| getMin | — | — | 返回 3 ✓ |
pop 之后最小值自动回退到 3——不需要扫描全栈重建最值。
我的 AC 代码
class MinStack {
public:
stack<int> minstack, mainstack;
MinStack() { }
void push(int value) {
if (minstack.empty()) {
minstack.push(value);
} else {
minstack.push(min(minstack.top(), value));
}
mainstack.push(value);
}
void pop() {
mainstack.pop();
minstack.pop();
}
int top() {
return mainstack.top();
}
int getMin() {
return minstack.top();
}
};
代码逐段解析
第 3 行:成员变量
stack<int> minstack, mainstack;
两个栈都是类的成员变量,不是局部变量。所有方法共享它们。调用 push/pop 都是操作同一个 minstack 和 mainstack。
第 4 行:构造函数
MinStack() { }
两个 stack<int> 默认构造就是空栈,构造函数不需要做任何事。
第 6-12 行:push
void push(int value) {
if (minstack.empty()) {
minstack.push(value); // 栈空 → 当前值就是最小值
} else {
minstack.push(min(minstack.top(), value)); // 和前一个最小值比
}
mainstack.push(value);
}
minstack 的层数和 mainstack 始终一样。每一层存的都是"从栈底到当前层的最小值"。
第 14-17 行:pop
void pop() {
mainstack.pop();
minstack.pop();
}
两栈同步弹出。pop 后最小值自动回退到上一层的记录。
第 19-25 行:top 和 getMin
int top() {
return mainstack.top();
}
int getMin() {
return minstack.top();
}
各看各的栈顶,都是 O(1)。
踩坑记录
坑 1:栈写成了局部变量
// ❌ 第一版
MinStack() {
stack<int> minstack, mainstack; // 局部变量,构造函数结束就销毁
}
构造函数退出后,push/pop/top/getMin 里用的 minstack 和 mainstack 压根不存在,编译报错。
修复:移到 public:(或 private:)作为类成员变量。
坑 2:std::min 需要 <algorithm>
minstack.push(min(minstack.top(), value));
力扣环境默认包含了,但本地编译须加 #include <algorithm>。
优化分析
为什么跑了 168ms / 5%?
首次提交跑出 168ms 不代表代码慢——力扣服务器负载和测试用例分配有波动。同一份代码重新提交常常跳到 10ms 以内。
但确实存在一个可优化的点:minStack 里存了大量重复值。
push 5 → minStack: [5]
push 3 → minStack: [5, 3]
push 7 → minStack: [5, 3, 3] ← 7 大于最小值 3,没必要多存一个 3
push 2 → minStack: [5, 3, 3, 2]
优化:只在最小值被打破时才压入 minStack(惰性 push):
void push(int value) {
mainstack.push(value);
if (minstack.empty() || value <= minstack.top()) {
minstack.push(value); // 只有 ≤ 当前最小值才压
}
}
void pop() {
if (mainstack.top() == minstack.top()) {
minstack.pop(); // 弹出的恰是最小值,同步弹
}
mainstack.pop();
}
| 对比 | 原版 | 惰性版 |
|---|---|---|
| push 5→3→7→2 | minStack=[5,3,3,2] | minStack=[5,3,2] |
| getMin | O(1) | O(1) |
| 内存 | 每次 push 都分配 | 仅最值打破时分配 |
注意
<=不能写成<——用<的话连续 push 两个相同最小值时,pop 一个就会把唯一记录弹出,getMin拿不到正确值。
极速方案(1ms 提交的秘诀)
力扣上 1ms 的提交通常用差值编码法——只用一个栈,不存值本身,存"值与当前最小值的差值",通过数学编码还原:
push(x): 如果 x < min,推入 2x - min 并更新 min;否则推入 x - min
pop: 如果栈顶 < 0,恢复上一个 min = min - 栈顶
top: 如果栈顶 < 0,返回 min;否则返回 min + 栈顶
为什么快?因为完全避开了 std::min 函数调用和双栈同步开销,纯加减法。但这属于"卷到底"的极限优化,面试中标准双栈版已足够。
关键点总结
| 关键点 | 说明 |
|---|---|
| 核心结构 | 双栈——主栈存数据,辅助栈存"历史最小值" |
| push | 两栈同步压入,minStack 存 min(top, value) |
| pop | 两栈同步弹出,最小值自动回退 |
| getMin | 看 minStack 栈顶,O(1) |
| 成员 vs 局部 | 栈必须是成员变量,不能写在构造函数里 |
复杂度分析
| 维度 | 值 | 说明 |
|---|---|---|
| 时间复杂度 | 全部 O(1) | push/pop/top/getMin 均为常数 |
| 空间复杂度 | O(n) | 两栈各存 n 个元素 |
本文档由 AI 辅助生成,作者提供问题,思路和代码,AI仅负责文本修饰,综合获得以上内容。

浙公网安备 33010602011771号