🔥 LeetCode Hot100 最小栈秒杀指南:双栈 + 单调栈双解法,面试再也不怕被追问!
作为面试高频 “守门员” 题,LeetCode 155. 最小栈堪称 “算法基础必过关卡”—— 看似简单的 push/pop/top/getMin 操作,却藏着时间复杂度与空间复杂度的权衡艺术。很多开发者刷题时只会死记 “双栈模板”,一旦面试官追问 “能不能用一个栈实现?”“为什么辅助栈要存重复最小值?” 就当场卡壳。
今天这篇文章,从问题本质拆解→双栈标准解法→单栈极致优化→面试追问应对,用最通俗的语言 + 可视化步骤 + 可运行代码,带你吃透最小栈的核心逻辑,不仅能秒杀 Hot100 原题,还能轻松应对 90% 的面试变种问题!
🚀 问题直击:最小栈的核心矛盾是什么?
先明确问题要求(LeetCode 155 原题):
设计一个支持 push ,pop ,top 操作,并能在常数时间 O (1) 内检索到最小元素的栈。
常规栈的 push/pop/top 都是 O (1),但getMin 要 O (1) 是关键难点—— 如果每次 getMin 都遍历栈找最小值,时间复杂度会变成 O (n),不符合要求。
核心矛盾:栈的 “后进先出” 特性与最小值的 “随机访问” 需求的冲突。解决方案的核心思路:用额外空间 “缓存” 最小值,或通过 “编码技巧” 在单个栈中同时存储数据和最小值信息。
🌟 解法一:双栈法(面试首选,简单易懂)
1. 原理:用 “辅助栈” 专门缓存最小值
- 主栈(dataStack) :正常存储所有元素,负责 push/pop/top 操作;
- 辅助栈(minStack) :专门存储 “当前栈中的最小值”,确保栈顶永远是最小值。
2. 关键规则(避免踩坑!)
- push 时:如果新元素≤辅助栈栈顶(注意是 “≤” 不是 “<”,避免重复最小值被覆盖),则新元素同时入辅助栈;
- pop 时:如果主栈弹出的元素等于辅助栈栈顶(说明弹出的是当前最小值),则辅助栈也同步弹出;
- getMin 时:直接返回辅助栈栈顶元素(O (1))。
3. 可视化步骤(举例:push 3→2→5→2)
| 操作 | 主栈(dataStack) | 辅助栈(minStack) | 最小值 |
|---|---|---|---|
| push 3 | [3] | [3] | 3 |
| push 2 | [3,2] | [3,2] | 2 |
| push 5 | [3,2,5] | [3,2] | 2 |
| push 2 | [3,2,5,2] | [3,2,2] | 2 |
| pop | [3,2,5] | [3,2] | 2 |
| pop | [3,2] | [3,2] | 2 |
| pop | [3] | [3] | 3 |
4. 完整代码(JavaScript 实现)
javascript
运行
class MinStack {
constructor() {
this.dataStack = []; // 主栈:存所有元素
this.minStack = []; // 辅助栈:存当前最小值
}
// 入栈操作
push(val) {
this.dataStack.push(val);
// 辅助栈为空 或 新元素≤当前最小值 → 同步入栈
if (this.minStack.length === 0 || val <= this.minStack[this.minStack.length - 1]) {
this.minStack.push(val);
}
}
// 出栈操作
pop() {
const top = this.dataStack.pop();
// 弹出的是当前最小值 → 辅助栈同步弹出
if (top === this.minStack[this.minStack.length - 1]) {
this.minStack.pop();
}
return top;
}
// 获取栈顶元素
top() {
return this.dataStack[this.dataStack.length - 1];
}
// 获取最小值(O(1))
getMin() {
return this.minStack[this.minStack.length - 1];
}
}
// 测试代码
const minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
console.log(minStack.getMin()); // -3
minStack.pop();
console.log(minStack.top()); // 0
console.log(minStack.getMin()); // -2
5. 复杂度分析
- 时间复杂度:所有操作(push/pop/top/getMin)均为 O (1);
- 空间复杂度:O (n),最坏情况下(元素单调递减)辅助栈需存储所有元素。
⚡ 解法二:单栈法(极致优化,面试加分项)
如果面试官问 “能不能不用辅助栈?”,单栈法就是你的 “加分答案”—— 核心思路是在栈中存储 “元素与当前最小值的差值” ,通过差值反推最小值,无需额外空间。
1. 原理:用 “差值编码” 替代辅助栈
-
栈中存储的不是原始元素,而是「当前元素 - 上一个最小值」的差值;
-
用一个变量
minVal实时记录当前栈中的最小值; -
通过差值的正负性判断:
- 若差值≥0:说明当前元素≥上一个最小值,原始元素 = minVal + 差值;
- 若差值 < 0:说明当前元素是新的最小值,原始元素 = minVal(因为差值 = 当前元素 - 上一个最小值 → 当前元素 = minVal),且上一个最小值 = 当前元素 - 差值(反推旧最小值)。
2. 可视化步骤(举例:push 3→2→5→2)
| 操作 | 栈中存储(差值) | minVal(当前最小值) | 原始元素推导 |
|---|---|---|---|
| push 3 | [0] | 3 | 3(差值 0=3-3) |
| push 2 | [0, -1] | 2 | 2(差值 - 1=2-3 → 新 minVal=2) |
| push 5 | [0, -1, 3] | 2 | 5(差值 3=5-2 ≥0 → 5=2+3) |
| push 2 | [0, -1, 3, 0] | 2 | 2(差值 0=2-2 ≥0 → 2=2+0) |
| pop | [0, -1, 3] | 2 | 弹出差值 0 → 原始元素 = 2+0=2 |
| pop | [0, -1] | 3 | 弹出差值 3 → 原始元素 = 2+3=5 |
| pop | [0] | 3 | 弹出差值 - 1 → 旧 minVal=2 - (-1)=3,原始元素 = 2 |
3. 完整代码(JavaScript 实现)
javascript
运行
class MinStack {
constructor() {
this.stack = []; // 栈中存储「元素与当前最小值的差值」
this.minVal = null; // 实时记录当前最小值
}
push(val) {
if (this.stack.length === 0) {
// 栈为空时,差值为0,minVal=当前元素
this.stack.push(0);
this.minVal = val;
} else {
const diff = val - this.minVal; // 计算差值
this.stack.push(diff);
// 差值<0 → 当前元素是新的最小值
if (diff < 0) {
this.minVal = val;
}
}
}
pop() {
const diff = this.stack.pop();
let topVal;
if (diff < 0) {
// 弹出的是最小值,反推上一个最小值
topVal = this.minVal;
this.minVal = this.minVal - diff; // 旧minVal = 当前minVal - 差值
} else {
// 弹出的不是最小值,原始值=当前minVal + 差值
topVal = this.minVal + diff;
}
return topVal;
}
top() {
const diff = this.stack[this.stack.length - 1];
return diff < 0 ? this.minVal : this.minVal + diff;
}
getMin() {
return this.minVal;
}
}
// 测试代码
const minStack2 = new MinStack();
minStack2.push(-2);
minStack2.push(0);
minStack2.push(-3);
console.log(minStack2.getMin()); // -3
minStack2.pop();
console.log(minStack2.top()); // 0
console.log(minStack2.getMin()); // -2
4. 复杂度分析
- 时间复杂度:所有操作均为 O (1);
- 空间复杂度:O (n),但无额外辅助栈,实际运行时内存占用比双栈法低(尤其适合大数据量场景)。
🎯 面试追问:这些坑你必须知道!
-
双栈法中,辅助栈为什么要存 “≤” 栈顶的元素? 答:避免重复最小值被覆盖。比如 push 2→2,若辅助栈只存 “<”,则第二个 2 不会入栈,pop 第一个 2 后,辅助栈栈顶还是 2(正确);但如果是 push 2→3→2,pop 3 后,第二个 2 需要成为最小值,若辅助栈没存第二个 2,就会错误返回 3。
-
单栈法中,差值可能出现溢出吗? 答:在 JavaScript 中不会,因为 JS 的 Number 是 64 位浮点数,支持很大范围;但在 Java/C++ 等语言中,若元素是 int 类型,可能出现 “当前元素 - 上一个最小值” 溢出(比如当前元素是 Integer.MIN_VALUE,上一个最小值是正数),此时需用 long 类型存储差值。
-
两种方法的适用场景? 答:
- 双栈法:代码简单易懂,维护成本低,适合日常开发和面试基础题;
- 单栈法:空间利用率更高,适合对内存敏感的场景(如嵌入式开发),但逻辑稍复杂,面试时能讲清原理可加分。
📝 实战总结:秒杀最小栈的 3 个关键
- 理解核心矛盾:栈的 LIFO 特性与 getMin 的 O (1) 需求冲突,解决方案是 “缓存最小值”;
- 双栈法是基础:必须熟练掌握,面试时优先写双栈法(不易出错);
- 单栈法是进阶:记住 “差值编码” 思路,能应对面试官的优化追问。
最后,建议大家动手实现两种解法,再做几道变种题(如 “实现最大栈”“栈中元素频次统计”),彻底巩固栈的操作和复杂度优化思维~
你在面试中遇到过最小栈的哪些变种问题?欢迎在评论区分享你的经历!👇