📌 题目链接:155. 最小栈 - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:栈、设计、辅助栈
⏱️ 目标时间复杂度:O(1) (每个操作)
💾 空间复杂度:O(n)
我们遇到了一道经典的数据结构设计题:最小栈(Min Stack) 。这道题不仅考察你对栈基本操作的理解,更深入检验你是否掌握如何通过辅助结构优化查询性能的能力。
这类题目在面试中极其常见,尤其是大厂系统设计或基础能力轮次——因为“常数时间获取最小值”看似简单,实则暗藏玄机。今天我们就来彻底拆解它!
🔍 题目分析
题目要求我们实现一个特殊的栈 MinStack,支持以下 五个操作:
push(val):将元素压入栈pop():弹出栈顶元素top():返回栈顶元素(不弹出)getMin():在 O(1) 时间内返回当前栈中的最小元素
⚠️ 关键约束:
- 所有操作必须在 常数时间 O(1) 内完成;
pop、top、getMin只在非空栈上调用;- 值域为
[-2^31, 2^31 - 1],即标准int范围。
💡 核心难点:
普通栈无法直接获取最小值(需遍历,O(n))。如何在不牺牲时间效率的前提下,动态维护最小值?
🛠️ 核心算法及代码讲解:辅助栈(Auxiliary Stack)
✅ 算法思想:空间换时间 + 同步维护最小值
我们引入一个辅助栈 min_stack,与主栈 x_stack 同步操作,但只存储“到当前位置为止的最小值”。
🎯 关键洞察:
对于任意时刻,若主栈顶部是元素
a,那么从栈底到a的所有元素中,最小值是确定的。我们可以在a入栈时就记录这个最小值,并存入辅助栈。
这样,getMin() 就退化为 min_stack.top(),天然 O(1)!
🔄 操作规则:
| 操作 | 主栈 x_stack | 辅助栈 min_stack |
|---|---|---|
push(x) | 压入 x | 压入 min(min_stack.top(), x) |
pop() | 弹出栈顶 | 同步弹出栈顶 |
getMin() | — | 返回 min_stack.top() |
📌 注意:初始化时,
min_stack可先压入INT_MAX,避免空栈判断(C++ 版常用技巧)。
💻 C++ 核心代码(带逐行注释)
class MinStack {
stack<int> x_stack; // 主栈:存储所有元素
stack<int> min_stack; // 辅助栈:存储对应位置的最小值
public:
MinStack() {
min_stack.push(INT_MAX); // 初始化辅助栈,避免空栈比较
}
void push(int x) {
x_stack.push(x); // 主栈正常入栈
// 辅助栈入栈:当前最小值 = min(之前最小值, 新元素)
min_stack.push(min(min_stack.top(), x));
}
void pop() {
x_stack.pop(); // 主栈弹出
min_stack.pop(); // 辅助栈同步弹出
}
int top() {
return x_stack.top(); // 返回主栈栈顶
}
int getMin() {
return min_stack.top(); // 辅助栈栈顶即为当前最小值
}
};
✅ 为什么这样是对的?
- 每次
push时,min_stack记录的是 从栈底到当前元素的全局最小值; pop时同步弹出,保证两个栈始终对齐;- 因此
min_stack.top()永远代表当前主栈的最小值。
🧩 解题思路(分步拆解)
-
明确需求:需要 O(1) 获取最小值 → 普通栈不行,必须预存信息。
-
选择策略:使用辅助数据结构 → 栈天然匹配 LIFO 特性,选辅助栈。
-
设计同步机制:
- 入栈时,辅助栈压入
min(当前最小, 新值); - 出栈时,辅助栈同步弹出;
- 入栈时,辅助栈压入
-
处理边界:
- 初始化辅助栈为
INT_MAX,简化逻辑; - 题目保证非空调用,无需额外判空(但实际工程建议加)。
- 初始化辅助栈为
-
验证示例:
push(-2): x=[-2], min=[INT_MAX, -2] push(0): x=[-2,0], min=[..., -2] push(-3): x=[-2,0,-3], min=[..., -3] getMin → -3 ✅ pop → x=[-2,0], min=[..., -2] getMin → -2 ✅
📊 算法分析
| 项目 | 分析 |
|---|---|
| 时间复杂度 | 所有操作均为 O(1) 。每个操作最多调用两次栈操作(push/pop/top),而栈操作本身是 O(1)。 |
| 空间复杂度 | O(n) ,其中 n 是 push 操作次数。最坏情况下,两个栈各存 n 个元素。 |
| 是否可优化空间? | 有!可以只在新元素 ≤ 当前最小值时才压入辅助栈(见 JS 版本),但 C++ 官方解法采用“全同步”更简洁,且面试中两种都可接受。 |
| 面试高频点 | ✅ 如何 O(1) 获取最值? ✅ 辅助栈 vs 单变量?(单变量无法处理 pop 后最小值恢复问题) ✅ 能否用 vector/deque 实现?(可以,但 stack 语义更清晰) |
💡 延伸思考:如果要求
getMax()呢?同理,再加一个max_stack即可!
💻 完整代码
✅ C++ 版本
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
class MinStack {
stack<int> x_stack;
stack<int> min_stack;
public:
MinStack() {
min_stack.push(INT_MAX);
}
void push(int x) {
x_stack.push(x);
min_stack.push(min(min_stack.top(), x));
}
void pop() {
x_stack.pop();
min_stack.pop();
}
int top() {
return x_stack.top();
}
int getMin() {
return min_stack.top();
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
MinStack minStack;
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
cout << minStack.getMin() << "\n"; // -3
minStack.pop();
cout << minStack.top() << "\n"; // 0
cout << minStack.getMin() << "\n"; // -2
return 0;
}
✅ JavaScript 版本(空间优化版:仅当 ≤ 当前最小值才入辅助栈)
const MinStack = function(){
this.stack = []; // 主栈
this.minStack = []; // 辅助栈:只存“历史最小值”
};
MinStack.prototype.push = function(x){
this.stack.push(x);
// 优化:只有新值 ≤ 当前最小值,才入辅助栈
if(this.minStack.length === 0 || x <= this.minStack[this.minStack.length-1]){
this.minStack.push(x);
}
};
MinStack.prototype.pop = function(){
if(this.stack.length === 0) return;
const popped = this.stack.pop();
// 如果弹出的是当前最小值,辅助栈也要弹出
if(popped === this.minStack[this.minStack.length-1]){
this.minStack.pop();
}
};
MinStack.prototype.top = function(){
return this.stack[this.stack.length-1];
};
MinStack.prototype.getMin = function(){
return this.minStack[this.minStack.length-1];
};
📌 JS 与 C++ 版本差异说明:
- C++ 版:全同步辅助栈,代码简洁,空间 O(n),适合快速实现;
- JS 版:条件入栈,空间更优(最坏仍是 O(n),但平均更省),适合强调空间效率的场景;
- 面试时两种都正确,可主动说明 trade-off!
🌟 总结 & 面试要点
-
核心思想:用辅助栈预存状态,实现 O(1) 查询;
-
适用场景:任何需要“动态维护极值 + LIFO 结构”的问题;
-
易错点:
- 忘记同步 pop;
- 初始化未处理空栈;
- 比较时用
<而非<=(导致重复最小值丢失);
-
进阶变体:
- 最大栈;
- 支持
getAverage()的栈(需维护 sum); - 支持 undo 操作的栈(需历史快照)。
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!